From 2e23ce1737a3b3ef990b385ecc84dc06947fae94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Tue, 31 May 2022 16:18:37 +0200 Subject: [PATCH] Rewrite long-range actors Create more fine-grained events for long-range actors to react to changes in the cell structure and box geometry. Convert Cython code to Python code with the ScriptInterface framework and remove global variables of CPU methods. Fix regressions in the energy and pressure analysis module. Split electrostatic and magnetostatic methods. --- doc/sphinx/electrostatics.rst | 65 +- doc/sphinx/integration.rst | 3 + doc/sphinx/magnetostatics.rst | 44 +- doc/sphinx/reaction_methods.rst | 3 +- .../charged_system/charged_system.ipynb | 17 +- .../ferrofluid/ferrofluid_part1.ipynb | 8 +- .../ferrofluid/ferrofluid_part2.ipynb | 18 +- .../ferrofluid/ferrofluid_part3.ipynb | 2 +- samples/load_checkpoint.py | 1 + samples/p3m.py | 16 +- samples/visualization_elc.py | 4 +- src/config/CMakeLists.txt | 3 +- src/config/config.hpp | 7 - src/config/version.hpp.in | 5 +- src/core/CMakeLists.txt | 18 +- src/core/EspressoSystemInterface.cpp | 10 + src/core/EspressoSystemInterface.hpp | 7 + src/core/actor/CMakeLists.txt | 6 - src/core/actor/DipolarBarnesHut.cpp | 102 -- src/core/actor/DipolarDirectSum.cpp | 89 - src/core/actor/Mmm1dgpuForce.hpp | 84 - src/core/actor/registration.hpp | 60 + .../common.hpp => actor/traits.hpp} | 14 +- src/core/actor/visit_try_catch.hpp | 49 + src/core/actor/visitors.hpp | 156 ++ .../bonded_interactions/bonded_coulomb_sr.hpp | 36 +- src/core/cell_system/RegularDecomposition.cpp | 10 +- .../cluster_analysis/ClusterStructure.hpp | 2 +- src/core/constraints/ShapeBasedConstraint.cpp | 16 +- src/core/cuda_interface.cpp | 6 + src/core/cuda_interface.hpp | 1 + .../electrostatics/CMakeLists.txt} | 29 +- src/core/electrostatics/actor.hpp | 62 + src/core/electrostatics/coulomb.cpp | 390 +++++ src/core/electrostatics/coulomb.hpp | 147 ++ src/core/electrostatics/coulomb_inline.hpp | 212 +++ src/core/electrostatics/debye_hueckel.hpp | 112 ++ src/core/electrostatics/elc.cpp | 1224 ++++++++++++++ src/core/electrostatics/elc.hpp | 346 ++++ src/core/electrostatics/icc.cpp | 286 ++++ src/core/electrostatics/icc.hpp | 111 ++ .../mmm-common.hpp | 7 +- .../mmm-modpsi.cpp | 0 .../mmm-modpsi.hpp | 7 +- .../mmm1d.cpp | 225 ++- src/core/electrostatics/mmm1d.hpp | 126 ++ .../mmm1d_gpu.cpp} | 63 +- src/core/electrostatics/mmm1d_gpu.hpp | 108 ++ .../mmm1d_gpu_cuda.cu} | 295 ++-- src/core/electrostatics/p3m.cpp | 790 +++++++++ src/core/electrostatics/p3m.hpp | 251 +++ src/core/electrostatics/p3m_gpu.cpp | 57 + src/core/electrostatics/p3m_gpu.hpp | 63 + .../p3m_gpu_cuda.cu | 61 +- .../p3m_gpu_cuda.cuh} | 16 +- .../p3m_gpu_error.hpp | 8 +- .../p3m_gpu_error_cuda.cu | 0 src/core/electrostatics/reaction_field.hpp | 124 ++ src/core/electrostatics/registration.hpp | 87 + src/core/electrostatics/scafacos.hpp | 96 ++ src/core/electrostatics/scafacos_impl.cpp | 140 ++ src/core/electrostatics/scafacos_impl.hpp | 98 ++ .../specfunc.cpp | 5 +- .../specfunc.cuh} | 7 +- .../specfunc.hpp | 12 +- .../CMakeLists.txt | 22 - .../ScafacosContext.cpp | 248 --- .../ScafacosContext.hpp | 107 -- .../electrostatics_magnetostatics/coulomb.cpp | 450 ----- .../electrostatics_magnetostatics/coulomb.hpp | 99 -- .../coulomb_inline.hpp | 181 -- .../debye_hueckel.hpp | 85 - .../electrostatics_magnetostatics/dipole.cpp | 300 ---- .../electrostatics_magnetostatics/dipole.hpp | 101 -- .../dipole_inline.hpp | 69 - .../electrostatics_magnetostatics/elc.cpp | 1369 --------------- .../electrostatics_magnetostatics/elc.hpp | 156 -- .../electrostatics_magnetostatics/icc.cpp | 272 --- .../electrostatics_magnetostatics/icc.hpp | 130 -- .../magnetic_non_p3m_methods.hpp | 82 - .../mdlc_correction.cpp | 518 ------ .../mdlc_correction.hpp | 86 - .../electrostatics_magnetostatics/mmm1d.hpp | 87 - .../p3m-common.cpp | 199 --- .../p3m-dipolar.cpp | 1501 ----------------- .../p3m-dipolar.hpp | 287 ---- .../electrostatics_magnetostatics/p3m.cpp | 1360 --------------- .../electrostatics_magnetostatics/p3m.hpp | 247 --- .../reaction_field.cpp | 51 - .../reaction_field.hpp | 95 -- .../scafacos.cpp | 164 -- .../scafacos.hpp | 61 - src/core/energy.cpp | 46 +- src/core/energy.hpp | 3 - src/core/energy_inline.hpp | 53 +- src/core/errorhandling.cpp | 8 + src/core/errorhandling.hpp | 3 + src/core/event.cpp | 110 +- src/core/event.hpp | 11 +- src/core/forces.cpp | 46 +- src/core/forces.hpp | 3 - src/core/forces_inline.hpp | 96 +- src/core/grid.cpp | 5 +- src/core/interactions.cpp | 30 +- .../core/magnetostatics/CMakeLists.txt | 31 +- src/core/magnetostatics/barnes_hut_gpu.cpp | 98 ++ .../barnes_hut_gpu.hpp} | 41 +- .../barnes_hut_gpu_cuda.cu} | 23 +- .../barnes_hut_gpu_cuda.cuh} | 10 +- src/core/magnetostatics/dds.cpp | 139 ++ src/core/magnetostatics/dds.hpp | 58 + src/core/magnetostatics/dds_gpu.cpp | 81 + .../dds_gpu.hpp} | 35 +- .../dds_gpu_cuda.cu} | 2 +- .../dds_gpu_cuda.cuh} | 7 +- .../dds_replica.cpp} | 170 +- src/core/magnetostatics/dds_replica.hpp | 60 + src/core/magnetostatics/dipoles.cpp | 218 +++ src/core/magnetostatics/dipoles.hpp | 122 ++ src/core/magnetostatics/dipoles_inline.hpp | 125 ++ src/core/magnetostatics/dlc.cpp | 512 ++++++ src/core/magnetostatics/dlc.hpp | 170 ++ src/core/magnetostatics/dp3m.cpp | 925 ++++++++++ src/core/magnetostatics/dp3m.hpp | 304 ++++ src/core/magnetostatics/registration.hpp | 65 + .../scafacos.hpp} | 49 +- src/core/magnetostatics/scafacos_impl.cpp | 99 ++ src/core/magnetostatics/scafacos_impl.hpp | 73 + .../nonbonded_interaction_data.cpp | 2 +- src/core/nonbonded_interactions/thole.hpp | 32 +- src/core/npt.cpp | 14 +- .../actors.pxd => core/p3m/CMakeLists.txt} | 14 +- src/core/p3m/TuningAlgorithm.cpp | 323 ++++ src/core/p3m/TuningAlgorithm.hpp | 183 ++ src/core/p3m/TuningLogger.hpp | 136 ++ src/core/p3m/common.cpp | 165 ++ .../p3m-common.hpp => p3m/common.hpp} | 244 +-- .../data_struct.hpp} | 9 +- .../fft.cpp | 23 +- .../fft.hpp | 20 +- .../influence_function.hpp} | 85 +- .../influence_function_dipolar.hpp} | 31 +- .../interpolation.hpp} | 6 +- .../p3m_send_mesh.cpp => p3m/send_mesh.cpp} | 43 +- .../p3m_send_mesh.hpp => p3m/send_mesh.hpp} | 8 +- .../BondCriterion.hpp} | 26 +- .../DistanceCriterion.hpp} | 45 +- src/core/pair_criteria/EnergyCriterion.hpp | 53 + src/core/pair_criteria/PairCriterion.hpp | 48 + src/core/pair_criteria/pair_criteria.hpp | 97 -- src/core/pressure.cpp | 28 +- src/core/pressure_inline.hpp | 57 +- src/core/scafacos/CMakeLists.txt | 3 + src/core/scafacos/ScafacosContext.cpp | 51 + src/core/scafacos/ScafacosContext.hpp | 69 + src/core/scafacos/ScafacosContextBase.cpp | 51 + .../ScafacosContextBase.hpp | 52 +- src/core/tuning.cpp | 69 +- src/core/tuning.hpp | 35 +- .../EspressoSystemStandAlone_test.cpp | 45 +- src/core/unit_tests/p3m_test.cpp | 8 +- src/python/espressomd/actors.py | 99 ++ src/python/espressomd/actors.pyx | 246 --- src/python/espressomd/analyze.pxd | 15 +- .../espressomd/electrostatic_extensions.pxd | 52 - .../espressomd/electrostatic_extensions.py | 170 ++ .../espressomd/electrostatic_extensions.pyx | 193 --- src/python/espressomd/electrostatics.pxd | 149 -- src/python/espressomd/electrostatics.py | 549 ++++++ src/python/espressomd/electrostatics.pyx | 737 -------- src/python/espressomd/gen_code_info.py | 38 +- src/python/espressomd/lb.pxd | 8 +- src/python/espressomd/lb.pyx | 162 +- .../espressomd/magnetostatic_extensions.pyx | 95 -- src/python/espressomd/magnetostatics.pxd | 74 - src/python/espressomd/magnetostatics.py | 391 +++++ src/python/espressomd/magnetostatics.pyx | 404 ----- src/python/espressomd/p3m_common.pxd | 32 - src/python/espressomd/reaction_methods.py | 2 +- src/python/espressomd/scafacos.pxd | 30 - src/python/espressomd/scafacos.pyx | 135 -- src/python/espressomd/utils.pxd | 1 + src/python/espressomd/utils.pyx | 67 +- src/scafacos/CMakeLists.txt | 20 +- src/scafacos/Scafacos.cpp | 187 -- src/scafacos/Scafacos.hpp | 110 -- src/scafacos/include/scafacos/Coulomb.hpp | 99 ++ src/scafacos/include/scafacos/Dipoles.hpp | 53 + src/scafacos/include/scafacos/Scafacos.hpp | 66 + src/scafacos/src/Coulomb.cpp | 97 ++ src/scafacos/src/Dipoles.cpp | 66 + src/scafacos/src/Scafacos.cpp | 111 ++ .../Actor.hpp => scafacos/src/utils.hpp} | 29 +- src/script_interface/CMakeLists.txt | 3 + .../cell_system/CellSystem.hpp | 13 +- .../cluster_analysis/ClusterStructure.hpp | 2 +- src/script_interface/electrostatics/Actor.hpp | 80 + .../electrostatics/Actor_impl.hpp | 98 ++ .../electrostatics/CMakeLists.txt | 2 + .../electrostatics/CoulombMMM1D.hpp | 74 + .../electrostatics/CoulombMMM1DGpu.hpp | 71 + .../electrostatics/CoulombP3M.hpp | 99 ++ .../electrostatics/CoulombP3MGPU.hpp | 102 ++ .../electrostatics/CoulombScafacos.hpp | 116 ++ .../electrostatics/DebyeHueckel.hpp | 66 + .../ElectrostaticLayerCorrection.hpp | 120 ++ .../electrostatics/ICCStar.hpp | 122 ++ .../electrostatics/ReactionField.hpp | 72 + .../electrostatics/initialize.cpp | 74 + .../electrostatics/initialize.hpp} | 26 +- src/script_interface/initialize.cpp | 4 + src/script_interface/magnetostatics/Actor.hpp | 73 + .../magnetostatics/Actor_impl.hpp | 51 + .../magnetostatics/CMakeLists.txt | 2 + .../magnetostatics/DipolarBarnesHutGpu.hpp | 63 + .../magnetostatics/DipolarDirectSum.hpp | 53 + .../magnetostatics/DipolarDirectSumGpu.hpp | 56 + .../DipolarDirectSumWithReplica.hpp | 62 + .../magnetostatics/DipolarLayerCorrection.hpp | 103 ++ .../magnetostatics/DipolarP3M.hpp | 97 ++ .../magnetostatics/DipolarScafacos.hpp | 83 + .../magnetostatics/initialize.cpp | 69 + .../magnetostatics/initialize.hpp} | 27 +- .../pair_criteria/BondCriterion.hpp | 58 + .../pair_criteria/DistanceCriterion.hpp | 59 + .../pair_criteria/EnergyCriterion.hpp | 58 + .../pair_criteria/PairCriterion.hpp | 54 + .../pair_criteria/initialize.cpp | 6 +- .../pair_criteria/pair_criteria.hpp | 106 -- src/script_interface/scafacos/CMakeLists.txt | 1 + src/script_interface/scafacos/scafacos.cpp | 203 +++ src/script_interface/scafacos/scafacos.hpp | 50 + src/script_interface/tests/Actors_test.cpp | 185 ++ src/script_interface/tests/CMakeLists.txt | 1 + testsuite/python/CMakeLists.txt | 26 +- testsuite/python/actor.py | 3 +- testsuite/python/analyze_energy.py | 21 +- testsuite/python/coulomb_cloud_wall.py | 74 +- .../python/coulomb_cloud_wall_duplicated.py | 16 +- testsuite/python/coulomb_interface.py | 251 +++ testsuite/python/coulomb_mixed_periodicity.py | 94 +- testsuite/python/coulomb_tuning.py | 53 +- testsuite/python/dawaanr-and-bh-gpu.py | 59 +- testsuite/python/dds-and-bh-gpu.py | 67 +- testsuite/python/dipolar_direct_summation.py | 18 +- testsuite/python/dipolar_interface.py | 166 +- .../dipolar_mdlc_p3m_scafacos_p2nfft.py | 14 +- testsuite/python/dipolar_mpi_exceptions.py | 47 - testsuite/python/dipolar_p3m.py | 60 +- testsuite/python/elc.py | 41 +- testsuite/python/elc_vs_analytic.py | 59 +- ...tions.py => electrostatic_interactions.py} | 51 +- testsuite/python/icc.py | 67 +- testsuite/python/icc_interface.py | 218 +++ testsuite/python/integrator_npt.py | 37 +- testsuite/python/long_range_actors.py | 358 ++++ testsuite/python/mmm1d.py | 79 +- .../python/p3m_electrostatic_pressure.py | 25 +- testsuite/python/p3m_fft.py | 4 +- testsuite/python/p3m_gpu.py | 48 - testsuite/python/p3m_tuning_exceptions.py | 481 +++--- testsuite/python/save_checkpoint.py | 10 +- testsuite/python/scafacos_dipoles_1d_2d.py | 167 +- testsuite/python/scafacos_interface.py | 267 ++- testsuite/python/test_checkpoint.py | 55 +- testsuite/python/tests_common.py | 68 +- testsuite/python/tune_skin.py | 2 +- testsuite/python/unittest_decorators.py | 7 + 268 files changed, 17175 insertions(+), 13649 deletions(-) delete mode 100644 src/core/actor/CMakeLists.txt delete mode 100644 src/core/actor/DipolarBarnesHut.cpp delete mode 100644 src/core/actor/DipolarDirectSum.cpp delete mode 100644 src/core/actor/Mmm1dgpuForce.hpp create mode 100644 src/core/actor/registration.hpp rename src/core/{electrostatics_magnetostatics/common.hpp => actor/traits.hpp} (68%) create mode 100644 src/core/actor/visit_try_catch.hpp create mode 100644 src/core/actor/visitors.hpp rename src/{python/espressomd/magnetostatic_extensions.pxd => core/electrostatics/CMakeLists.txt} (53%) create mode 100644 src/core/electrostatics/actor.hpp create mode 100644 src/core/electrostatics/coulomb.cpp create mode 100644 src/core/electrostatics/coulomb.hpp create mode 100644 src/core/electrostatics/coulomb_inline.hpp create mode 100644 src/core/electrostatics/debye_hueckel.hpp create mode 100644 src/core/electrostatics/elc.cpp create mode 100644 src/core/electrostatics/elc.hpp create mode 100644 src/core/electrostatics/icc.cpp create mode 100644 src/core/electrostatics/icc.hpp rename src/core/{electrostatics_magnetostatics => electrostatics}/mmm-common.hpp (92%) rename src/core/{electrostatics_magnetostatics => electrostatics}/mmm-modpsi.cpp (100%) rename src/core/{electrostatics_magnetostatics => electrostatics}/mmm-modpsi.hpp (91%) rename src/core/{electrostatics_magnetostatics => electrostatics}/mmm1d.cpp (56%) create mode 100644 src/core/electrostatics/mmm1d.hpp rename src/core/{actor/Mmm1dgpuForce.cpp => electrostatics/mmm1d_gpu.cpp} (50%) create mode 100644 src/core/electrostatics/mmm1d_gpu.hpp rename src/core/{actor/Mmm1dgpuForce_cuda.cu => electrostatics/mmm1d_gpu_cuda.cu} (63%) create mode 100644 src/core/electrostatics/p3m.cpp create mode 100644 src/core/electrostatics/p3m.hpp create mode 100644 src/core/electrostatics/p3m_gpu.cpp create mode 100644 src/core/electrostatics/p3m_gpu.hpp rename src/core/{electrostatics_magnetostatics => electrostatics}/p3m_gpu_cuda.cu (93%) rename src/core/{actor/ActorList.hpp => electrostatics/p3m_gpu_cuda.cuh} (74%) rename src/core/{electrostatics_magnetostatics => electrostatics}/p3m_gpu_error.hpp (88%) rename src/core/{electrostatics_magnetostatics => electrostatics}/p3m_gpu_error_cuda.cu (100%) create mode 100644 src/core/electrostatics/reaction_field.hpp create mode 100644 src/core/electrostatics/registration.hpp create mode 100644 src/core/electrostatics/scafacos.hpp create mode 100644 src/core/electrostatics/scafacos_impl.cpp create mode 100644 src/core/electrostatics/scafacos_impl.hpp rename src/core/{electrostatics_magnetostatics => electrostatics}/specfunc.cpp (99%) rename src/core/{actor/specfunc_cuda.hpp => electrostatics/specfunc.cuh} (98%) rename src/core/{electrostatics_magnetostatics => electrostatics}/specfunc.hpp (95%) delete mode 100644 src/core/electrostatics_magnetostatics/CMakeLists.txt delete mode 100644 src/core/electrostatics_magnetostatics/ScafacosContext.cpp delete mode 100644 src/core/electrostatics_magnetostatics/ScafacosContext.hpp delete mode 100644 src/core/electrostatics_magnetostatics/coulomb.cpp delete mode 100644 src/core/electrostatics_magnetostatics/coulomb.hpp delete mode 100644 src/core/electrostatics_magnetostatics/coulomb_inline.hpp delete mode 100644 src/core/electrostatics_magnetostatics/debye_hueckel.hpp delete mode 100644 src/core/electrostatics_magnetostatics/dipole.cpp delete mode 100644 src/core/electrostatics_magnetostatics/dipole.hpp delete mode 100644 src/core/electrostatics_magnetostatics/dipole_inline.hpp delete mode 100644 src/core/electrostatics_magnetostatics/elc.cpp delete mode 100644 src/core/electrostatics_magnetostatics/elc.hpp delete mode 100644 src/core/electrostatics_magnetostatics/icc.cpp delete mode 100644 src/core/electrostatics_magnetostatics/icc.hpp delete mode 100644 src/core/electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp delete mode 100644 src/core/electrostatics_magnetostatics/mdlc_correction.cpp delete mode 100644 src/core/electrostatics_magnetostatics/mdlc_correction.hpp delete mode 100644 src/core/electrostatics_magnetostatics/mmm1d.hpp delete mode 100644 src/core/electrostatics_magnetostatics/p3m-common.cpp delete mode 100644 src/core/electrostatics_magnetostatics/p3m-dipolar.cpp delete mode 100644 src/core/electrostatics_magnetostatics/p3m-dipolar.hpp delete mode 100644 src/core/electrostatics_magnetostatics/p3m.cpp delete mode 100644 src/core/electrostatics_magnetostatics/p3m.hpp delete mode 100644 src/core/electrostatics_magnetostatics/reaction_field.cpp delete mode 100644 src/core/electrostatics_magnetostatics/reaction_field.hpp delete mode 100644 src/core/electrostatics_magnetostatics/scafacos.cpp delete mode 100644 src/core/electrostatics_magnetostatics/scafacos.hpp rename testsuite/python/mmm1d_gpu.py => src/core/magnetostatics/CMakeLists.txt (57%) create mode 100644 src/core/magnetostatics/barnes_hut_gpu.cpp rename src/core/{actor/DipolarBarnesHut.hpp => magnetostatics/barnes_hut_gpu.hpp} (59%) rename src/core/{actor/DipolarBarnesHut_cuda.cu => magnetostatics/barnes_hut_gpu_cuda.cu} (99%) rename src/core/{actor/DipolarBarnesHut_cuda.cuh => magnetostatics/barnes_hut_gpu_cuda.cuh} (94%) create mode 100644 src/core/magnetostatics/dds.cpp create mode 100644 src/core/magnetostatics/dds.hpp create mode 100644 src/core/magnetostatics/dds_gpu.cpp rename src/core/{actor/DipolarDirectSum.hpp => magnetostatics/dds_gpu.hpp} (60%) rename src/core/{actor/DipolarDirectSum_cuda.cu => magnetostatics/dds_gpu_cuda.cu} (99%) rename src/core/{actor/DipolarDirectSum_cuda.cuh => magnetostatics/dds_gpu_cuda.cuh} (89%) rename src/core/{electrostatics_magnetostatics/magnetic_non_p3m_methods.cpp => magnetostatics/dds_replica.cpp} (54%) create mode 100644 src/core/magnetostatics/dds_replica.hpp create mode 100644 src/core/magnetostatics/dipoles.cpp create mode 100644 src/core/magnetostatics/dipoles.hpp create mode 100644 src/core/magnetostatics/dipoles_inline.hpp create mode 100644 src/core/magnetostatics/dlc.cpp create mode 100644 src/core/magnetostatics/dlc.hpp create mode 100644 src/core/magnetostatics/dp3m.cpp create mode 100644 src/core/magnetostatics/dp3m.hpp create mode 100644 src/core/magnetostatics/registration.hpp rename src/core/{electrostatics_magnetostatics/common.cpp => magnetostatics/scafacos.hpp} (51%) create mode 100644 src/core/magnetostatics/scafacos_impl.cpp create mode 100644 src/core/magnetostatics/scafacos_impl.hpp rename src/{python/espressomd/actors.pxd => core/p3m/CMakeLists.txt} (69%) create mode 100644 src/core/p3m/TuningAlgorithm.cpp create mode 100644 src/core/p3m/TuningAlgorithm.hpp create mode 100644 src/core/p3m/TuningLogger.hpp create mode 100644 src/core/p3m/common.cpp rename src/core/{electrostatics_magnetostatics/p3m-common.hpp => p3m/common.hpp} (58%) rename src/core/{electrostatics_magnetostatics/p3m-data_struct.hpp => p3m/data_struct.hpp} (89%) rename src/core/{electrostatics_magnetostatics => p3m}/fft.cpp (97%) rename src/core/{electrostatics_magnetostatics => p3m}/fft.hpp (95%) rename src/core/{electrostatics_magnetostatics/p3m_influence_function.hpp => p3m/influence_function.hpp} (76%) rename src/core/{electrostatics_magnetostatics/dp3m_influence_function.hpp => p3m/influence_function_dipolar.hpp} (92%) rename src/core/{electrostatics_magnetostatics/p3m_interpolation.hpp => p3m/interpolation.hpp} (98%) rename src/core/{electrostatics_magnetostatics/p3m_send_mesh.cpp => p3m/send_mesh.cpp} (82%) rename src/core/{electrostatics_magnetostatics/p3m_send_mesh.hpp => p3m/send_mesh.hpp} (95%) rename src/core/{electrostatics_magnetostatics/p3m_gpu.cpp => pair_criteria/BondCriterion.hpp} (50%) rename src/core/{electrostatics_magnetostatics/debye_hueckel.cpp => pair_criteria/DistanceCriterion.hpp} (52%) create mode 100644 src/core/pair_criteria/EnergyCriterion.hpp create mode 100644 src/core/pair_criteria/PairCriterion.hpp delete mode 100644 src/core/pair_criteria/pair_criteria.hpp create mode 100644 src/core/scafacos/CMakeLists.txt create mode 100644 src/core/scafacos/ScafacosContext.cpp create mode 100644 src/core/scafacos/ScafacosContext.hpp create mode 100644 src/core/scafacos/ScafacosContextBase.cpp rename src/core/{electrostatics_magnetostatics => scafacos}/ScafacosContextBase.hpp (59%) create mode 100644 src/python/espressomd/actors.py delete mode 100644 src/python/espressomd/actors.pyx delete mode 100644 src/python/espressomd/electrostatic_extensions.pxd create mode 100644 src/python/espressomd/electrostatic_extensions.py delete mode 100644 src/python/espressomd/electrostatic_extensions.pyx delete mode 100644 src/python/espressomd/electrostatics.pxd create mode 100644 src/python/espressomd/electrostatics.py delete mode 100644 src/python/espressomd/electrostatics.pyx delete mode 100644 src/python/espressomd/magnetostatic_extensions.pyx delete mode 100644 src/python/espressomd/magnetostatics.pxd create mode 100644 src/python/espressomd/magnetostatics.py delete mode 100644 src/python/espressomd/magnetostatics.pyx delete mode 100644 src/python/espressomd/p3m_common.pxd delete mode 100644 src/python/espressomd/scafacos.pxd delete mode 100644 src/python/espressomd/scafacos.pyx delete mode 100644 src/scafacos/Scafacos.cpp delete mode 100644 src/scafacos/Scafacos.hpp create mode 100644 src/scafacos/include/scafacos/Coulomb.hpp create mode 100644 src/scafacos/include/scafacos/Dipoles.hpp create mode 100644 src/scafacos/include/scafacos/Scafacos.hpp create mode 100644 src/scafacos/src/Coulomb.cpp create mode 100644 src/scafacos/src/Dipoles.cpp create mode 100644 src/scafacos/src/Scafacos.cpp rename src/{core/actor/Actor.hpp => scafacos/src/utils.hpp} (55%) create mode 100644 src/script_interface/electrostatics/Actor.hpp create mode 100644 src/script_interface/electrostatics/Actor_impl.hpp create mode 100644 src/script_interface/electrostatics/CMakeLists.txt create mode 100644 src/script_interface/electrostatics/CoulombMMM1D.hpp create mode 100644 src/script_interface/electrostatics/CoulombMMM1DGpu.hpp create mode 100644 src/script_interface/electrostatics/CoulombP3M.hpp create mode 100644 src/script_interface/electrostatics/CoulombP3MGPU.hpp create mode 100644 src/script_interface/electrostatics/CoulombScafacos.hpp create mode 100644 src/script_interface/electrostatics/DebyeHueckel.hpp create mode 100644 src/script_interface/electrostatics/ElectrostaticLayerCorrection.hpp create mode 100644 src/script_interface/electrostatics/ICCStar.hpp create mode 100644 src/script_interface/electrostatics/ReactionField.hpp create mode 100644 src/script_interface/electrostatics/initialize.cpp rename src/{core/actor/ActorList.cpp => script_interface/electrostatics/initialize.hpp} (61%) create mode 100644 src/script_interface/magnetostatics/Actor.hpp create mode 100644 src/script_interface/magnetostatics/Actor_impl.hpp create mode 100644 src/script_interface/magnetostatics/CMakeLists.txt create mode 100644 src/script_interface/magnetostatics/DipolarBarnesHutGpu.hpp create mode 100644 src/script_interface/magnetostatics/DipolarDirectSum.hpp create mode 100644 src/script_interface/magnetostatics/DipolarDirectSumGpu.hpp create mode 100644 src/script_interface/magnetostatics/DipolarDirectSumWithReplica.hpp create mode 100644 src/script_interface/magnetostatics/DipolarLayerCorrection.hpp create mode 100644 src/script_interface/magnetostatics/DipolarP3M.hpp create mode 100644 src/script_interface/magnetostatics/DipolarScafacos.hpp create mode 100644 src/script_interface/magnetostatics/initialize.cpp rename src/{core/electrostatics_magnetostatics/p3m_gpu.hpp => script_interface/magnetostatics/initialize.hpp} (61%) create mode 100644 src/script_interface/pair_criteria/BondCriterion.hpp create mode 100644 src/script_interface/pair_criteria/DistanceCriterion.hpp create mode 100644 src/script_interface/pair_criteria/EnergyCriterion.hpp create mode 100644 src/script_interface/pair_criteria/PairCriterion.hpp delete mode 100644 src/script_interface/pair_criteria/pair_criteria.hpp create mode 100644 src/script_interface/scafacos/CMakeLists.txt create mode 100644 src/script_interface/scafacos/scafacos.cpp create mode 100644 src/script_interface/scafacos/scafacos.hpp create mode 100644 src/script_interface/tests/Actors_test.cpp create mode 100644 testsuite/python/coulomb_interface.py delete mode 100644 testsuite/python/dipolar_mpi_exceptions.py rename testsuite/python/{electrostaticInteractions.py => electrostatic_interactions.py} (84%) create mode 100644 testsuite/python/icc_interface.py create mode 100644 testsuite/python/long_range_actors.py delete mode 100644 testsuite/python/p3m_gpu.py diff --git a/doc/sphinx/electrostatics.rst b/doc/sphinx/electrostatics.rst index 80aacbd702d..0ef0870e39e 100644 --- a/doc/sphinx/electrostatics.rst +++ b/doc/sphinx/electrostatics.rst @@ -47,11 +47,16 @@ call a tuning function to achieve the requested accuracy:: system = espressomd.System(box_l=[10, 10, 10]) system.time_step = 0.01 system.part.add(pos=[[0, 0, 0], [1, 1, 1]], q=[-1, 1]) - solver = espressomd.electrostatics.P3M(prefactor=2, accuracy=1e-3) + solver = espressomd.electrostatics.P3M(prefactor=2., accuracy=1e-3) system.actors.add(solver) where the prefactor is defined as :math:`C` in Eqn. :eq:`coulomb_prefactor`. +The list of actors can be cleared with +:meth:`system.actors.clear() ` and +:meth:`system.actors.remove(actor) `. + + .. _Coulomb P3M: Coulomb P3M @@ -73,27 +78,16 @@ If you are not sure, read the following references: Tuning Coulomb P3M ~~~~~~~~~~~~~~~~~~ -The tuning method is called when the handle of the Coulomb P3M is added to the -actor list. At this point, the system should already contain the charged -particles. Set parameters are fixed and not changed by the tuning algorithm. -This can be useful to speed up the tuning during testing or if the parameters -are already known. - -To prevent the automatic tuning, set the ``tune`` parameter to ``False``. -To manually tune or retune P3M, call :meth:`espressomd.electrostatics.P3M.tune -`. -Note, however, that this is a method the P3M object inherited from -:attr:`espressomd.electrostatics.ElectrostaticInteraction`. -All parameters passed to the method are fixed in the tuning routine. If not -specified in the ``tune()`` method, the parameters ``prefactor`` and -``accuracy`` are reused. - It is not easy to calculate the various parameters of the P3M method such that the method provides the desired accuracy at maximum speed. To simplify this, it provides a function to automatically tune the algorithm. Note that for this function to work properly, your system should already -contain an initial configuration of charges and the correct initial box -size. Also note that the provided tuning algorithms works very well on +contain an initial configuration of charges and the correct initial box size. +The tuning method is called when the handle of the Coulomb P3M is added to +the actor list. Some parameters can be fixed (``r_cut``, ``cao``, ``mesh``) +to speed up the tuning if the parameters are already known. + +Please note that the provided tuning algorithms works very well on homogeneous charge distributions, but might not achieve the requested precision for highly inhomogeneous or symmetric systems. For example, because of the nature of the P3M algorithm, systems are problematic @@ -106,7 +100,7 @@ obtain sets of parameters that yield the desired accuracy, then it measures how long it takes to compute the Coulomb interaction using these parameter sets and chooses the set with the shortest run time. -After execution the tuning routines report the tested parameter sets, +During tuning, the algorithm reports the tested parameter sets, the corresponding k-space and real-space errors and the timings needed for force calculations. In the output, the timings are given in units of milliseconds, length scales are in units of inverse box lengths. @@ -118,14 +112,16 @@ Coulomb P3M on GPU :class:`espressomd.electrostatics.P3MGPU` -The GPU implementation of P3M calculates the far field portion on the GPU. -It uses the same parameters and interface functionality as the CPU version of -the solver. It should be noted that this does not always provide significant +The GPU implementation of P3M calculates the far field contribution to the +forces on the GPU. The near-field contribution to the forces, as well as the +near- and far-field contributions to the energies are calculated on the CPU. +It uses the same parameters +and interface functionality as the CPU version of the solver. +It should be noted that this does not always provide significant increase in performance. Furthermore it computes the far field interactions -with only single precision which limits the maximum precision. The algorithm -does not work in combination with the electrostatic extensions -:ref:`Dielectric interfaces with the ICC* algorithm ` -and :ref:`Electrostatic Layer Correction (ELC)`. +with only single precision which limits the maximum precision. +The algorithm does not work in combination with the electrostatic extension +:ref:`Dielectric interfaces with the ICC* algorithm `. The algorithm doesn't have kernels to compute energies, and will therefore contribute 0 to the long-range potential energy of the system. This can be @@ -269,8 +265,6 @@ Electrostatic Layer Correction (ELC) systems. It can account for different dielectric jumps on both sides of the non-periodic direction. In more detail, it is a special procedure that converts a 3D electrostatic method to a 2D method in computational order N. -Currently, it only supports P3M without GPU. This means, -that you will first have to set up the P3M algorithm before using ELC. The periodicity has to be set to (1 1 1). *ELC* cancels the electrostatic contribution of the periodic replica in **z-direction**. Make sure that you read the papers on ELC (:cite:`arnold02c,arnold02d,tyagi08a`) before using it. @@ -291,9 +285,15 @@ Usage notes: import espressomd.electrostatics p3m = espressomd.electrostatics.P3M(prefactor=1, accuracy=1e-4) - elc = espressomd.electrostatics.ELC(p3m_actor=p3m, gap_size=box_l * 0.2, maxPWerror=1e-3) + elc = espressomd.electrostatics.ELC(actor=p3m, gap_size=box_l * 0.2, maxPWerror=1e-3) system.actors.add(elc) +Although it is technically feasible to remove ``elc`` from the list of actors +and then to add the ``p3m`` object, it is not recommended because the P3M +parameters are mutated by *ELC*, e.g. the ``epsilon`` is made metallic. +It is safer to instantiate a new P3M object instead of recycling one that +has been adapted by *ELC*. + *ELC* can also be used to simulate 2D periodic systems with image charges, specified by dielectric contrasts on the non-periodic boundaries (:cite:`tyagi08a`). This is achieved by setting the dielectric jump from the @@ -303,7 +303,7 @@ simulation region (*middle*) to *bottom* (at :math:`z=0`) and from *middle* to are :math:`\Delta_t=\frac{\varepsilon_m-\varepsilon_t}{\varepsilon_m+\varepsilon_t}` and :math:`\Delta_b=\frac{\varepsilon_m-\varepsilon_b}{\varepsilon_m+\varepsilon_b}`:: - elc = espressomd.electrostatics.ELC(p3m_actor=p3m, gap_size=box_l * 0.2, maxPWerror=1e-3, + elc = espressomd.electrostatics.ELC(actor=p3m, gap_size=box_l * 0.2, maxPWerror=1e-3, delta_mid_top=0.9, delta_mid_bot=0.1) The fully metallic case :math:`\Delta_t=\Delta_b=-1` would lead to divergence @@ -313,7 +313,7 @@ of the forces/energies in *ELC* and is therefore only possible with the Toggle ``const_pot`` on to maintain a constant electric potential difference ``pot_diff`` between the xy-planes at :math:`z=0` and :math:`z = L_z - h`:: - elc = espressomd.electrostatics.ELC(p3m_actor=p3m, gap_size=box_l * 0.2, maxPWerror=1e-3, + elc = espressomd.electrostatics.ELC(actor=p3m, gap_size=box_l * 0.2, maxPWerror=1e-3, const_pot=True, delta_mid_bot=100.0) This is done by countering the total dipole moment of the system with the @@ -396,7 +396,8 @@ ScaFaCoS electrostatics |es| can use the methods from the ScaFaCoS *Scalable fast Coulomb solvers* library. The specific methods available depend on the compile-time options of -the library, and can be queried using :meth:`espressomd.scafacos.available_methods`. +the library, and can be queried with +:meth:`espressomd.electrostatics.Scafacos.get_available_methods`. To use ScaFaCoS, create an instance of :class:`~espressomd.electrostatics.Scafacos` and add it to the list of active actors. Three parameters have to be specified: diff --git a/doc/sphinx/integration.rst b/doc/sphinx/integration.rst index f75eab3dccf..dfe66078b02 100644 --- a/doc/sphinx/integration.rst +++ b/doc/sphinx/integration.rst @@ -303,6 +303,9 @@ The algorithm can also be used for energy minimization:: system.integrator.set_vv() Please note that not all features support energy calculation. +For example :ref:`IBM ` +and :ref:`OIF ` do not implement energy calculation for +mesh surface deformation. .. _Brownian Dynamics: diff --git a/doc/sphinx/magnetostatics.rst b/doc/sphinx/magnetostatics.rst index 01184177d75..3afc02ba3d1 100644 --- a/doc/sphinx/magnetostatics.rst +++ b/doc/sphinx/magnetostatics.rst @@ -35,14 +35,12 @@ Magnetostatic interactions are activated via the actor framework:: system.part.add(pos=[[0, 0, 0], [1, 1, 1]], rotation=2 * [(1, 1, 1)], dip=2 * [(1, 0, 0)]) - direct_sum = espressomd.magnetostatics.DipolarDirectSumCpu(prefactor=1) - system.actors.add(direct_sum) - # ... - system.actors.remove(direct_sum) + actor = espressomd.magnetostatics.DipolarDirectSumCpu(prefactor=1.) + system.actors.add(actor) -The magnetostatics algorithms for periodic boundary conditions require -some knowledge to use them properly. Uneducated use can result in -completely unphysical simulations. +The list of actors can be cleared with +:meth:`system.actors.clear() ` and +:meth:`system.actors.remove(actor) `. .. _Dipolar P3M: @@ -81,7 +79,7 @@ simulation, actual force and torque errors can be significantly larger. Dipolar Layer Correction (DLC) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:class:`espressomd.magnetostatic_extensions.DLC` +:class:`espressomd.magnetostatics.DLC` The dipolar layer correction (DLC) is used in conjunction with the dipolar P3M method to calculate dipolar interactions in a 2D-periodic system. @@ -104,15 +102,14 @@ Usage notes: assumed that all dipole moment are as large as the largest of the dipoles in the system. -The method is used as follows:: +* When the base solver is not a P3M method, metallic epsilon is assumed. - import espressomd.magnetostatics as magnetostatics - import espressomd.magnetostatic_extensions as magnetostatic_extensions +The method is used as follows:: - p3m = magnetostatics.DipolarP3M(prefactor=1, accuracy=1E-4) - dlc = magnetostatic_extensions.DLC(maxPWerror=1E-5, gap_size=2.) - system.actors.add(p3m) - system.actors.add(dlc) + import espressomd.magnetostatics + dp3m = espressomd.magnetostatics.DipolarP3M(prefactor=1, accuracy=1E-4) + mdlc = espressomd.magnetostatics.DLC(actor=dp3m, maxPWerror=1E-5, gap_size=2.) + system.actors.add(mdlc) .. _Dipolar direct sum: @@ -149,9 +146,9 @@ To use the methods, create an instance of either system's list of active actors. The only required parameter is the Prefactor :eq:`dipolar_prefactor`:: - import espressomd.magnetostatics - dds = espressomd.magnetostatics.DipolarDirectSumGpu(bjerrum_length=1) - system.actors.add(dds) + import espressomd.magnetostatics + dds = espressomd.magnetostatics.DipolarDirectSumGpu(prefactor=1) + system.actors.add(dds) For testing purposes, a variant of the dipolar direct sum is available which adds periodic copies to the system in periodic directions: @@ -184,9 +181,9 @@ refer to :cite:`polyakov13a`. To use the method, create an instance of :class:`~espressomd.magnetostatics.DipolarBarnesHutGpu` and add it to the system's list of active actors:: - import espressomd.magnetostatics - bh = espressomd.magnetostatics.DipolarBarnesHutGpu(prefactor=pf_dds_gpu, epssq=200.0, itolsq=8.0) - system.actors.add(bh) + import espressomd.magnetostatics + bh = espressomd.magnetostatics.DipolarBarnesHutGpu(prefactor=1., epssq=200.0, itolsq=8.0) + system.actors.add(bh) .. _ScaFaCoS magnetostatics: @@ -199,8 +196,9 @@ ScaFaCoS magnetostatics |es| can use the methods from the ScaFaCoS *Scalable fast Coulomb solvers* library for dipoles, if the methods support dipolar calculations. The feature ``SCAFACOS_DIPOLES`` has to be added to :file:`myconfig.hpp` to activate this -feature. Dipolar calculations are only included in the ``dipolar`` branch of -the ScaFaCoS code. +feature. Dipolar calculations are only included in the ``dipoles`` branch of +the ScaFaCoS code. The specific methods available can be queried with +:meth:`espressomd.electrostatics.Scafacos.get_available_methods`. To use ScaFaCoS, create an instance of :class:`~espressomd.magnetostatics.Scafacos` and add it to the list of active actors. Three parameters have to be specified: diff --git a/doc/sphinx/reaction_methods.rst b/doc/sphinx/reaction_methods.rst index 62c1c27a9cc..0c27ba5469f 100644 --- a/doc/sphinx/reaction_methods.rst +++ b/doc/sphinx/reaction_methods.rst @@ -16,7 +16,8 @@ Please keep in mind the following remarks: * All reaction methods uses Monte Carlo moves which require potential energies. Therefore reaction methods require support for energy calculations for all active interactions in the simulation. Some algorithms do not support energy - calculation, e.g. :ref:`Coulomb P3M on GPU`. + calculation, e.g. :ref:`OIF ` and + :ref:`IBM `. * When modeling reactions that do not conserve the number of particles, the method has to create or delete particles from the system. This process can diff --git a/doc/tutorials/charged_system/charged_system.ipynb b/doc/tutorials/charged_system/charged_system.ipynb index 620152e07ce..b37da9572b3 100644 --- a/doc/tutorials/charged_system/charged_system.ipynb +++ b/doc/tutorials/charged_system/charged_system.ipynb @@ -39,7 +39,7 @@ "import espressomd.accumulators\n", "import espressomd.math\n", "\n", - "espressomd.assert_features(['WCA', 'ELECTROSTATICS'])\n", + "espressomd.assert_features(['ELECTROSTATICS', 'P3M', 'WCA'])\n", "\n", "import numpy as np\n", "import scipy.optimize\n", @@ -503,11 +503,15 @@ "* Write a function ``clear_system(system)`` that\n", " * turns off the thermostat\n", " * removes all particles\n", + " * removes all actors\n", " * removes all accumulators added to the auto-update-list\n", " * resets the system clock\n", "\n", "**Hints:**\n", - "* The relevant parts of the documentation can be found here: [Thermostats](https://espressomd.github.io/doc/integration.html#thermostats), [ParticleList](https://espressomd.github.io/doc/espressomd.html#espressomd.particle_data.ParticleList),\n", + "* The relevant parts of the documentation can be found here:\n", + "[Thermostats](https://espressomd.github.io/doc/integration.html#thermostats),\n", + "[ParticleList](https://espressomd.github.io/doc/espressomd.html#espressomd.particle_data.ParticleList),\n", + "[Electrostatics](https://espressomd.github.io/doc/electrostatics.html),\n", "[AutoUpdateAccumulators](https://espressomd.github.io/doc/espressomd.html#espressomd.accumulators.AutoUpdateAccumulators),\n", "[System properties](https://espressomd.github.io/doc/espressomd.html#module-espressomd.system)" ] @@ -522,6 +526,7 @@ "def clear_system(system):\n", " system.thermostat.turn_off()\n", " system.part.clear()\n", + " system.actors.clear()\n", " system.auto_update_accumulators.clear()\n", " system.time = 0.\n", "```" @@ -597,7 +602,7 @@ "\n", "**Hints:**\n", "* Don't forget to clear the system before setting up the system with a new set of parameters\n", - "* Don't forget to ``tune()`` the ``p3m`` instance after each change of parameters. If we reuse the p3m that was tuned before, likely the desired accuracy will not be achieved. \n", + "* Don't forget to add a new ``p3m`` instance after each change of parameters. If we reuse the p3m that was tuned before, likely the desired accuracy will not be achieved. \n", "* Extract the radial density profile from the accumulator via ``.mean()``" ] }, @@ -613,7 +618,8 @@ " setup_rod_and_counterions(\n", " system, run['params']['counterion_valency'], COUNTERION_TYPE,\n", " run['params']['rod_charge_dens'], N_rod_beads, ROD_TYPE)\n", - " p3m.tune()\n", + " p3m = espressomd.electrostatics.P3M(**p3m_params)\n", + " system.actors.add(p3m)\n", " remove_overlap(system, STEEPEST_DESCENT_PARAMS)\n", " system.thermostat.set_langevin(**LANGEVIN_PARAMS)\n", " print('starting warmup')\n", @@ -840,7 +846,8 @@ "anions, cations = add_salt(system, ANION_PARAMS, CATION_PARAMS)\n", "assert abs(sum(anions.q) + sum(cations.q)) < 1e-10\n", "\n", - "p3m.tune()\n", + "p3m = espressomd.electrostatics.P3M(**p3m_params)\n", + "system.actors.add(p3m)\n", "remove_overlap(system, STEEPEST_DESCENT_PARAMS)\n", "system.thermostat.set_langevin(**LANGEVIN_PARAMS)\n", "print('starting warmup')\n", diff --git a/doc/tutorials/ferrofluid/ferrofluid_part1.ipynb b/doc/tutorials/ferrofluid/ferrofluid_part1.ipynb index 6a2f2370cd4..8f5e6e17548 100644 --- a/doc/tutorials/ferrofluid/ferrofluid_part1.ipynb +++ b/doc/tutorials/ferrofluid/ferrofluid_part1.ipynb @@ -235,11 +235,10 @@ "source": [ "import espressomd\n", "import espressomd.magnetostatics\n", - "import espressomd.magnetostatic_extensions\n", "import espressomd.cluster_analysis\n", "import espressomd.pair_criteria\n", "\n", - "espressomd.assert_features('DIPOLES', 'LENNARD_JONES')\n", + "espressomd.assert_features('DIPOLES', 'DP3M', 'LENNARD_JONES')\n", "\n", "import numpy as np" ] @@ -553,9 +552,8 @@ "CI_DP3M_PARAMS = {} # debug variable for continuous integration, can be left empty\n", "# Setup dipolar P3M and dipolar layer correction\n", "dp3m = espressomd.magnetostatics.DipolarP3M(accuracy=5E-4, prefactor=DIP_LAMBDA * LJ_SIGMA**3 * KT, **CI_DP3M_PARAMS)\n", - "dlc = espressomd.magnetostatic_extensions.DLC(maxPWerror=1E-4, gap_size=BOX_SIZE - LJ_SIGMA)\n", - "system.actors.add(dp3m)\n", - "system.actors.add(dlc)\n", + "mdlc = espressomd.magnetostatics.DLC(actor=dp3m, maxPWerror=1E-4, gap_size=BOX_SIZE - LJ_SIGMA)\n", + "system.actors.add(mdlc)\n", "\n", "# tune verlet list skin\n", "system.cell_system.tune_skin(min_skin=0.4, max_skin=2., tol=0.2, int_steps=100)\n", diff --git a/doc/tutorials/ferrofluid/ferrofluid_part2.ipynb b/doc/tutorials/ferrofluid/ferrofluid_part2.ipynb index ce3266f047f..2e62dca75c2 100644 --- a/doc/tutorials/ferrofluid/ferrofluid_part2.ipynb +++ b/doc/tutorials/ferrofluid/ferrofluid_part2.ipynb @@ -52,9 +52,8 @@ "source": [ "import espressomd\n", "import espressomd.magnetostatics\n", - "import espressomd.magnetostatic_extensions\n", "\n", - "espressomd.assert_features('DIPOLES', 'LENNARD_JONES')\n", + "espressomd.assert_features('DIPOLES', 'DP3M', 'LENNARD_JONES')\n", "\n", "import numpy as np" ] @@ -170,9 +169,8 @@ "\n", "# Setup dipolar P3M and dipolar layer correction (DLC)\n", "dp3m = espressomd.magnetostatics.DipolarP3M(accuracy=5E-4, prefactor=DIP_LAMBDA * LJ_SIGMA**3 * KT)\n", - "dlc = espressomd.magnetostatic_extensions.DLC(maxPWerror=1E-4, gap_size=box_size - LJ_SIGMA)\n", - "system.actors.add(dp3m)\n", - "system.actors.add(dlc)\n", + "mdlc = espressomd.magnetostatics.DLC(actor=dp3m, maxPWerror=1E-4, gap_size=box_size - LJ_SIGMA)\n", + "system.actors.add(mdlc)\n", "\n", "# tune verlet list skin again\n", "system.cell_system.tune_skin(min_skin=0.4, max_skin=2., tol=0.2, int_steps=100)\n", @@ -499,6 +497,7 @@ "source": [ "# remove all particles\n", "system.part.clear()\n", + "system.actors.clear()\n", "system.thermostat.turn_off()\n", "\n", "# Random dipole moments\n", @@ -530,14 +529,9 @@ "system.cell_system.tune_skin(min_skin=0.4, max_skin=2., tol=0.2, int_steps=100)\n", "\n", "# Setup dipolar P3M and dipolar layer correction\n", - "system.actors.remove(dp3m)\n", - "system.actors.remove(dlc)\n", - "\n", "dp3m = espressomd.magnetostatics.DipolarP3M(accuracy=5E-4, prefactor=DIP_LAMBDA * LJ_SIGMA**3 * KT)\n", - "dlc = espressomd.magnetostatic_extensions.DLC(maxPWerror=1E-4, gap_size=box_size - LJ_SIGMA)\n", - "\n", - "system.actors.add(dp3m)\n", - "system.actors.add(dlc)\n", + "mdlc = espressomd.magnetostatics.DLC(actor=dp3m, maxPWerror=1E-4, gap_size=box_size - LJ_SIGMA)\n", + "system.actors.add(mdlc)\n", "\n", "# tune verlet list skin again\n", "system.cell_system.tune_skin(min_skin=0.4, max_skin=2., tol=0.2, int_steps=100)" diff --git a/doc/tutorials/ferrofluid/ferrofluid_part3.ipynb b/doc/tutorials/ferrofluid/ferrofluid_part3.ipynb index 37c822ec07b..acb1c21fd78 100644 --- a/doc/tutorials/ferrofluid/ferrofluid_part3.ipynb +++ b/doc/tutorials/ferrofluid/ferrofluid_part3.ipynb @@ -124,7 +124,7 @@ "import espressomd\n", "import espressomd.magnetostatics\n", "\n", - "espressomd.assert_features('DIPOLES', 'LENNARD_JONES')\n", + "espressomd.assert_features('DIPOLES', 'DP3M', 'LENNARD_JONES')\n", "\n", "import numpy as np" ] diff --git a/samples/load_checkpoint.py b/samples/load_checkpoint.py index 480aae15d3c..658a878308d 100644 --- a/samples/load_checkpoint.py +++ b/samples/load_checkpoint.py @@ -25,6 +25,7 @@ """ # pylint: disable=undefined-variable import espressomd +import espressomd.electrostatics import espressomd.checkpointing required_features = ["P3M", "WCA"] diff --git a/samples/p3m.py b/samples/p3m.py index 3c0496aa793..f1252b00ccd 100644 --- a/samples/p3m.py +++ b/samples/p3m.py @@ -111,21 +111,13 @@ print("\nSCRIPT--->Create p3m\n") if args.mode == "gpu": - p3m = espressomd.electrostatics.P3MGPU(prefactor=2.0, accuracy=1e-2) + p3m = espressomd.electrostatics.P3MGPU(prefactor=2.0, accuracy=1e-3) else: - p3m = espressomd.electrostatics.P3M(prefactor=1.0, accuracy=1e-2) + p3m = espressomd.electrostatics.P3M(prefactor=1.0, accuracy=1e-3) -print("\nSCRIPT--->Add actor\n") +print("\nSCRIPT--->Add actor (automatic tuning)\n") system.actors.add(p3m) -print("\nSCRIPT--->P3M parameter:\n") -p3m_params = p3m.get_params() -for key, val in p3m_params.items(): - print(f"{key} = {val}") - -print("\nSCRIPT--->Explicit tune call\n") -p3m.tune(accuracy=1e3) - print("\nSCRIPT--->P3M parameter:\n") p3m_params = p3m.get_params() for key, val in p3m_params.items(): @@ -161,8 +153,6 @@ # Just to see what else we may get from the C++ core import pprint pprint.pprint(system.cell_system.get_state(), width=1) -# pprint.pprint(system.part.__getstate__(), width=1) -pprint.pprint(system.__getstate__()) ############################################################# diff --git a/samples/visualization_elc.py b/samples/visualization_elc.py index 3038448c723..ea2445d88f7 100644 --- a/samples/visualization_elc.py +++ b/samples/visualization_elc.py @@ -74,8 +74,8 @@ p3m = espressomd.electrostatics.P3M(prefactor=1.0, accuracy=1e-2) elc = espressomd.electrostatics.ELC( - p3m_actor=p3m, maxPWerror=1.0, gap_size=elc_gap, const_pot=True, - pot_diff=potential_diff) + actor=p3m, maxPWerror=1.0, gap_size=elc_gap, const_pot=True, + pot_diff=potential_diff, delta_mid_top=-1., delta_mid_bot=-1.) system.actors.add(elc) visualizer.run(1) diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index a98f00de09d..634ffbd362f 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -42,7 +42,8 @@ add_custom_target( version COMMAND ${CMAKE_COMMAND} -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DPROJECT_VERSION=${PROJECT_VERSION} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -P + -DPROJECT_VERSION=${PROJECT_VERSION} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -P ${PROJECT_SOURCE_DIR}/cmake/version.cmake) set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES version.hpp version.hpp.tmp) diff --git a/src/config/config.hpp b/src/config/config.hpp index 65ba5cae25a..bfdba2f9cd9 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -52,13 +52,6 @@ #ifndef P3M_BRILLOUIN #define P3M_BRILLOUIN 0 #endif -/** P3M: Maximal mesh size that will be checked. The current setting - * limits the memory consumption to below 1GB, which is probably - * reasonable for a while. - */ -#ifndef P3M_MAX_MESH -#define P3M_MAX_MESH 128 -#endif /** Whether to use the approximation of Abramowitz/Stegun @cite abramowitz65a * @ref AS_erfc_part() for \f$\exp(d^2) \mathrm{erfc}(d)\f$, diff --git a/src/config/version.hpp.in b/src/config/version.hpp.in index 22923ab7f08..0609f0d73a8 100644 --- a/src/config/version.hpp.in +++ b/src/config/version.hpp.in @@ -1,9 +1,10 @@ -#ifndef VERSION_HPP -#define VERSION_HPP +#ifndef ESPRESSO_SRC_CONFIG_VERSION_HPP +#define ESPRESSO_SRC_CONFIG_VERSION_HPP #define ESPRESSO_VERSION_MAJOR 4 #define ESPRESSO_VERSION_MINOR 2 #define ESPRESSO_VERSION "@PROJECT_VERSION@" +#define ESPRESSO_BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define GIT_BRANCH "@GIT_BRANCH@" #define GIT_COMMIT_HASH "@GIT_COMMIT_HASH@" #define GIT_STATE "@GIT_STATE@" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b9f036bedad..e54a276fc56 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -44,16 +44,16 @@ set(EspressoCore_SRC if(CUDA) set(EspressoCuda_SRC - actor/DipolarBarnesHut_cuda.cu - actor/DipolarDirectSum_cuda.cu - actor/Mmm1dgpuForce_cuda.cu cuda_common_cuda.cu cuda_init.cpp cuda_init_cuda.cu cuda_interface.cpp CudaHostAllocator.cu - electrostatics_magnetostatics/p3m_gpu_cuda.cu - electrostatics_magnetostatics/p3m_gpu_error_cuda.cu + magnetostatics/barnes_hut_gpu_cuda.cu + magnetostatics/dds_gpu_cuda.cu + electrostatics/mmm1d_gpu_cuda.cu + electrostatics/p3m_gpu_cuda.cu + electrostatics/p3m_gpu_error_cuda.cu EspressoSystemInterface_cuda.cu grid_based_algorithms/electrokinetics_cuda.cu grid_based_algorithms/lbgpu_cuda.cu @@ -71,7 +71,7 @@ install(TARGETS EspressoCore LIBRARY DESTINATION ${PYTHON_INSTDIR}/espressomd) target_link_libraries( EspressoCore PRIVATE EspressoConfig EspressoShapes Profiler - $<$:Scafacos> cxx_interface + $<$:EspressoScafacos> cxx_interface PUBLIC EspressoUtils MPI::MPI_CXX Random123 EspressoParticleObservables Boost::serialization Boost::mpi "$<$:${HDF5_LIBRARIES}>" $<$:Boost::filesystem> $<$:h5xx> @@ -86,21 +86,23 @@ target_include_directories( target_compile_definitions(EspressoCore PUBLIC $<$:H5XX_USE_MPI>) add_subdirectory(accumulators) -add_subdirectory(actor) add_subdirectory(bond_breakage) add_subdirectory(bonded_interactions) add_subdirectory(cell_system) add_subdirectory(cluster_analysis) add_subdirectory(constraints) -add_subdirectory(electrostatics_magnetostatics) +add_subdirectory(electrostatics) add_subdirectory(grid_based_algorithms) add_subdirectory(immersed_boundary) add_subdirectory(integrators) add_subdirectory(io) +add_subdirectory(magnetostatics) add_subdirectory(nonbonded_interactions) add_subdirectory(object-in-fluid) add_subdirectory(observables) +add_subdirectory(p3m) add_subdirectory(reaction_methods) +add_subdirectory(scafacos) add_subdirectory(virtual_sites) if(WITH_TESTS) diff --git a/src/core/EspressoSystemInterface.cpp b/src/core/EspressoSystemInterface.cpp index 17c7015b2ad..cbe85e7610a 100644 --- a/src/core/EspressoSystemInterface.cpp +++ b/src/core/EspressoSystemInterface.cpp @@ -50,6 +50,16 @@ void EspressoSystemInterface::enableParticleCommunication() { } } } + +void EspressoSystemInterface::enableParticleCommunicationParallel() { + if (m_gpu) { + if (!gpu_get_global_particle_vars_pointer_host()->communication_enabled) { + gpu_init_particle_comm(this_node); + cuda_bcast_global_part_params_parallel(); + reallocDeviceMemory(gpu_get_particle_pointer().size()); + } + } +} #endif void EspressoSystemInterface::init() { gatherParticles(); } diff --git a/src/core/EspressoSystemInterface.hpp b/src/core/EspressoSystemInterface.hpp index 9a146495a66..a3332b5d249 100644 --- a/src/core/EspressoSystemInterface.hpp +++ b/src/core/EspressoSystemInterface.hpp @@ -90,6 +90,12 @@ class EspressoSystemInterface : public SystemInterface { enableParticleCommunication(); } + void requestParticleStructGpuParallel() { + m_needsParticleStructGpu = true; + m_gpu |= m_needsParticleStructGpu; + enableParticleCommunicationParallel(); + } + float *fGpuBegin() override { return gpu_get_particle_force_pointer(); } bool hasFGpu() override { return true; } void requestFGpu() override { @@ -133,6 +139,7 @@ class EspressoSystemInterface : public SystemInterface { void split_particle_struct(); #ifdef CUDA void enableParticleCommunication(); + void enableParticleCommunicationParallel(); void reallocDeviceMemory(std::size_t n); private: diff --git a/src/core/actor/CMakeLists.txt b/src/core/actor/CMakeLists.txt deleted file mode 100644 index 30c0ba2bc7b..00000000000 --- a/src/core/actor/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -target_sources( - EspressoCore - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ActorList.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/DipolarBarnesHut.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/DipolarDirectSum.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/Mmm1dgpuForce.cpp) diff --git a/src/core/actor/DipolarBarnesHut.cpp b/src/core/actor/DipolarBarnesHut.cpp deleted file mode 100644 index 67d82ad2960..00000000000 --- a/src/core/actor/DipolarBarnesHut.cpp +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2016-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "config.hpp" - -#ifdef DIPOLAR_BARNES_HUT - -#include "DipolarBarnesHut.hpp" -#include "DipolarBarnesHut_cuda.cuh" - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" - -#include "SystemInterface.hpp" -#include "cuda_interface.hpp" -#include "cuda_utils.hpp" -#include "energy.hpp" -#include "errorhandling.hpp" -#include "forces.hpp" - -DipolarBarnesHut::DipolarBarnesHut(SystemInterface &s) { - s.requestFGpu(); - s.requestTorqueGpu(); - s.requestRGpu(); - s.requestDipGpu(); -} - -void DipolarBarnesHut::activate() { - dipole.method = DIPOLAR_BH_GPU; - mpi_bcast_coulomb_params(); - - forceActors.push_back(this); - energyActors.push_back(this); -} - -void DipolarBarnesHut::deactivate() { - dipole.method = DIPOLAR_NONE; - mpi_bcast_coulomb_params(); - - forceActors.remove(this); - energyActors.remove(this); -} - -void DipolarBarnesHut::set_params(float epssq, float itolsq) { - m_prefactor = static_cast(dipole.prefactor); - m_epssq = epssq; - m_itolsq = itolsq; - setBHPrecision(m_epssq, m_itolsq); -} - -int DipolarBarnesHut::initialize_data_structure(SystemInterface &s) { - try { - allocBHmemCopy(static_cast(s.npart_gpu()), &m_bh_data); - } catch (cuda_runtime_error const &err) { - runtimeErrorMsg() << "DipolarBarnesHut: " << err.what(); - return ES_ERROR; - } - - fill_bh_data(s.rGpuBegin(), s.dipGpuBegin(), &m_bh_data); - initBHgpu(m_bh_data.blocks); - buildBoxBH(m_bh_data.blocks); - buildTreeBH(m_bh_data.blocks); - summarizeBH(m_bh_data.blocks); - sortBH(m_bh_data.blocks); - - return ES_OK; -} - -void DipolarBarnesHut::computeForces(SystemInterface &s) { - if (initialize_data_structure(s) == ES_OK) { - if (forceBH(&m_bh_data, m_prefactor, s.fGpuBegin(), s.torqueGpuBegin())) { - runtimeErrorMsg() << "kernels encountered a functional error"; - } - } -} - -void DipolarBarnesHut::computeEnergy(SystemInterface &s) { - if (initialize_data_structure(s) == ES_OK) { - auto energy = reinterpret_cast(s.eGpu()); - if (energyBH(&m_bh_data, m_prefactor, &(energy->dipolar))) { - runtimeErrorMsg() << "kernels encountered a functional error"; - } - } -} - -#endif diff --git a/src/core/actor/DipolarDirectSum.cpp b/src/core/actor/DipolarDirectSum.cpp deleted file mode 100644 index 95f4cd4dc6b..00000000000 --- a/src/core/actor/DipolarDirectSum.cpp +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "config.hpp" - -#ifdef DIPOLAR_DIRECT_SUM - -#include "DipolarDirectSum.hpp" -#include "DipolarDirectSum_cuda.cuh" - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" - -#include "SystemInterface.hpp" -#include "cuda_interface.hpp" -#include "energy.hpp" -#include "forces.hpp" -#include "grid.hpp" - -DipolarDirectSum::DipolarDirectSum(SystemInterface &s) { - s.requestFGpu(); - s.requestTorqueGpu(); - s.requestRGpu(); - s.requestDipGpu(); -} - -void DipolarDirectSum::activate() { - // also necessary on 1 CPU or GPU, does more than just broadcasting - dipole.method = DIPOLAR_DS_GPU; - mpi_bcast_coulomb_params(); - - forceActors.push_back(this); - energyActors.push_back(this); -} - -void DipolarDirectSum::deactivate() { - dipole.method = DIPOLAR_NONE; - mpi_bcast_coulomb_params(); - - forceActors.remove(this); - energyActors.remove(this); -} - -void DipolarDirectSum::set_params() { - m_prefactor = static_cast(dipole.prefactor); -} - -void DipolarDirectSum::computeForces(SystemInterface &s) { - float box[3]; - int per[3]; - for (int i = 0; i < 3; i++) { - box[i] = static_cast(s.box()[i]); - per[i] = box_geo.periodic(i); - } - DipolarDirectSum_kernel_wrapper_force( - m_prefactor, static_cast(s.npart_gpu()), s.rGpuBegin(), - s.dipGpuBegin(), s.fGpuBegin(), s.torqueGpuBegin(), box, per); -} - -void DipolarDirectSum::computeEnergy(SystemInterface &s) { - float box[3]; - int per[3]; - for (int i = 0; i < 3; i++) { - box[i] = static_cast(s.box()[i]); - per[i] = box_geo.periodic(i); - } - auto energy = reinterpret_cast(s.eGpu()); - DipolarDirectSum_kernel_wrapper_energy( - m_prefactor, static_cast(s.npart_gpu()), s.rGpuBegin(), - s.dipGpuBegin(), box, per, &(energy->dipolar)); -} - -#endif diff --git a/src/core/actor/Mmm1dgpuForce.hpp b/src/core/actor/Mmm1dgpuForce.hpp deleted file mode 100644 index ea16efb3d31..00000000000 --- a/src/core/actor/Mmm1dgpuForce.hpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2014-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef ESPRESSO_CORE_ACTOR_MMM1DGPUFORCE_HPP -#define ESPRESSO_CORE_ACTOR_MMM1DGPUFORCE_HPP - -#include "config.hpp" - -#ifdef MMM1D_GPU - -#include "Actor.hpp" -#include "SystemInterface.hpp" - -class Mmm1dgpuForce : public Actor { -public: - Mmm1dgpuForce(SystemInterface &s); - ~Mmm1dgpuForce() override; - - // interface methods - void computeForces(SystemInterface &s) override; - void computeEnergy(SystemInterface &s) override; - // configuration methods - void setup(SystemInterface &s); - void tune(SystemInterface &s, float _maxPWerror, float _far_switch_radius, - int _bessel_cutoff); - void set_params(float _boxz, float _coulomb_prefactor, float _maxPWerror, - float _far_switch_radius, int _bessel_cutoff); - void activate(); - void deactivate(); - -private: - // CUDA parameters - unsigned int numThreads = 64; - unsigned int numBlocks(SystemInterface const &s) const; - - // the box length currently set on the GPU - // Needed to make sure it hasn't been modified after inter coulomb was used. - float host_boxz = 0; - // the number of particles we had during the last run. Needed to check if we - // have to realloc dev_forcePairs - unsigned int host_npart = 0; - bool need_tune = true; - - // pairs==0: return forces using atomicAdd - // pairs==1: return force pairs - // pairs==2: return forces using a global memory reduction - int pairs = -1; - // variables for forces and energies calculated pre-reduction - float *dev_forcePairs = nullptr; - float *dev_energyBlocks = nullptr; - - // MMM1D parameters - float coulomb_prefactor = 0; - float maxPWerror = -1; - float far_switch_radius = -1; - int bessel_cutoff = -1; - - // run a single force calculation and return the time it takes using - // high-precision CUDA timers - float force_benchmark(SystemInterface &s); - - void modpsi_init(); - - // some functions to move MPI dependencies out of the .cu file - void sanity_checks(); -}; - -#endif -#endif diff --git a/src/core/actor/registration.hpp b/src/core/actor/registration.hpp new file mode 100644 index 00000000000..bb359ab773f --- /dev/null +++ b/src/core/actor/registration.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_ACTOR_REGISTRATION_HPP +#define ESPRESSO_SRC_CORE_ACTOR_REGISTRATION_HPP + +#include "actor/visitors.hpp" + +#include +#include + +#include + +/** @brief Register an actor in a thread-safe manner. */ +template +void add_actor(boost::optional &active_actor, + std::shared_ptr const &actor, void (&on_actor_change)(), + bool (&flag_all_reduce)(bool)) { + auto const cleanup_if_any_rank_failed = [&](bool this_failed) { + auto const any_failed = flag_all_reduce(this_failed); + if (any_failed) { + active_actor = boost::none; + on_actor_change(); + } + }; + try { + active_actor = actor; + actor->on_activation(); + on_actor_change(); + cleanup_if_any_rank_failed(false); + } catch (...) { + cleanup_if_any_rank_failed(true); + throw; + } +} + +template +void remove_actor(boost::optional &active_actor, + std::shared_ptr const &actor, void (&on_actor_change)()) { + active_actor = boost::none; + on_actor_change(); +} + +#endif diff --git a/src/core/electrostatics_magnetostatics/common.hpp b/src/core/actor/traits.hpp similarity index 68% rename from src/core/electrostatics_magnetostatics/common.hpp rename to src/core/actor/traits.hpp index 58609d58755..7afaca7443f 100644 --- a/src/core/electrostatics_magnetostatics/common.hpp +++ b/src/core/actor/traits.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The ESPResSo project + * Copyright (C) 2022 The ESPResSo project * * This file is part of ESPResSo. * @@ -16,10 +16,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef ELECTROSTATICS_MAGNETOSTATICS_COMMON_HPP -#define ELECTROSTATICS_MAGNETOSTATICS_COMMON_HPP +#ifndef ESPRESSO_SRC_CORE_ACTOR_TRAITS_HPP +#define ESPRESSO_SRC_CORE_ACTOR_TRAITS_HPP -/** Send new Coulomb/Dipole parameters. */ -void mpi_bcast_coulomb_params(); +#include + +namespace traits { +/** @brief Whether an actor is a layer correction method. */ +template struct is_layer_correction : std::false_type {}; +} // namespace traits #endif diff --git a/src/core/actor/visit_try_catch.hpp b/src/core/actor/visit_try_catch.hpp new file mode 100644 index 00000000000..cc4feccf6ff --- /dev/null +++ b/src/core/actor/visit_try_catch.hpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_CORE_ACTOR_VISIT_TRY_CATCH_HPP +#define ESPRESSO_SRC_CORE_ACTOR_VISIT_TRY_CATCH_HPP + +#include "errorhandling.hpp" + +#include +#include + +#include +#include + +/** @brief Run a kernel on a variant and queue errors. */ +template +void visit_active_actor_try_catch(Visitor &&visitor, Variant &actor) { + try { + boost::apply_visitor(visitor, actor); + } catch (std::runtime_error const &err) { + runtimeErrorMsg() << err.what(); + } +} + +/** @brief Run a kernel on a variant and queue errors. */ +template +void visit_active_actor_try_catch(Visitor &&visitor, + boost::optional &actor) { + if (actor) { + visit_active_actor_try_catch(std::forward(visitor), *actor); + } +} + +#endif diff --git a/src/core/actor/visitors.hpp b/src/core/actor/visitors.hpp new file mode 100644 index 00000000000..4aede20fe77 --- /dev/null +++ b/src/core/actor/visitors.hpp @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_ACTOR_VISITORS_HPP +#define ESPRESSO_SRC_CORE_ACTOR_VISITORS_HPP + +#include "actor/traits.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + +struct GetActorName : public boost::static_visitor { + template auto operator()(std::shared_ptr const &) const { + return Utils::demangle(); + } +}; + +/** @brief Get the symbol name of an actor. */ +template auto get_actor_name(Variant const &variant) { + return boost::apply_visitor(GetActorName{}, variant); +} + +/** @brief Get an actor of a specific type, recursively. */ +template +struct GetActorByType : public boost::static_visitor> { +private: + template + static constexpr bool is_exact_match_v = std::is_same::value; + template + static constexpr bool is_layer_correction_v = + traits::is_layer_correction::value; + +public: + template > * = nullptr> + auto operator()(std::shared_ptr const &obj) const { + return obj; + } + + template and + is_layer_correction_v> * = nullptr> + auto operator()(std::shared_ptr const &obj) const { + return boost::apply_visitor(*this, obj->base_solver); + } + + template and + not is_layer_correction_v> * = nullptr> + auto operator()(std::shared_ptr const &) const { + return std::shared_ptr{nullptr}; + } +}; + +/** @brief Get an active actor of a specific type, recursively. */ +template +std::shared_ptr get_actor_by_type(Variant const &variant) { + return boost::apply_visitor(GetActorByType{}, variant); +} + +template +std::shared_ptr +get_actor_by_type(boost::optional const &optional) { + return (optional) ? get_actor_by_type(*optional) : nullptr; +} + +/** @brief Check if an actor of a specific type is active, recursively. */ +template +struct HasActorOfType : public boost::static_visitor { +private: + template + static constexpr bool is_exact_match_v = std::is_same::value; + template + static constexpr bool is_layer_correction_v = + traits::is_layer_correction::value; + +public: + template > * = nullptr> + auto operator()(std::shared_ptr const &) const { + return true; + } + + template and + is_layer_correction_v> * = nullptr> + auto operator()(std::shared_ptr const &obj) const { + return boost::apply_visitor(*this, obj->base_solver); + } + + template and + not is_layer_correction_v> * = nullptr> + auto operator()(std::shared_ptr const &) const { + return false; + } +}; + +/** @brief Check if an actor of a specific type is active, recursively. */ +template +auto has_actor_of_type(Variant const &variant) { + return boost::apply_visitor(HasActorOfType{}, variant); +} + +template +auto has_actor_of_type(boost::optional const &optional) { + return (optional) ? has_actor_of_type(*optional) : false; +} + +/** Check whether two actors are identical by pointer. */ +struct ActorEquality : public boost::static_visitor { + template ::value, + std::nullptr_t> = nullptr> + bool operator()(std::shared_ptr const &lhs, + std::shared_ptr const &rhs) const { + return lhs == rhs; + } + + template ::value, + std::nullptr_t> = nullptr> + bool operator()(std::shared_ptr const &, + std::shared_ptr const &) const { + return false; + } +}; + +/** @brief Check if an actor is already stored in an optional. */ +template +bool is_already_stored(std::shared_ptr const &actor, + boost::optional const &active_actor) { + auto const visitor = std::bind(ActorEquality{}, actor, std::placeholders::_1); + return active_actor and boost::apply_visitor(visitor, *active_actor); +} + +#endif diff --git a/src/core/bonded_interactions/bonded_coulomb_sr.hpp b/src/core/bonded_interactions/bonded_coulomb_sr.hpp index ef891794727..051fe86ebde 100644 --- a/src/core/bonded_interactions/bonded_coulomb_sr.hpp +++ b/src/core/bonded_interactions/bonded_coulomb_sr.hpp @@ -29,13 +29,13 @@ #include "config.hpp" #include "Particle.hpp" -#include "electrostatics_magnetostatics/coulomb_inline.hpp" #include #include #include +#include /** Parameters for %Coulomb bond short-range Potential */ struct BondedCoulombSR { @@ -48,9 +48,15 @@ struct BondedCoulombSR { BondedCoulombSR(double q1q2) { this->q1q2 = q1q2; } - boost::optional force(Utils::Vector3d const &dx) const; - boost::optional energy(Particle const &p1, Particle const &p2, - Utils::Vector3d const &dx) const; + boost::optional + force(Utils::Vector3d const &dx, + std::function const &kernel) const; + boost::optional + energy(Particle const &p1, Particle const &p2, Utils::Vector3d const &dx, + std::function const &kernel) + const; private: friend boost::serialization::access; @@ -62,12 +68,14 @@ struct BondedCoulombSR { /** Compute the short-range bonded Coulomb pair force. * @param[in] dx %Distance between the particles. + * @param[in] kernel %Coulomb force kernel. */ -inline boost::optional -BondedCoulombSR::force(Utils::Vector3d const &dx) const { +inline boost::optional BondedCoulombSR::force( + Utils::Vector3d const &dx, + std::function const &kernel) const { #ifdef ELECTROSTATICS - auto const dist = dx.norm(); - return Coulomb::central_force(q1q2, dx, dist); + return kernel(q1q2, dx, dx.norm()); #else return Utils::Vector3d{}; #endif @@ -77,13 +85,15 @@ BondedCoulombSR::force(Utils::Vector3d const &dx) const { * @param[in] p1 First particle. * @param[in] p2 Second particle. * @param[in] dx %Distance between the particles. + * @param[in] kernel %Coulomb energy kernel. */ -inline boost::optional -BondedCoulombSR::energy(Particle const &p1, Particle const &p2, - Utils::Vector3d const &dx) const { +inline boost::optional BondedCoulombSR::energy( + Particle const &p1, Particle const &p2, Utils::Vector3d const &dx, + std::function const &kernel) + const { #ifdef ELECTROSTATICS - auto const dist = dx.norm(); - return Coulomb::pair_energy(p1, p2, q1q2, dx, dist); + return kernel(p1, p2, q1q2, dx, dx.norm()); #else return 0.; #endif diff --git a/src/core/cell_system/RegularDecomposition.cpp b/src/core/cell_system/RegularDecomposition.cpp index 965fbebc3f0..10feb3e3b78 100644 --- a/src/core/cell_system/RegularDecomposition.cpp +++ b/src/core/cell_system/RegularDecomposition.cpp @@ -353,10 +353,12 @@ void RegularDecomposition::create_cell_grid(double range) { /* sanity check */ if (n_local_cells < min_num_cells) { - runtimeErrorMsg() - << "number of cells " << n_local_cells << " is smaller than minimum " - << min_num_cells - << " (interaction range too large or min_num_cells too large)"; + runtimeErrorMsg() << "number of cells " << n_local_cells + << " is smaller than minimum " << min_num_cells + << ": either interaction range is too large for " + << "the current skin (range=" << range << ", " + << "half_local_box_l=[" << local_box_l / 2. << "]) " + << "or min_num_cells too large"; } } diff --git a/src/core/cluster_analysis/ClusterStructure.hpp b/src/core/cluster_analysis/ClusterStructure.hpp index 3c58c704cec..338d491ef2d 100644 --- a/src/core/cluster_analysis/ClusterStructure.hpp +++ b/src/core/cluster_analysis/ClusterStructure.hpp @@ -20,7 +20,7 @@ #ifndef CLUSTER_ANALYSIS_CLUSTER_STRUCTURE_HPP #define CLUSTER_ANALYSIS_CLUSTER_STRUCTURE_HPP -#include "pair_criteria/pair_criteria.hpp" +#include "pair_criteria/PairCriterion.hpp" #include "Cluster.hpp" #include "Particle.hpp" diff --git a/src/core/constraints/ShapeBasedConstraint.cpp b/src/core/constraints/ShapeBasedConstraint.cpp index c3e0e92c0eb..85a8550024d 100644 --- a/src/core/constraints/ShapeBasedConstraint.cpp +++ b/src/core/constraints/ShapeBasedConstraint.cpp @@ -81,6 +81,7 @@ ParticleForce ShapeBasedConstraint::force(Particle const &p, double dist = 0.; Utils::Vector3d dist_vec; m_shape->calculate_dist(folded_pos, dist, dist_vec); + auto const coulomb_kernel = Coulomb::pair_force_kernel(); #ifdef DPD Utils::Vector3d dpd_force{}; @@ -89,7 +90,8 @@ ParticleForce ShapeBasedConstraint::force(Particle const &p, if (dist > 0) { outer_normal_vec = -dist_vec / dist; - pf = calc_non_bonded_pair_force(p, part_rep, ia_params, dist_vec, dist); + pf = calc_non_bonded_pair_force(p, part_rep, ia_params, dist_vec, dist, + coulomb_kernel.get_ptr()); #ifdef DPD if (thermo_switch & THERMO_DPD) { dpd_force = @@ -100,8 +102,8 @@ ParticleForce ShapeBasedConstraint::force(Particle const &p, #endif } else if (m_penetrable && (dist <= 0)) { if ((!m_only_positive) && (dist < 0)) { - pf = - calc_non_bonded_pair_force(p, part_rep, ia_params, dist_vec, -dist); + pf = calc_non_bonded_pair_force(p, part_rep, ia_params, dist_vec, -dist, + coulomb_kernel.get_ptr()); #ifdef DPD if (thermo_switch & THERMO_DPD) { dpd_force = dpd_pair_force(p, part_rep, ia_params, dist_vec, dist, @@ -136,15 +138,17 @@ void ShapeBasedConstraint::add_energy(const Particle &p, IA_parameters const &ia_params = *get_ia_param(p.type(), part_rep.type()); if (checkIfInteraction(ia_params)) { + auto const coulomb_kernel = Coulomb::pair_energy_kernel(); double dist = 0.0; Utils::Vector3d vec; m_shape->calculate_dist(folded_pos, dist, vec); if (dist > 0) { - energy = calc_non_bonded_pair_energy(p, part_rep, ia_params, vec, dist); + energy = calc_non_bonded_pair_energy(p, part_rep, ia_params, vec, dist, + coulomb_kernel.get_ptr()); } else if ((dist <= 0) && m_penetrable) { if (!m_only_positive && (dist < 0)) { - energy = calc_non_bonded_pair_energy(p, part_rep, ia_params, vec, - -1.0 * dist); + energy = calc_non_bonded_pair_energy(p, part_rep, ia_params, vec, -dist, + coulomb_kernel.get_ptr()); } } else { runtimeErrorMsg() << "Constraint violated by particle " << p.id(); diff --git a/src/core/cuda_interface.cpp b/src/core/cuda_interface.cpp index b04782c4a6f..b2d82518474 100644 --- a/src/core/cuda_interface.cpp +++ b/src/core/cuda_interface.cpp @@ -164,4 +164,10 @@ void cuda_bcast_global_part_params() { mpi_call_all(cuda_bcast_global_part_params_local); } +void cuda_bcast_global_part_params_parallel() { + MPI_Bcast(gpu_get_global_particle_vars_pointer_host(), + sizeof(CUDA_global_part_vars), MPI_BYTE, 0, comm_cart); + EspressoSystemInterface::Instance().requestParticleStructGpuParallel(); +} + #endif /* ifdef CUDA */ diff --git a/src/core/cuda_interface.hpp b/src/core/cuda_interface.hpp index af9eda408b3..9bbfa78b669 100644 --- a/src/core/cuda_interface.hpp +++ b/src/core/cuda_interface.hpp @@ -137,6 +137,7 @@ void copy_part_data_to_gpu(ParticleRange particles, int this_node); void cuda_mpi_send_forces(const ParticleRange &particles, Utils::Span host_forces, Utils::Span host_torques); +void cuda_bcast_global_part_params_parallel(); void cuda_bcast_global_part_params(); #endif /* ifdef CUDA */ diff --git a/src/python/espressomd/magnetostatic_extensions.pxd b/src/core/electrostatics/CMakeLists.txt similarity index 53% rename from src/python/espressomd/magnetostatic_extensions.pxd rename to src/core/electrostatics/CMakeLists.txt index 26b9d841ab3..3c61890a9be 100644 --- a/src/python/espressomd/magnetostatic_extensions.pxd +++ b/src/core/electrostatics/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (C) 2013-2019 The ESPResSo project +# Copyright (C) 2018-2022 The ESPResSo project # # This file is part of ESPResSo. # @@ -16,18 +16,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # - -include "myconfig.pxi" - -IF DIPOLES: - - cdef extern from "electrostatics_magnetostatics/mdlc_correction.hpp": - ctypedef struct dlc_struct "DLC_struct": - double maxPWerror - double gap_size - double far_cut - - void mdlc_set_params(double maxPWerror, double gap_size, double far_cut) except + - - # links intern C-struct with python object - cdef extern dlc_struct dlc_params +target_sources( + EspressoCore + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/coulomb.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elc.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/icc.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mmm1d_gpu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mmm1d.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mmm-modpsi.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/p3m.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/p3m_gpu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/scafacos_impl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/specfunc.cpp) diff --git a/src/core/electrostatics/actor.hpp b/src/core/electrostatics/actor.hpp new file mode 100644 index 00000000000..138be4ed5c5 --- /dev/null +++ b/src/core/electrostatics/actor.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_ACTOR_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_ACTOR_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include + +namespace Coulomb { + +void check_charge_neutrality(double excess_ratio); + +template class Actor { +public: + static auto constexpr charge_neutrality_tolerance_default = 2e-12; + /** + * @brief Electrostatics prefactor. + */ + double prefactor = 0.; + /** + * @brief Relative tolerance for the charge excess during neutrality checks. + * To deactivate neutrality checks, set this value to -1. + */ + double charge_neutrality_tolerance = charge_neutrality_tolerance_default; + + void set_prefactor(double new_prefactor) { + if (new_prefactor <= 0.) { + throw std::domain_error("Parameter 'prefactor' must be > 0"); + } + prefactor = new_prefactor; + } + + void sanity_checks_charge_neutrality() const { + if (charge_neutrality_tolerance != -1.) { + Coulomb::check_charge_neutrality(charge_neutrality_tolerance); + } + } +}; + +} // namespace Coulomb + +#endif // ELECTROSTATICS +#endif diff --git a/src/core/electrostatics/coulomb.cpp b/src/core/electrostatics/coulomb.cpp new file mode 100644 index 00000000000..86e3e9b9eac --- /dev/null +++ b/src/core/electrostatics/coulomb.cpp @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "electrostatics/coulomb.hpp" + +#include "ParticleRange.hpp" +#include "actor/visit_try_catch.hpp" +#include "actor/visitors.hpp" +#include "cells.hpp" +#include "communication.hpp" +#include "electrostatics/icc.hpp" +#include "errorhandling.hpp" +#include "grid_based_algorithms/electrokinetics.hpp" +#include "integrate.hpp" +#include "npt.hpp" +#include "partCfg_global.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +boost::optional electrostatics_actor; +boost::optional electrostatics_extension; + +namespace Coulomb { + +void sanity_checks() { + if (electrostatics_actor) { + boost::apply_visitor([](auto &actor) { actor->sanity_checks(); }, + *electrostatics_actor); + } +} + +void on_coulomb_change() { + visit_active_actor_try_catch([](auto &actor) { actor->init(); }, + electrostatics_actor); +} + +void on_boxl_change() { + visit_active_actor_try_catch([](auto &actor) { actor->on_boxl_change(); }, + electrostatics_actor); +} + +void on_node_grid_change() { + if (electrostatics_actor) { + boost::apply_visitor([](auto &actor) { actor->on_node_grid_change(); }, + *electrostatics_actor); + } +} + +void on_periodicity_change() { + visit_active_actor_try_catch( + [](auto &actor) { actor->on_periodicity_change(); }, + electrostatics_actor); +} + +void on_cell_structure_change() { + visit_active_actor_try_catch( + [](auto &actor) { actor->on_cell_structure_change(); }, + electrostatics_actor); +} + +struct LongRangePressure : public boost::static_visitor { + explicit LongRangePressure(ParticleRange const &particles) + : m_particles{particles} {} + +#ifdef P3M + auto operator()(std::shared_ptr const &actor) const { + actor->charge_assign(m_particles); + return actor->p3m_calc_kspace_pressure_tensor(); + } +#endif // P3M + + auto operator()(std::shared_ptr const &actor) const { + return Utils::Vector9d{}; + } + + auto operator()(std::shared_ptr const &actor) const { + return Utils::Vector9d{}; + } + + template ::value> * = nullptr> + auto operator()(std::shared_ptr const &) const { + runtimeWarningMsg() << "Pressure calculation not implemented by " + << "electrostatics method " << Utils::demangle(); + return Utils::Vector9d{}; + } + +private: + ParticleRange const &m_particles; +}; + +Utils::Vector9d calc_pressure_long_range(ParticleRange const &particles) { + if (electrostatics_actor) { + return boost::apply_visitor(LongRangePressure{particles}, + *electrostatics_actor); + } + return {}; +} + +struct ShortRangeCutoff : public boost::static_visitor { + explicit ShortRangeCutoff(Utils::Vector3d const &box_l) : m_box_l{box_l} {} + +#ifdef P3M + auto operator()(std::shared_ptr const &actor) const { + return actor->p3m.params.r_cut; + } +#ifdef CUDA + auto operator()(std::shared_ptr const &actor) const { + return actor->p3m.params.r_cut; + } +#endif // CUDA + auto + operator()(std::shared_ptr const &actor) const { + return std::max(actor->elc.space_layer, + boost::apply_visitor(*this, actor->base_solver)); + } +#endif // P3M +#ifdef MMM1D_GPU + auto operator()(std::shared_ptr const &actor) const { + return std::numeric_limits::infinity(); + } +#endif // MMM1D_GPU + auto operator()(std::shared_ptr const &actor) const { + return std::numeric_limits::infinity(); + } +#ifdef SCAFACOS + auto operator()(std::shared_ptr const &actor) const { + return actor->get_r_cut(); + } +#endif // SCAFACOS + auto operator()(std::shared_ptr const &actor) const { + return actor->r_cut; + } + auto operator()(std::shared_ptr const &actor) const { + return actor->r_cut; + } + +private: + Utils::Vector3d const &m_box_l; +}; + +double cutoff(Utils::Vector3d const &box_l) { + if (electrostatics_actor) { + return boost::apply_visitor(ShortRangeCutoff{box_l}, *electrostatics_actor); + } + return -1.0; +} + +struct EventOnObservableCalc : public boost::static_visitor { + template void operator()(std::shared_ptr const &) const {} + +#ifdef P3M + void operator()(std::shared_ptr const &actor) const { + actor->count_charged_particles(); + } +#ifdef CUDA + void operator()(std::shared_ptr const &actor) const { + actor->count_charged_particles(); + } +#endif // CUDA + void + operator()(std::shared_ptr const &actor) const { + boost::apply_visitor(*this, actor->base_solver); + } +#endif // P3M +}; + +void on_observable_calc() { + if (electrostatics_actor) { + boost::apply_visitor(EventOnObservableCalc{}, *electrostatics_actor); + } +} + +struct LongRangeForce : public boost::static_visitor { + explicit LongRangeForce(ParticleRange const &particles) + : m_particles(particles) {} + +#ifdef P3M + void operator()(std::shared_ptr const &actor) const { + actor->charge_assign(m_particles); +#ifdef NPT + if (integ_switch == INTEG_METHOD_NPT_ISO) { + auto const energy = actor->long_range_kernel(true, true, m_particles); + npt_add_virial_contribution(energy); + } else +#endif // NPT + actor->add_long_range_forces(m_particles); + } +#ifdef CUDA + void operator()(std::shared_ptr const &actor) const { +#ifdef NPT + if (integ_switch == INTEG_METHOD_NPT_ISO) { + actor->charge_assign(m_particles); + auto const energy = actor->long_range_energy(m_particles); + npt_add_virial_contribution(energy); + } +#endif // NPT + actor->add_long_range_forces(m_particles); + } +#endif // CUDA + void + operator()(std::shared_ptr const &actor) const { + actor->add_long_range_forces(m_particles); + } +#endif // P3M +#ifdef MMM1D_GPU + void operator()(std::shared_ptr const &actor) const { + actor->add_long_range_forces(); + } +#endif +#ifdef SCAFACOS + void operator()(std::shared_ptr const &actor) const { + actor->add_long_range_forces(); + } +#endif + /* Several algorithms only provide near-field kernels */ + void operator()(std::shared_ptr const &) const {} + void operator()(std::shared_ptr const &) const {} + void operator()(std::shared_ptr const &) const {} + +private: + ParticleRange const &m_particles; +}; + +struct LongRangeEnergy : public boost::static_visitor { + explicit LongRangeEnergy(ParticleRange const &particles) + : m_particles(particles) {} + +#ifdef P3M + auto operator()(std::shared_ptr const &actor) const { + actor->charge_assign(m_particles); + return actor->long_range_energy(m_particles); + } +#ifdef CUDA + auto operator()(std::shared_ptr const &actor) const { + actor->charge_assign(m_particles); + return actor->long_range_energy(m_particles); + } +#endif // CUDA + auto + operator()(std::shared_ptr const &actor) const { + return actor->long_range_energy(m_particles); + } +#endif // P3M +#ifdef MMM1D_GPU + auto operator()(std::shared_ptr const &actor) const { + actor->add_long_range_energy(); + return 0.; + } +#endif // MMM1D_GPU +#ifdef SCAFACOS + auto operator()(std::shared_ptr const &actor) const { + return actor->long_range_energy(); + } +#endif + /* Several algorithms only provide near-field kernels */ + auto operator()(std::shared_ptr const &) const { return 0.; } + auto operator()(std::shared_ptr const &) const { return 0.; } + auto operator()(std::shared_ptr const &) const { return 0.; } + +private: + ParticleRange const &m_particles; +}; + +void calc_long_range_force(ParticleRange const &particles) { + if (electrostatics_actor) { + boost::apply_visitor(LongRangeForce{particles}, *electrostatics_actor); + } +#ifdef ELECTROKINETICS + /* Add fields from EK if enabled */ + if (this_node == 0) { + ek_calculate_electrostatic_coupling(); + } +#endif +} + +double calc_energy_long_range(ParticleRange const &particles) { + if (electrostatics_actor) { + return boost::apply_visitor(LongRangeEnergy{particles}, + *electrostatics_actor); + } + return 0.; +} + +/** @brief Compute the net charge rescaled by the smallest non-zero charge. */ +static auto calc_charge_excess_ratio(std::vector const &charges) { + using namespace boost::accumulators; + using KahanSum = accumulator_set>; + + KahanSum q_sum; + auto q_min = std::numeric_limits::infinity(); + + for (auto const q : charges) { + if (q != 0.) { + q_sum(q); + q_min = std::min(q_min, std::abs(q)); + } + } + + return std::abs(sum_kahan(q_sum)) / q_min; +} + +void check_charge_neutrality(double relative_tolerance) { + // collect non-zero particle charges from all nodes + auto const &local_particles = cell_structure.local_particles(); + std::vector local_charges; + for (auto const &p : local_particles) { + local_charges.push_back(p.q()); + } + std::vector> node_charges; + boost::mpi::gather(comm_cart, local_charges, node_charges, 0); + + // run Kahan sum on charges + auto excess_ratio = 0.; + if (this_node == 0) { + auto charges = std::move(local_charges); + for (auto it = ++node_charges.begin(); it != node_charges.end(); ++it) { + charges.insert(charges.end(), it->begin(), it->end()); + } + excess_ratio = calc_charge_excess_ratio(charges); + } + boost::mpi::broadcast(comm_cart, excess_ratio, 0); + + if (excess_ratio >= relative_tolerance) { + std::ostringstream serializer; + serializer << std::scientific << std::setprecision(4); + serializer << excess_ratio; + throw std::runtime_error( + "The system is not charge neutral. Please neutralize the system " + "before adding a new actor by adding the corresponding counterions " + "to the system. Alternatively you can turn off the electroneutrality " + "check by supplying check_neutrality=False when creating the actor. " + "In this case you may be simulating a non-neutral system which will " + "affect physical observables like e.g. the pressure, the chemical " + "potentials of charged species or potential energies of the system. " + "Since simulations of non charge neutral systems are special please " + "make sure you know what you are doing. The relative charge excess " + "was " + + serializer.str()); + } +} + +namespace detail { +bool flag_all_reduce(bool flag) { + return boost::mpi::all_reduce(comm_cart, flag, std::logical_or<>()); +} +} // namespace detail + +} // namespace Coulomb + +#endif // ELECTROSTATICS diff --git a/src/core/electrostatics/coulomb.hpp b/src/core/electrostatics/coulomb.hpp new file mode 100644 index 00000000000..93e32e95527 --- /dev/null +++ b/src/core/electrostatics/coulomb.hpp @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_COULOMB_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_COULOMB_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "actor/traits.hpp" + +#include "electrostatics/debye_hueckel.hpp" +#include "electrostatics/elc.hpp" +#include "electrostatics/icc.hpp" +#include "electrostatics/mmm1d.hpp" +#include "electrostatics/mmm1d_gpu.hpp" +#include "electrostatics/p3m.hpp" +#include "electrostatics/p3m_gpu.hpp" +#include "electrostatics/reaction_field.hpp" +#include "electrostatics/scafacos.hpp" + +#include "ParticleRange.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + +using ElectrostaticsActor = + boost::variant, +#ifdef P3M + std::shared_ptr, +#ifdef CUDA + std::shared_ptr, +#endif // CUDA + std::shared_ptr, +#endif // P3M + std::shared_ptr, +#ifdef MMM1D_GPU + std::shared_ptr, +#endif // MMM1D_GPU +#ifdef SCAFACOS + std::shared_ptr, +#endif // SCAFACOS + std::shared_ptr>; + +using ElectrostaticsExtension = boost::variant>; + +extern boost::optional electrostatics_actor; +extern boost::optional electrostatics_extension; + +/** Get the electrostatics prefactor. */ +struct GetCoulombPrefactor : public boost::static_visitor { + template + double operator()(std::shared_ptr const &actor) const { + return actor->prefactor; + } +}; + +namespace Coulomb { + +namespace traits { + +#ifdef P3M +/** @brief Whether an actor can be adapted by ELC. */ +template +using elc_adaptable = + std::is_convertible, + ElectrostaticLayerCorrection::BaseSolver>; +#endif // P3M + +/** @brief Whether an actor is a solver. */ +template +using is_solver = std::is_convertible, ElectrostaticsActor>; +/** @brief Whether an actor is an extension. */ +template +using is_extension = + std::is_convertible, ElectrostaticsExtension>; + +/** @brief The electrostatic method supports pressure calculation. */ +template struct has_pressure : std::true_type {}; +#ifdef P3M +template <> +struct has_pressure : std::false_type {}; +#endif // P3M +#ifdef MMM1D_GPU +template <> struct has_pressure : std::false_type {}; +#endif // MMM1D_GPU +#ifdef SCAFACOS +template <> struct has_pressure : std::false_type {}; +#endif // SCAFACOS +template <> struct has_pressure : std::false_type {}; + +} // namespace traits + +/** @brief Check if the system is charge-neutral. */ +void check_charge_neutrality(double relative_tolerance); + +Utils::Vector9d calc_pressure_long_range(ParticleRange const &particles); + +void sanity_checks(); +double cutoff(Utils::Vector3d const &box_l); + +void on_observable_calc(); +void on_coulomb_change(); +void on_boxl_change(); +void on_node_grid_change(); +void on_periodicity_change(); +void on_cell_structure_change(); + +void calc_long_range_force(ParticleRange const &particles); +double calc_energy_long_range(ParticleRange const &particles); + +namespace detail { +bool flag_all_reduce(bool flag); +} // namespace detail + +} // namespace Coulomb +#else // ELECTROSTATICS +#include +namespace Coulomb { +constexpr std::size_t pressure_n() { return 0; } +constexpr std::size_t energy_n() { return 0; } +} // namespace Coulomb +#endif // ELECTROSTATICS +#endif diff --git a/src/core/electrostatics/coulomb_inline.hpp b/src/core/electrostatics/coulomb_inline.hpp new file mode 100644 index 00000000000..7ba558dd334 --- /dev/null +++ b/src/core/electrostatics/coulomb_inline.hpp @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_COULOMB_INLINE_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_COULOMB_INLINE_HPP + +#include "config.hpp" + +#include "electrostatics/coulomb.hpp" + +#include "Particle.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace Coulomb { + +struct ShortRangeForceKernel + : public boost::static_visitor>> { + + using kernel_type = result_type::value_type; + +#ifdef ELECTROSTATICS + template + result_type operator()(std::shared_ptr const &ptr) const { + auto const &actor = *ptr; + return kernel_type{ + [&actor](double q1q2, Utils::Vector3d const &d, double dist) { + return actor.pair_force(q1q2, d, dist); + }}; + } + +#ifdef P3M + auto + operator()(std::shared_ptr const &ptr) const { + return boost::apply_visitor(*this, ptr->base_solver); + } +#endif // P3M + +#ifdef MMM1D_GPU + result_type operator()(std::shared_ptr const &) const { + return {}; + } +#endif // MMM1D_GPU +#endif // ELECTROSTATICS +}; + +struct ShortRangeForceCorrectionsKernel + : public boost::static_visitor>> { + + using kernel_type = result_type::value_type; + + template + result_type operator()(std::shared_ptr const &) const { + return {}; + } + +#ifdef P3M + result_type + operator()(std::shared_ptr const &ptr) const { + auto const &actor = *ptr; + return kernel_type{[&actor](Particle &p1, Particle &p2, double q1q2) { + actor.add_pair_force_corrections(p1, p2, q1q2); + }}; + } +#endif // P3M +}; + +inline ShortRangeForceKernel::result_type pair_force_kernel() { +#ifdef ELECTROSTATICS + if (electrostatics_actor) { + auto const visitor = ShortRangeForceKernel{}; + return boost::apply_visitor(visitor, *electrostatics_actor); + } +#endif // ELECTROSTATICS + return {}; +} + +struct ShortRangePressureKernel + : public boost::static_visitor(double, Utils::Vector3d const &, double)>>> { + + using kernel_type = result_type::value_type; + +#ifdef ELECTROSTATICS + template ::value> * = nullptr> + result_type operator()(std::shared_ptr const &ptr) const { + result_type pressure_kernel = {}; + if (auto const force_kernel_opt = pair_force_kernel()) { + pressure_kernel = + kernel_type{[force_kernel = *force_kernel_opt]( + double q1q2, Utils::Vector3d const &d, double dist) { + auto const force = force_kernel(q1q2, d, dist); + return Utils::tensor_product(force, d); + }}; + } + return pressure_kernel; + } + + template ::value> * = nullptr> + result_type operator()(std::shared_ptr const &) const { + return {}; + } +#endif // ELECTROSTATICS +}; + +struct ShortRangeEnergyKernel + : public boost::static_visitor>> { + + using kernel_type = result_type::value_type; + +#ifdef ELECTROSTATICS + template + result_type operator()(std::shared_ptr const &ptr) const { + auto const &actor = *ptr; + return kernel_type{[&actor](Particle const &, Particle const &, double q1q2, + Utils::Vector3d const &, double dist) { + return actor.pair_energy(q1q2, dist); + }}; + } +#ifdef P3M + result_type + operator()(std::shared_ptr const &ptr) const { + auto const &actor = *ptr; + auto const energy_kernel = boost::apply_visitor(*this, actor.base_solver); + return kernel_type{[&actor, energy_kernel]( + Particle const &p1, Particle const &p2, double q1q2, + Utils::Vector3d const &d, double dist) { + auto energy = 0.; + if (energy_kernel) { + energy = (*energy_kernel)(p1, p2, q1q2, d, dist); + } + return energy + actor.pair_energy_correction(q1q2, p1, p2); + }}; + } +#endif // P3M +#ifdef MMM1D_GPU + result_type operator()(std::shared_ptr const &) const { + return {}; + } +#endif // MMM1D_GPU + result_type operator()(std::shared_ptr const &actor) const { + return kernel_type{[&actor](Particle const &, Particle const &, double q1q2, + Utils::Vector3d const &d, double dist) { + return actor->pair_energy(q1q2, d, dist); + }}; + } +#endif // ELECTROSTATICS +}; + +inline ShortRangeForceCorrectionsKernel::result_type pair_force_elc_kernel() { +#ifdef ELECTROSTATICS + if (electrostatics_actor) { + auto const visitor = ShortRangeForceCorrectionsKernel{}; + return boost::apply_visitor(visitor, *electrostatics_actor); + } +#endif // ELECTROSTATICS + return {}; +} + +inline ShortRangePressureKernel::result_type pair_pressure_kernel() { +#ifdef ELECTROSTATICS + if (electrostatics_actor) { + auto const visitor = ShortRangePressureKernel{}; + return boost::apply_visitor(visitor, *electrostatics_actor); + } +#endif // ELECTROSTATICS + return {}; +} + +inline ShortRangeEnergyKernel::result_type pair_energy_kernel() { +#ifdef ELECTROSTATICS + if (electrostatics_actor) { + auto const visitor = ShortRangeEnergyKernel{}; + return boost::apply_visitor(visitor, *electrostatics_actor); + } +#endif // ELECTROSTATICS + return {}; +} + +} // namespace Coulomb + +#endif diff --git a/src/core/electrostatics/debye_hueckel.hpp b/src/core/electrostatics/debye_hueckel.hpp new file mode 100644 index 00000000000..1994b5e2bf7 --- /dev/null +++ b/src/core/electrostatics/debye_hueckel.hpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file + * Calculate the Debye-Hückel energy and force for a particle pair. + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_DEBYE_HUECKEL_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_DEBYE_HUECKEL_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "electrostatics/actor.hpp" + +#include "Particle.hpp" + +#include +#include + +#include +#include + +/** @brief Debye-Hückel parameters. */ +struct DebyeHueckel : public Coulomb::Actor { + /** @brief Ionic strength. */ + double kappa; + /** @brief Interaction cutoff. */ + double r_cut; + + DebyeHueckel(double prefactor, double kappa, double r_cut) { + if (kappa < 0.0) { + throw std::domain_error("Parameter 'kappa' must be >= 0"); + } + if (r_cut < 0.0) { + throw std::domain_error("Parameter 'r_cut' must be >= 0"); + } + + set_prefactor(prefactor); + this->kappa = kappa; + this->r_cut = r_cut; + } + + void on_activation() const { sanity_checks(); } + void on_boxl_change() const {} + void on_node_grid_change() const {} + void on_periodicity_change() const {} + void on_cell_structure_change() const {} + void init() const {} + + void sanity_checks() const { sanity_checks_charge_neutrality(); } + + /** @brief Compute the pair force. + * @param[in] q1q2 Product of the charges on p1 and p2. + * @param[in] d Vector pointing from p1 to p2. + * @param[in] dist Distance between p1 and p2. + */ + Utils::Vector3d pair_force(double const q1q2, Utils::Vector3d const &d, + double const dist) const { + if (dist >= r_cut) { + return {}; + } + // pure Coulomb case + auto fac = prefactor * q1q2 / Utils::int_pow<3>(dist); + if (kappa > 0.) { + // Debye-Hueckel case + fac *= std::exp(-kappa * dist) * (1. + kappa * dist); + } + return fac * d; + } + + /** @brief Compute the pair energy. + * @param q1q2 Product of the charges on p1 and p2. + * @param dist Distance between p1 and p2. + */ + double pair_energy(double const q1q2, double const dist) const { + if (dist >= r_cut) { + return 0.; + } + // pure Coulomb case + auto energy = prefactor * q1q2 / dist; + if (kappa > 0.) { + // Debye-Hueckel case + energy *= std::exp(-kappa * dist); + } + return energy; + } +}; + +#endif // ELECTROSTATICS + +#endif diff --git a/src/core/electrostatics/elc.cpp b/src/core/electrostatics/elc.cpp new file mode 100644 index 00000000000..07fab44ca34 --- /dev/null +++ b/src/core/electrostatics/elc.cpp @@ -0,0 +1,1224 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef P3M + +#include "electrostatics/elc.hpp" + +#include "electrostatics/coulomb.hpp" +#include "electrostatics/mmm-common.hpp" +#include "electrostatics/p3m.hpp" +#include "electrostatics/p3m_gpu.hpp" + +#include "Particle.hpp" +#include "ParticleRange.hpp" +#include "cells.hpp" +#include "communication.hpp" +#include "errorhandling.hpp" +#include "event.hpp" +#include "grid.hpp" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +/** \name Product decomposition data organization + * For the cell blocks it is assumed that the lower blocks part is in the + * lower half. This has to have positive sign, so that has to be first. + */ +/**@{*/ +#define POQESP 0 +#define POQECP 1 +#define POQESM 2 +#define POQECM 3 + +#define PQESSP 0 +#define PQESCP 1 +#define PQECSP 2 +#define PQECCP 3 +#define PQESSM 4 +#define PQESCM 5 +#define PQECSM 6 +#define PQECCM 7 +/**@}*/ + +/** ELC axes (x and y directions)*/ +enum class PoQ : int { P, Q }; +/** ELC charge sum/assign protocol: real charges, image charges, or both. */ +enum class ChargeProtocol : int { REAL, IMAGE, BOTH }; + +/** temporary buffers for product decomposition */ +static std::vector partblk; +/** collected data from the other cells */ +static double gblcblk[8]; + +/** structure for caching sin and cos values */ +struct SCCache { + double s, c; +}; + +/** Cached sin/cos values along the x-axis and y-axis */ +/**@{*/ +static std::vector scxcache; +static std::vector scycache; +/**@}*/ + +/** + * @brief Calculate cached sin/cos values for one direction. + * + * @tparam dir Index of the dimension to consider (e.g. 0 for x ...). + * + * @param particles Particle to calculate values for + * @param n_freq Number of frequencies to calculate per particle + * @param u Inverse box length + * @return Calculated values. + */ +template +static std::vector calc_sc_cache(ParticleRange const &particles, + std::size_t n_freq, double u) { + auto constexpr c_2pi = 2. * Utils::pi(); + auto const n_part = particles.size(); + std::vector ret(n_freq * n_part); + + for (std::size_t freq = 1; freq <= n_freq; freq++) { + auto const pref = c_2pi * u * static_cast(freq); + + std::size_t o = (freq - 1) * n_part; + for (auto const &part : particles) { + auto const arg = pref * part.r.p[dir]; + ret[o++] = {sin(arg), cos(arg)}; + } + } + + return ret; +} + +static std::pair +prepare_sc_cache(ParticleRange const &particles, double far_cut) { + assert(far_cut >= 0.); + auto const n_freq_x = + static_cast(std::ceil(far_cut * box_geo.length()[0]) + 1.); + auto const n_freq_y = + static_cast(std::ceil(far_cut * box_geo.length()[1]) + 1.); + auto const u_x = box_geo.length_inv()[0]; + auto const u_y = box_geo.length_inv()[1]; + scxcache = calc_sc_cache<0>(particles, n_freq_x, u_x); + scycache = calc_sc_cache<1>(particles, n_freq_y, u_y); + return {n_freq_x, n_freq_y}; +} + +/*****************************************************************/ +/* data distribution */ +/*****************************************************************/ + +static void clear_vec(double *pdc, std::size_t size) { + for (std::size_t i = 0; i < size; i++) + pdc[i] = 0.; +} + +static void copy_vec(double *pdc_d, double const *pdc_s, std::size_t size) { + for (std::size_t i = 0; i < size; i++) + pdc_d[i] = pdc_s[i]; +} + +static void add_vec(double *pdc_d, double const *pdc_s1, double const *pdc_s2, + std::size_t size) { + for (std::size_t i = 0; i < size; i++) + pdc_d[i] = pdc_s1[i] + pdc_s2[i]; +} + +static void addscale_vec(double *pdc_d, double scale, double const *pdc_s1, + double const *pdc_s2, std::size_t size) { + for (std::size_t i = 0; i < size; i++) + pdc_d[i] = scale * pdc_s1[i] + pdc_s2[i]; +} + +static void scale_vec(double scale, double *pdc, std::size_t size) { + for (std::size_t i = 0; i < size; i++) + pdc[i] *= scale; +} + +static double *block(double *p, std::size_t index, std::size_t size) { + return &p[index * size]; +} + +static void distribute(std::size_t size) { + assert(size <= 8); + double send_buf[8]; + copy_vec(send_buf, gblcblk, size); + boost::mpi::all_reduce(comm_cart, send_buf, static_cast(size), gblcblk, + std::plus<>()); +} + +void ElectrostaticLayerCorrection::check_gap(Particle const &p) const { + if (p.q() != 0.) { + auto const z = p.pos()[2]; + if (z < 0. or z > elc.box_h) { + runtimeErrorMsg() << "Particle " << p.id() << " entered ELC gap " + << "region by " << ((z < 0.) ? z : z - elc.box_h); + } + } +} + +/*****************************************************************/ +/* dipole terms */ +/*****************************************************************/ + +/** Calculate the dipole force. + * See @cite yeh99a. + */ +void ElectrostaticLayerCorrection::add_dipole_force( + ParticleRange const &particles) const { + constexpr std::size_t size = 3; + auto const pref = prefactor * 4. * Utils::pi() / box_geo.volume(); + + /* for non-neutral systems, this shift gives the background contribution + * (rsp. for this shift, the DM of the background is zero) */ + auto const shift = box_geo.length_half()[2]; + + // collect moments + + gblcblk[0] = 0.; // sum q_i (z_i - L/2) + gblcblk[1] = 0.; // sum q_i z_i + gblcblk[2] = 0.; // sum q_i + + for (auto const &p : particles) { + check_gap(p); + auto const q = p.q(); + auto const z = p.pos()[2]; + + gblcblk[0] += q * (z - shift); + gblcblk[1] += q * z; + gblcblk[2] += q; + + if (elc.dielectric_contrast_on) { + if (z < elc.space_layer) { + gblcblk[0] += elc.delta_mid_bot * q * (-z - shift); + gblcblk[2] += elc.delta_mid_bot * q; + } + if (z > (elc.box_h - elc.space_layer)) { + gblcblk[0] += elc.delta_mid_top * q * (2. * elc.box_h - z - shift); + gblcblk[2] += elc.delta_mid_top * q; + } + } + } + + gblcblk[0] *= pref; + gblcblk[1] *= pref / elc.box_h * box_geo.length()[2]; + gblcblk[2] *= pref; + + distribute(size); + + // Yeh + Berkowitz dipole term @cite yeh99a + auto field_tot = gblcblk[0]; + + // Constand potential contribution + if (elc.const_pot) { + auto const field_induced = gblcblk[1]; + auto const field_applied = elc.pot_diff / elc.box_h; + field_tot -= field_applied + field_induced; + } + + for (auto &p : particles) { + p.force()[2] -= field_tot * p.q(); + + if (!elc.neutralize) { + // SUBTRACT the forces of the P3M homogeneous neutralizing background + p.force()[2] += gblcblk[2] * p.q() * (p.pos()[2] - shift); + } + } +} + +/** Calculate the dipole energy. + * See @cite yeh99a. + */ +double ElectrostaticLayerCorrection::dipole_energy( + ParticleRange const &particles) const { + constexpr std::size_t size = 7; + auto const pref = prefactor * 2. * Utils::pi() / box_geo.volume(); + auto const lz = box_geo.length()[2]; + /* for nonneutral systems, this shift gives the background contribution + (rsp. for this shift, the DM of the background is zero) */ + auto const shift = box_geo.length_half()[2]; + + // collect moments + + gblcblk[0] = 0.; // sum q_i primary box + gblcblk[1] = 0.; // sum q_i boundary layers + gblcblk[2] = 0.; // sum q_i (z_i - L/2) primary box + gblcblk[3] = 0.; // sum q_i (z_i - L/2) boundary layers + gblcblk[4] = 0.; // sum q_i (z_i - L/2)^2 primary box + gblcblk[5] = 0.; // sum q_i (z_i - L/2)^2 boundary layers + gblcblk[6] = 0.; // sum q_i z_i primary box + + for (auto &p : particles) { + check_gap(p); + auto const q = p.q(); + auto const z = p.pos()[2]; + + gblcblk[0] += q; + gblcblk[2] += q * (z - shift); + gblcblk[4] += q * (Utils::sqr(z - shift)); + gblcblk[6] += q * z; + + if (elc.dielectric_contrast_on) { + if (z < elc.space_layer) { + gblcblk[1] += elc.delta_mid_bot * q; + gblcblk[3] += elc.delta_mid_bot * q * (-z - shift); + gblcblk[5] += elc.delta_mid_bot * q * (Utils::sqr(-z - shift)); + } + if (z > (elc.box_h - elc.space_layer)) { + gblcblk[1] += elc.delta_mid_top * q; + gblcblk[3] += elc.delta_mid_top * q * (2. * elc.box_h - z - shift); + gblcblk[5] += + elc.delta_mid_top * q * (Utils::sqr(2. * elc.box_h - z - shift)); + } + } + } + + distribute(size); + + // Yeh + Berkowitz term @cite yeh99a + auto energy = 2. * pref * (Utils::sqr(gblcblk[2]) + gblcblk[2] * gblcblk[3]); + + if (!elc.neutralize) { + // SUBTRACT the energy of the P3M homogeneous neutralizing background + energy += 2. * pref * + (-gblcblk[0] * gblcblk[4] - + (.25 - .5 / 3.) * Utils::sqr(gblcblk[0] * lz)); + } + + if (elc.dielectric_contrast_on) { + if (elc.const_pot) { + // zero potential difference contribution + energy += pref / elc.box_h * lz * Utils::sqr(gblcblk[6]); + // external potential shift contribution + energy -= 2. * elc.pot_diff / elc.box_h * gblcblk[6]; + } + + /* counter the P3M homogeneous background contribution to the + boundaries. We never need that, since a homogeneous background + spanning the artificial boundary layers is aphysical. */ + energy += + pref * (-(gblcblk[1] * gblcblk[4] + gblcblk[0] * gblcblk[5]) - + (1. - 2. / 3.) * gblcblk[0] * gblcblk[1] * Utils::sqr(lz)); + } + + return this_node == 0 ? energy : 0.; +} + +/*****************************************************************/ + +static auto image_sum_b(double q, double z, double d) { + auto const shift = box_geo.length_half()[2]; + auto const lz = box_geo.length()[2]; + return q / (1. - d) * (z - 2. * d * lz / (1. - d)) - q * shift / (1. - d); +} + +static auto image_sum_t(double q, double z, double d) { + auto const shift = box_geo.length_half()[2]; + auto const lz = box_geo.length()[2]; + return q / (1. - d) * (z + 2. * d * lz / (1. - d)) - q * shift / (1. - d); +} + +double +ElectrostaticLayerCorrection::z_energy(ParticleRange const &particles) const { + constexpr std::size_t size = 4; + auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1]; + auto const pref = prefactor * 2. * Utils::pi() * xy_area_inv; + auto const delta = elc.delta_mid_top * elc.delta_mid_bot; + auto const fac_delta_mid_bot = elc.delta_mid_bot / (1. - delta); + auto const fac_delta_mid_top = elc.delta_mid_top / (1. - delta); + auto const fac_delta = delta / (1. - delta); + + /* for non-neutral systems, this shift gives the background contribution + * (rsp. for this shift, the DM of the background is zero) */ + double const shift = box_geo.length_half()[2]; + + if (elc.dielectric_contrast_on) { + if (elc.const_pot) { + clear_vec(gblcblk, size); + for (auto &p : particles) { + auto const z = p.pos()[2]; + auto const q = p.q(); + gblcblk[0] += q; + gblcblk[1] += q * (z - shift); + if (z < elc.space_layer) { + gblcblk[2] -= elc.delta_mid_bot * q; + gblcblk[3] -= elc.delta_mid_bot * q * (-z - shift); + } + if (z > (elc.box_h - elc.space_layer)) { + gblcblk[2] += elc.delta_mid_top * q; + gblcblk[3] += elc.delta_mid_top * q * (2. * elc.box_h - z - shift); + } + } + } else { + // metallic boundaries + clear_vec(gblcblk, size); + auto const h = elc.box_h; + for (auto &p : particles) { + auto const z = p.pos()[2]; + auto const q = p.q(); + gblcblk[0] += q; + gblcblk[1] += q * (z - shift); + if (elc.dielectric_contrast_on) { + if (z < elc.space_layer) { + gblcblk[2] += fac_delta * (elc.delta_mid_bot + 1.) * q; + gblcblk[3] += q * (image_sum_b(elc.delta_mid_bot * delta, + -(2. * h + z), delta) + + image_sum_b(delta, -(2. * h - z), delta)); + } else { + gblcblk[2] += fac_delta_mid_bot * (1. + elc.delta_mid_top) * q; + gblcblk[3] += q * (image_sum_b(elc.delta_mid_bot, -z, delta) + + image_sum_b(delta, -(2. * h - z), delta)); + } + if (z > (h - elc.space_layer)) { + // note the minus sign here which is required due to |z_i-z_j| + gblcblk[2] -= fac_delta * (elc.delta_mid_top + 1.) * q; + gblcblk[3] -= + q * (image_sum_t(elc.delta_mid_top * delta, 4. * h - z, delta) + + image_sum_t(delta, 2. * h + z, delta)); + } else { + // note the minus sign here which is required due to |z_i-z_j| + gblcblk[2] -= fac_delta_mid_top * (1. + elc.delta_mid_bot) * q; + gblcblk[3] -= + q * (image_sum_t(elc.delta_mid_top, 2. * h - z, delta) + + image_sum_t(delta, 2. * h + z, delta)); + } + } + } + } + } + distribute(size); + + auto const energy = gblcblk[1] * gblcblk[2] - gblcblk[0] * gblcblk[3]; + return (this_node == 0) ? -pref * energy : 0.; +} + +void ElectrostaticLayerCorrection::add_z_force( + ParticleRange const &particles) const { + constexpr std::size_t size = 1; + auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1]; + auto const pref = prefactor * 2. * Utils::pi() * xy_area_inv; + auto const delta = elc.delta_mid_top * elc.delta_mid_bot; + auto const fac_delta_mid_bot = elc.delta_mid_bot / (1. - delta); + auto const fac_delta_mid_top = elc.delta_mid_top / (1. - delta); + auto const fac_delta = delta / (1. - delta); + + if (elc.dielectric_contrast_on) { + if (elc.const_pot) { + clear_vec(gblcblk, size); + /* just counter the 2 pi |z| contribution stemming from P3M */ + for (auto &p : particles) { + auto const z = p.pos()[2]; + auto const q = p.q(); + if (z < elc.space_layer) + gblcblk[0] -= elc.delta_mid_bot * q; + if (z > (elc.box_h - elc.space_layer)) + gblcblk[0] += elc.delta_mid_top * q; + } + } else { + clear_vec(gblcblk, size); + for (auto &p : particles) { + auto const z = p.pos()[2]; + auto const q = p.q(); + if (z < elc.space_layer) { + gblcblk[0] += fac_delta * (elc.delta_mid_bot + 1.) * q; + } else { + gblcblk[0] += fac_delta_mid_bot * (elc.delta_mid_top + 1.) * q; + } + if (z > (elc.box_h - elc.space_layer)) { + // note the minus sign here which is required due to |z_i-z_j| + gblcblk[0] -= fac_delta * (elc.delta_mid_top + 1.) * q; + } else { + // note the minus sign here which is required due to |z_i-z_j| + gblcblk[0] -= fac_delta_mid_top * (elc.delta_mid_bot + 1.) * q; + } + } + } + + gblcblk[0] *= pref; + + distribute(size); + + for (auto &p : particles) { + p.force()[2] += gblcblk[0] * p.q(); + } + } +} + +/*****************************************************************/ +/* PoQ exp sum */ +/*****************************************************************/ + +/** \name q=0 or p=0 per frequency code */ +/**@{*/ +template +void setup_PoQ(elc_data const &elc, double prefactor, std::size_t index, + double omega, ParticleRange const &particles) { + assert(index >= 1); + constexpr std::size_t size = 4; + auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1]; + auto const pref_di = prefactor * 4. * Utils::pi() * xy_area_inv; + auto const pref = -pref_di / expm1(omega * box_geo.length()[2]); + double lclimgebot[4], lclimgetop[4], lclimge[4]; + double fac_delta_mid_bot = 1., fac_delta_mid_top = 1., fac_delta = 1.; + + if (elc.dielectric_contrast_on) { + auto const delta = elc.delta_mid_top * elc.delta_mid_bot; + auto const fac_elc = 1. / (1. - delta * exp(-omega * 2. * elc.box_h)); + fac_delta_mid_bot = elc.delta_mid_bot * fac_elc; + fac_delta_mid_top = elc.delta_mid_top * fac_elc; + fac_delta = fac_delta_mid_bot * elc.delta_mid_top; + } + + clear_vec(lclimge, size); + clear_vec(gblcblk, size); + auto const &sc_cache = (axis == PoQ::P) ? scxcache : scycache; + + std::size_t ic = 0; + auto const o = (index - 1) * particles.size(); + for (auto &p : particles) { + auto const z = p.pos()[2]; + auto const q = p.q(); + auto e = exp(omega * z); + + partblk[size * ic + POQESM] = q * sc_cache[o + ic].s / e; + partblk[size * ic + POQESP] = q * sc_cache[o + ic].s * e; + partblk[size * ic + POQECM] = q * sc_cache[o + ic].c / e; + partblk[size * ic + POQECP] = q * sc_cache[o + ic].c * e; + + add_vec(gblcblk, gblcblk, block(partblk.data(), ic, size), size); + + if (elc.dielectric_contrast_on) { + if (z < elc.space_layer) { // handle the lower case first + // negative sign is okay here as the image is located at -z + + e = exp(-omega * z); + + auto const scale = q * elc.delta_mid_bot; + + lclimgebot[POQESM] = sc_cache[o + ic].s / e; + lclimgebot[POQESP] = sc_cache[o + ic].s * e; + lclimgebot[POQECM] = sc_cache[o + ic].c / e; + lclimgebot[POQECP] = sc_cache[o + ic].c * e; + + addscale_vec(gblcblk, scale, lclimgebot, gblcblk, size); + + e = (exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot + + exp(omega * (+z - 2. * elc.box_h))) * + fac_delta; + } else { + e = (exp(-omega * z) + + exp(omega * (z - 2. * elc.box_h)) * elc.delta_mid_top) * + fac_delta_mid_bot; + } + + lclimge[POQESP] += q * sc_cache[o + ic].s * e; + lclimge[POQECP] += q * sc_cache[o + ic].c * e; + + if (z > (elc.box_h - elc.space_layer)) { // handle the upper case now + e = exp(omega * (2. * elc.box_h - z)); + + auto const scale = q * elc.delta_mid_top; + + lclimgetop[POQESM] = sc_cache[o + ic].s / e; + lclimgetop[POQESP] = sc_cache[o + ic].s * e; + lclimgetop[POQECM] = sc_cache[o + ic].c / e; + lclimgetop[POQECP] = sc_cache[o + ic].c * e; + + addscale_vec(gblcblk, scale, lclimgetop, gblcblk, size); + + e = (exp(omega * (+z - 4. * elc.box_h)) * elc.delta_mid_top + + exp(omega * (-z - 2. * elc.box_h))) * + fac_delta; + } else { + e = (exp(omega * (+z - 2. * elc.box_h)) + + exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot) * + fac_delta_mid_top; + } + + lclimge[POQESM] += q * sc_cache[o + ic].s * e; + lclimge[POQECM] += q * sc_cache[o + ic].c * e; + } + + ++ic; + } + + scale_vec(pref, gblcblk, size); + + if (elc.dielectric_contrast_on) { + scale_vec(pref_di, lclimge, size); + add_vec(gblcblk, gblcblk, lclimge, size); + } +} + +template void add_PoQ_force(ParticleRange const &particles) { + constexpr auto i = static_cast(axis); + constexpr std::size_t size = 4; + + std::size_t ic = 0; + for (auto &p : particles) { + auto &force = p.force(); + force[i] += partblk[size * ic + POQESM] * gblcblk[POQECP] - + partblk[size * ic + POQECM] * gblcblk[POQESP] + + partblk[size * ic + POQESP] * gblcblk[POQECM] - + partblk[size * ic + POQECP] * gblcblk[POQESM]; + force[2] += partblk[size * ic + POQECM] * gblcblk[POQECP] + + partblk[size * ic + POQESM] * gblcblk[POQESP] - + partblk[size * ic + POQECP] * gblcblk[POQECM] - + partblk[size * ic + POQESP] * gblcblk[POQESM]; + ++ic; + } +} + +static double PoQ_energy(double omega, std::size_t n_part) { + constexpr std::size_t size = 4; + + auto energy = 0.; + for (std::size_t ic = 0; ic < n_part; ic++) { + energy += partblk[size * ic + POQECM] * gblcblk[POQECP] + + partblk[size * ic + POQESM] * gblcblk[POQESP] + + partblk[size * ic + POQECP] * gblcblk[POQECM] + + partblk[size * ic + POQESP] * gblcblk[POQESM]; + } + + return energy / omega; +} +/**@}*/ + +/*****************************************************************/ +/* PQ particle blocks */ +/*****************************************************************/ + +/** \name p,q <> 0 per frequency code */ +/**@{*/ +static void setup_PQ(elc_data const &elc, double prefactor, std::size_t index_p, + std::size_t index_q, double omega, + ParticleRange const &particles) { + assert(index_p >= 1); + assert(index_q >= 1); + constexpr std::size_t size = 8; + auto const xy_area_inv = box_geo.length_inv()[0] * box_geo.length_inv()[1]; + auto const pref_di = prefactor * 8 * Utils::pi() * xy_area_inv; + auto const pref = -pref_di / expm1(omega * box_geo.length()[2]); + double lclimgebot[8], lclimgetop[8], lclimge[8]; + double fac_delta_mid_bot = 1, fac_delta_mid_top = 1, fac_delta = 1; + if (elc.dielectric_contrast_on) { + auto const delta = elc.delta_mid_top * elc.delta_mid_bot; + auto const fac_elc = 1. / (1. - delta * exp(-omega * 2. * elc.box_h)); + fac_delta_mid_bot = elc.delta_mid_bot * fac_elc; + fac_delta_mid_top = elc.delta_mid_top * fac_elc; + fac_delta = fac_delta_mid_bot * elc.delta_mid_top; + } + + clear_vec(lclimge, size); + clear_vec(gblcblk, size); + + std::size_t ic = 0; + auto const ox = (index_p - 1) * particles.size(); + auto const oy = (index_q - 1) * particles.size(); + for (auto const &p : particles) { + auto const z = p.pos()[2]; + auto const q = p.q(); + auto e = exp(omega * z); + + partblk[size * ic + PQESSM] = + scxcache[ox + ic].s * scycache[oy + ic].s * q / e; + partblk[size * ic + PQESCM] = + scxcache[ox + ic].s * scycache[oy + ic].c * q / e; + partblk[size * ic + PQECSM] = + scxcache[ox + ic].c * scycache[oy + ic].s * q / e; + partblk[size * ic + PQECCM] = + scxcache[ox + ic].c * scycache[oy + ic].c * q / e; + + partblk[size * ic + PQESSP] = + scxcache[ox + ic].s * scycache[oy + ic].s * q * e; + partblk[size * ic + PQESCP] = + scxcache[ox + ic].s * scycache[oy + ic].c * q * e; + partblk[size * ic + PQECSP] = + scxcache[ox + ic].c * scycache[oy + ic].s * q * e; + partblk[size * ic + PQECCP] = + scxcache[ox + ic].c * scycache[oy + ic].c * q * e; + + add_vec(gblcblk, gblcblk, block(partblk.data(), ic, size), size); + + if (elc.dielectric_contrast_on) { + if (z < elc.space_layer) { // handle the lower case first + // change e to take into account the z position of the images + + e = exp(-omega * z); + auto const scale = q * elc.delta_mid_bot; + + lclimgebot[PQESSM] = scxcache[ox + ic].s * scycache[oy + ic].s / e; + lclimgebot[PQESCM] = scxcache[ox + ic].s * scycache[oy + ic].c / e; + lclimgebot[PQECSM] = scxcache[ox + ic].c * scycache[oy + ic].s / e; + lclimgebot[PQECCM] = scxcache[ox + ic].c * scycache[oy + ic].c / e; + + lclimgebot[PQESSP] = scxcache[ox + ic].s * scycache[oy + ic].s * e; + lclimgebot[PQESCP] = scxcache[ox + ic].s * scycache[oy + ic].c * e; + lclimgebot[PQECSP] = scxcache[ox + ic].c * scycache[oy + ic].s * e; + lclimgebot[PQECCP] = scxcache[ox + ic].c * scycache[oy + ic].c * e; + + addscale_vec(gblcblk, scale, lclimgebot, gblcblk, size); + + e = (exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot + + exp(omega * (+z - 2. * elc.box_h))) * + fac_delta * q; + + } else { + + e = (exp(-omega * z) + + exp(omega * (z - 2. * elc.box_h)) * elc.delta_mid_top) * + fac_delta_mid_bot * q; + } + + lclimge[PQESSP] += scxcache[ox + ic].s * scycache[oy + ic].s * e; + lclimge[PQESCP] += scxcache[ox + ic].s * scycache[oy + ic].c * e; + lclimge[PQECSP] += scxcache[ox + ic].c * scycache[oy + ic].s * e; + lclimge[PQECCP] += scxcache[ox + ic].c * scycache[oy + ic].c * e; + + if (z > (elc.box_h - elc.space_layer)) { // handle the upper case now + + e = exp(omega * (2. * elc.box_h - z)); + auto const scale = q * elc.delta_mid_top; + + lclimgetop[PQESSM] = scxcache[ox + ic].s * scycache[oy + ic].s / e; + lclimgetop[PQESCM] = scxcache[ox + ic].s * scycache[oy + ic].c / e; + lclimgetop[PQECSM] = scxcache[ox + ic].c * scycache[oy + ic].s / e; + lclimgetop[PQECCM] = scxcache[ox + ic].c * scycache[oy + ic].c / e; + + lclimgetop[PQESSP] = scxcache[ox + ic].s * scycache[oy + ic].s * e; + lclimgetop[PQESCP] = scxcache[ox + ic].s * scycache[oy + ic].c * e; + lclimgetop[PQECSP] = scxcache[ox + ic].c * scycache[oy + ic].s * e; + lclimgetop[PQECCP] = scxcache[ox + ic].c * scycache[oy + ic].c * e; + + addscale_vec(gblcblk, scale, lclimgetop, gblcblk, size); + + e = (exp(omega * (+z - 4. * elc.box_h)) * elc.delta_mid_top + + exp(omega * (-z - 2. * elc.box_h))) * + fac_delta * q; + + } else { + + e = (exp(omega * (+z - 2. * elc.box_h)) + + exp(omega * (-z - 2. * elc.box_h)) * elc.delta_mid_bot) * + fac_delta_mid_top * q; + } + + lclimge[PQESSM] += scxcache[ox + ic].s * scycache[oy + ic].s * e; + lclimge[PQESCM] += scxcache[ox + ic].s * scycache[oy + ic].c * e; + lclimge[PQECSM] += scxcache[ox + ic].c * scycache[oy + ic].s * e; + lclimge[PQECCM] += scxcache[ox + ic].c * scycache[oy + ic].c * e; + } + + ic++; + } + + scale_vec(pref, gblcblk, size); + if (elc.dielectric_contrast_on) { + scale_vec(pref_di, lclimge, size); + add_vec(gblcblk, gblcblk, lclimge, size); + } +} + +static void add_PQ_force(std::size_t index_p, std::size_t index_q, double omega, + const ParticleRange &particles) { + auto constexpr c_2pi = 2. * Utils::pi(); + auto const pref_x = + c_2pi * box_geo.length_inv()[0] * static_cast(index_p) / omega; + auto const pref_y = + c_2pi * box_geo.length_inv()[1] * static_cast(index_q) / omega; + constexpr std::size_t size = 8; + + std::size_t ic = 0; + for (auto &p : particles) { + auto &force = p.force(); + force[0] += pref_x * (partblk[size * ic + PQESCM] * gblcblk[PQECCP] + + partblk[size * ic + PQESSM] * gblcblk[PQECSP] - + partblk[size * ic + PQECCM] * gblcblk[PQESCP] - + partblk[size * ic + PQECSM] * gblcblk[PQESSP] + + partblk[size * ic + PQESCP] * gblcblk[PQECCM] + + partblk[size * ic + PQESSP] * gblcblk[PQECSM] - + partblk[size * ic + PQECCP] * gblcblk[PQESCM] - + partblk[size * ic + PQECSP] * gblcblk[PQESSM]); + force[1] += pref_y * (partblk[size * ic + PQECSM] * gblcblk[PQECCP] + + partblk[size * ic + PQESSM] * gblcblk[PQESCP] - + partblk[size * ic + PQECCM] * gblcblk[PQECSP] - + partblk[size * ic + PQESCM] * gblcblk[PQESSP] + + partblk[size * ic + PQECSP] * gblcblk[PQECCM] + + partblk[size * ic + PQESSP] * gblcblk[PQESCM] - + partblk[size * ic + PQECCP] * gblcblk[PQECSM] - + partblk[size * ic + PQESCP] * gblcblk[PQESSM]); + force[2] += (partblk[size * ic + PQECCM] * gblcblk[PQECCP] + + partblk[size * ic + PQECSM] * gblcblk[PQECSP] + + partblk[size * ic + PQESCM] * gblcblk[PQESCP] + + partblk[size * ic + PQESSM] * gblcblk[PQESSP] - + partblk[size * ic + PQECCP] * gblcblk[PQECCM] - + partblk[size * ic + PQECSP] * gblcblk[PQECSM] - + partblk[size * ic + PQESCP] * gblcblk[PQESCM] - + partblk[size * ic + PQESSP] * gblcblk[PQESSM]); + ic++; + } +} + +static double PQ_energy(double omega, std::size_t n_part) { + constexpr std::size_t size = 8; + + auto energy = 0.; + for (std::size_t ic = 0; ic < n_part; ic++) { + energy += partblk[size * ic + PQECCM] * gblcblk[PQECCP] + + partblk[size * ic + PQECSM] * gblcblk[PQECSP] + + partblk[size * ic + PQESCM] * gblcblk[PQESCP] + + partblk[size * ic + PQESSM] * gblcblk[PQESSP] + + partblk[size * ic + PQECCP] * gblcblk[PQECCM] + + partblk[size * ic + PQECSP] * gblcblk[PQECSM] + + partblk[size * ic + PQESCP] * gblcblk[PQESCM] + + partblk[size * ic + PQESSP] * gblcblk[PQESSM]; + } + return energy / omega; +} +/**@}*/ + +void ElectrostaticLayerCorrection::add_force( + ParticleRange const &particles) const { + auto constexpr c_2pi = 2. * Utils::pi(); + auto const n_freqs = prepare_sc_cache(particles, elc.far_cut); + auto const n_scxcache = std::get<0>(n_freqs); + auto const n_scycache = std::get<1>(n_freqs); + partblk.resize(particles.size() * 8); + + add_dipole_force(particles); + add_z_force(particles); + + /* the second condition is just for the case of numerical accident */ + for (std::size_t p = 1; + box_geo.length_inv()[0] * static_cast(p - 1) < elc.far_cut && + p <= n_scxcache; + p++) { + auto const omega = c_2pi * box_geo.length_inv()[0] * static_cast(p); + setup_PoQ(elc, prefactor, p, omega, particles); + distribute(4); + add_PoQ_force(particles); + } + + for (std::size_t q = 1; + box_geo.length_inv()[1] * static_cast(q - 1) < elc.far_cut && + q <= n_scycache; + q++) { + auto const omega = c_2pi * box_geo.length_inv()[1] * static_cast(q); + setup_PoQ(elc, prefactor, q, omega, particles); + distribute(4); + add_PoQ_force(particles); + } + + for (std::size_t p = 1; + box_geo.length_inv()[0] * static_cast(p - 1) < elc.far_cut && + p <= n_scxcache; + p++) { + for (std::size_t q = 1; + Utils::sqr(box_geo.length_inv()[0] * static_cast(p - 1)) + + Utils::sqr(box_geo.length_inv()[1] * + static_cast(q - 1)) < + elc.far_cut2 && + q <= n_scycache; + q++) { + auto const omega = + c_2pi * + sqrt(Utils::sqr(box_geo.length_inv()[0] * static_cast(p)) + + Utils::sqr(box_geo.length_inv()[1] * static_cast(q))); + setup_PQ(elc, prefactor, p, q, omega, particles); + distribute(8); + add_PQ_force(p, q, omega, particles); + } + } +} + +double ElectrostaticLayerCorrection::calc_energy( + ParticleRange const &particles) const { + auto constexpr c_2pi = 2. * Utils::pi(); + auto energy = dipole_energy(particles) + z_energy(particles); + auto const n_freqs = prepare_sc_cache(particles, elc.far_cut); + auto const n_scxcache = std::get<0>(n_freqs); + auto const n_scycache = std::get<1>(n_freqs); + + auto const n_localpart = particles.size(); + partblk.resize(n_localpart * 8); + + /* the second condition is just for the case of numerical accident */ + for (std::size_t p = 1; + box_geo.length_inv()[0] * static_cast(p - 1) < elc.far_cut && + p <= n_scxcache; + p++) { + auto const omega = c_2pi * box_geo.length_inv()[0] * static_cast(p); + setup_PoQ(elc, prefactor, p, omega, particles); + distribute(4); + energy += PoQ_energy(omega, n_localpart); + } + + for (std::size_t q = 1; + box_geo.length_inv()[1] * static_cast(q - 1) < elc.far_cut && + q <= n_scycache; + q++) { + auto const omega = c_2pi * box_geo.length_inv()[1] * static_cast(q); + setup_PoQ(elc, prefactor, q, omega, particles); + distribute(4); + energy += PoQ_energy(omega, n_localpart); + } + + for (std::size_t p = 1; + box_geo.length_inv()[0] * static_cast(p - 1) < elc.far_cut && + p <= n_scxcache; + p++) { + for (std::size_t q = 1; + Utils::sqr(box_geo.length_inv()[0] * static_cast(p - 1)) + + Utils::sqr(box_geo.length_inv()[1] * + static_cast(q - 1)) < + elc.far_cut2 && + q <= n_scycache; + q++) { + auto const omega = + c_2pi * + sqrt(Utils::sqr(box_geo.length_inv()[0] * static_cast(p)) + + Utils::sqr(box_geo.length_inv()[1] * static_cast(q))); + setup_PQ(elc, prefactor, p, q, omega, particles); + distribute(8); + energy += PQ_energy(omega, n_localpart); + } + } + /* we count both i<->j and j<->i, so return just half of it */ + return 0.5 * energy; +} + +double ElectrostaticLayerCorrection::tune_far_cut() const { + // Largest reasonable cutoff for far formula + auto constexpr maximal_far_cut = 50.; + auto const box_l_x_inv = box_geo.length_inv()[0]; + auto const box_l_y_inv = box_geo.length_inv()[1]; + auto const min_inv_boxl = std::min(box_l_x_inv, box_l_y_inv); + auto const box_l_z = box_geo.length()[2]; + // adjust lz according to dielectric layer method + auto const lz = + (elc.dielectric_contrast_on) ? elc.box_h + elc.space_layer : box_l_z; + + auto tuned_far_cut = min_inv_boxl; + double err; + do { + auto const pref = 2. * Utils::pi() * tuned_far_cut; + auto const sum = pref + 2. * (box_l_x_inv + box_l_y_inv); + auto const den = -expm1(-pref * lz); + auto const num1 = exp(pref * (elc.box_h - lz)); + auto const num2 = exp(-pref * (elc.box_h + lz)); + + err = 0.5 / den * + (num1 * (sum + 1. / (lz - elc.box_h)) / (lz - elc.box_h) + + num2 * (sum + 1. / (lz + elc.box_h)) / (lz + elc.box_h)); + + tuned_far_cut += min_inv_boxl; + } while (err > elc.maxPWerror and tuned_far_cut < maximal_far_cut); + if (tuned_far_cut >= maximal_far_cut) { + throw std::runtime_error("ELC tuning failed: maxPWerror too small"); + } + return tuned_far_cut - min_inv_boxl; +} + +static auto calc_total_charge() { + auto local_q = 0.; + for (auto const &p : cell_structure.local_particles()) { + local_q += p.q(); + } + return boost::mpi::all_reduce(comm_cart, local_q, std::plus<>()); +} + +void ElectrostaticLayerCorrection::sanity_checks_periodicity() const { + if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { + throw std::runtime_error("ELC: requires periodicity (1 1 1)"); + } +} + +void ElectrostaticLayerCorrection::sanity_checks_dielectric_contrasts() const { + if (elc.dielectric_contrast_on) { + auto const precision_threshold = std::sqrt(ROUND_ERROR_PREC); + auto const total_charge = std::abs(calc_total_charge()); + if (total_charge >= precision_threshold) { + if (elc.const_pot) { + // Disable this line to make ELC work again with non-neutral systems + // and metallic boundaries + throw std::runtime_error("ELC does not currently support non-neutral " + "systems with a dielectric contrast."); + } + // ELC with non-neutral systems and no fully metallic boundaries + // does not work + throw std::runtime_error("ELC does not work for non-neutral systems and " + "non-metallic dielectric contrast."); + } + } +} + +void ElectrostaticLayerCorrection::adapt_solver() { + boost::apply_visitor( + [this](auto &solver) { + set_prefactor(solver->prefactor); + solver->p3m.params.epsilon = P3M_EPSILON_METALLIC; + }, + base_solver); +} + +void ElectrostaticLayerCorrection::recalc_box_h() { + auto const new_box_h = box_geo.length()[2] - elc.gap_size; + if (new_box_h < 0.) { + throw std::runtime_error("ELC gap size (" + std::to_string(elc.gap_size) + + ") larger than box length in z-direction (" + + std::to_string(box_geo.length()[2]) + ")"); + } + elc.box_h = new_box_h; +} + +void ElectrostaticLayerCorrection::recalc_space_layer() { + if (elc.dielectric_contrast_on) { + auto const p3m_r_cut = boost::apply_visitor( + [](auto &solver) { return solver->p3m.params.r_cut; }, base_solver); + // recalculate the space layer size: + // 1. set the space_layer to be 1/3 of the gap size, so that box = layer + elc.space_layer = (1. / 3.) * elc.gap_size; + // 2. but make sure we don't overlap with the near-field formula + auto const free_space = elc.gap_size - p3m_r_cut; + // 3. and make sure the space layer is not bigger than half the actual + // simulation box, to avoid overlaps + auto const half_box_h = elc.box_h / 2.; + auto const max_space_layer = std::min(free_space, half_box_h); + if (elc.space_layer > max_space_layer) { + if (max_space_layer <= 0.) { + throw std::runtime_error("P3M real-space cutoff too large for ELC w/ " + "dielectric contrast"); + } + elc.space_layer = max_space_layer; + } + elc.space_box = elc.gap_size - 2. * elc.space_layer; + } +} + +elc_data::elc_data(double maxPWerror, double gap_size, double far_cut, + bool neutralize, double delta_top, double delta_bot, + bool with_const_pot, double potential_diff) + : maxPWerror{maxPWerror}, gap_size{gap_size}, + box_h{box_geo.length()[2] - gap_size}, far_cut{far_cut}, far_cut2{-1.}, + far_calculated{far_cut == -1.}, dielectric_contrast_on{delta_top != 0. or + delta_bot != 0.}, + const_pot{with_const_pot and dielectric_contrast_on}, + neutralize{neutralize and !dielectric_contrast_on}, + delta_mid_top{boost::algorithm::clamp(delta_top, -1., +1.)}, + delta_mid_bot{boost::algorithm::clamp(delta_bot, -1., +1.)}, + pot_diff{(with_const_pot) ? potential_diff : 0.}, + // initial setup of parameters, may change later when P3M is finally tuned + // set the space_layer to be 1/3 of the gap size, so that box = layer + space_layer{(dielectric_contrast_on) ? gap_size / 3. : 0.}, + space_box{gap_size - ((dielectric_contrast_on) ? 2. * space_layer : 0.)} { + + auto const delta_range = 1. + std::sqrt(ROUND_ERROR_PREC); + if (far_cut <= 0. and not far_calculated) { + throw std::domain_error("Parameter 'far_cut' must be > 0"); + } + if (maxPWerror <= 0.) { + throw std::domain_error("Parameter 'maxPWerror' must be > 0"); + } + if (gap_size <= 0.) { + throw std::domain_error("Parameter 'gap_size' must be > 0"); + } + if (potential_diff != 0. and not with_const_pot) { + throw std::invalid_argument( + "Parameter 'const_pot' must be True when 'pot_diff' is non-zero"); + } + if (delta_top < -delta_range or delta_top > delta_range) { + throw std::domain_error( + "Parameter 'delta_mid_top' must be >= -1 and <= +1"); + } + if (delta_bot < -delta_range or delta_bot > delta_range) { + throw std::domain_error( + "Parameter 'delta_mid_bot' must be >= -1 and <= +1"); + } + /* Dielectric contrasts: the deltas should be either both -1 or both +1 when + * no constant potential difference is applied. The case of two non-metallic + * parallel boundaries can only be treated with a constant potential. */ + if (dielectric_contrast_on and not const_pot and + (std::fabs(1. - delta_mid_top * delta_mid_bot) < ROUND_ERROR_PREC)) { + throw std::domain_error("ELC with two parallel metallic boundaries " + "requires the const_pot option"); + } +} + +ElectrostaticLayerCorrection::ElectrostaticLayerCorrection( + elc_data &¶meters, BaseSolver &&solver) + : elc{parameters}, base_solver{solver} { + adapt_solver(); +} + +Utils::Vector3d elc_data::get_mi_vector(Utils::Vector3d const &a, + Utils::Vector3d const &b) const { + return box_geo.get_mi_vector(a, b); +} + +static void p3m_assign_image_charge(elc_data const &elc, CoulombP3M &p3m, + double q, Utils::Vector3d const &pos) { + if (pos[2] < elc.space_layer) { + auto const q_eff = elc.delta_mid_bot * q; + p3m.assign_charge(q_eff, {pos[0], pos[1], -pos[2]}); + } + if (pos[2] > (elc.box_h - elc.space_layer)) { + auto const q_eff = elc.delta_mid_top * q; + p3m.assign_charge(q_eff, {pos[0], pos[1], 2. * elc.box_h - pos[2]}); + } +} + +template +void charge_assign(elc_data const &elc, CoulombP3M &solver, + ParticleRange const &particles) { + if (protocol == ChargeProtocol::BOTH or protocol == ChargeProtocol::IMAGE) { + solver.p3m.inter_weights.reset(solver.p3m.params.cao); + } + /* prepare local FFT mesh */ + for (int i = 0; i < solver.p3m.local_mesh.size; i++) + solver.p3m.rs_mesh[i] = 0.; + + for (auto const &p : particles) { + if (p.q() != 0.) { + if (protocol == ChargeProtocol::BOTH or + protocol == ChargeProtocol::REAL) { + solver.assign_charge(p.q(), p.pos(), solver.p3m.inter_weights); + } + if (protocol == ChargeProtocol::BOTH or + protocol == ChargeProtocol::IMAGE) { + p3m_assign_image_charge(elc, solver, p.q(), p.pos()); + } + } + } +} + +template +void modify_p3m_sums(elc_data const &elc, CoulombP3M &solver, + ParticleRange const &particles) { + + Utils::Vector3d node_sums{}; + for (auto const &p : particles) { + auto const q = p.q(); + if (q != 0.) { + auto const z = p.pos()[2]; + + if (protocol == ChargeProtocol::BOTH or + protocol == ChargeProtocol::REAL) { + node_sums[0] += 1.; + node_sums[1] += Utils::sqr(q); + node_sums[2] += q; + } + + if (protocol == ChargeProtocol::BOTH or + protocol == ChargeProtocol::IMAGE) { + if (z < elc.space_layer) { + node_sums[0] += 1.; + node_sums[1] += Utils::sqr(elc.delta_mid_bot * q); + node_sums[2] += elc.delta_mid_bot * q; + } + + if (z > (elc.box_h - elc.space_layer)) { + node_sums[0] += 1.; + node_sums[1] += Utils::sqr(elc.delta_mid_top * q); + node_sums[2] += elc.delta_mid_top * q; + } + } + } + } + + auto const tot_sums = + boost::mpi::all_reduce(comm_cart, node_sums, std::plus<>()); + solver.p3m.sum_qpart = static_cast(tot_sums[0] + 0.1); + solver.p3m.sum_q2 = tot_sums[1]; + solver.p3m.square_sum_q = Utils::sqr(tot_sums[2]); +} + +double ElectrostaticLayerCorrection::long_range_energy( + ParticleRange const &particles) const { + auto const energy = boost::apply_visitor( + [this, &particles](auto const &solver_ptr) { + auto &solver = *solver_ptr; + + // assign the original charges (they may not have been assigned yet) + solver.charge_assign(particles); + + if (!elc.dielectric_contrast_on) { + return solver.long_range_energy(particles); + } + + auto energy = 0.; + energy += 0.5 * solver.long_range_energy(particles); + energy += 0.5 * elc.dielectric_layers_self_energy(solver, particles); + + // assign both original and image charges + charge_assign(elc, solver, particles); + modify_p3m_sums(elc, solver, particles); + energy += 0.5 * solver.long_range_energy(particles); + + // assign only the image charges now + charge_assign(elc, solver, particles); + modify_p3m_sums(elc, solver, particles); + energy -= 0.5 * solver.long_range_energy(particles); + + // restore modified sums + modify_p3m_sums(elc, solver, particles); + + return energy; + }, + base_solver); + return energy + calc_energy(particles); +} + +void ElectrostaticLayerCorrection::add_long_range_forces( + ParticleRange const &particles) const { + boost::apply_visitor( + [this, &particles](auto const &solver_ptr) { + auto &solver = *solver_ptr; + if (elc.dielectric_contrast_on) { + modify_p3m_sums(elc, solver, particles); + charge_assign(elc, solver, particles); + elc.dielectric_layers_self_forces(solver, particles); + } else { + solver.charge_assign(particles); + } + solver.add_long_range_forces(particles); + if (elc.dielectric_contrast_on) { + modify_p3m_sums(elc, solver, particles); + } + }, + base_solver); + add_force(particles); +} + +#endif diff --git a/src/core/electrostatics/elc.hpp b/src/core/electrostatics/elc.hpp new file mode 100644 index 00000000000..3dc586ec67d --- /dev/null +++ b/src/core/electrostatics/elc.hpp @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** @file + * @brief ELC algorithm for long-range Coulomb interactions. + * + * Implementation of the ELC method for the calculation of the electrostatic + * interaction in two dimensional periodic systems. For details on the method + * see MMM in general. The ELC method works together with any three-dimensional + * method, for example @ref p3m.hpp "P3M", with metallic boundary conditions. + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_ELC_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_ELC_HPP + +#include "config.hpp" + +#ifdef P3M + +#include "actor/traits.hpp" + +#include "electrostatics/p3m.hpp" +#include "electrostatics/p3m_gpu.hpp" + +#include "Particle.hpp" +#include "ParticleRange.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + +struct ElectrostaticLayerCorrection; + +namespace traits { +template <> +struct is_layer_correction : std::true_type {}; +} // namespace traits + +/** @brief Parameters for the ELC method */ +struct elc_data { + elc_data(double maxPWerror, double gap_size, double far_cut, bool neutralize, + double delta_top, double delta_bot, bool const_pot, double pot_diff); + + /** + * @brief Maximal allowed pairwise error for the potential and force. + * Note that this counts for the plain 1/r contribution + * alone, without the prefactor and the charge prefactor. + */ + double maxPWerror; + /** Size of the empty gap. Note that ELC relies on the user to make sure + * that this condition is fulfilled. + */ + double gap_size; + /** Up to where particles can be found. */ + double box_h; + /** + * @brief Cutoff of the exponential sum. + * Since in all other MMM methods this is the far formula, + * it is given the same name here. + */ + double far_cut; + /** Squared value of #far_cut. */ + double far_cut2; + /** Flag whether #far_cut was set by the user, or calculated by ESPResSo. + * In the latter case, the cutoff will be adapted if important parameters, + * such as the box dimensions, change. + */ + bool far_calculated; + + /// @brief Flag whether there is any dielectric contrast in the system. + bool dielectric_contrast_on; + /// @brief Flag whether a constant potential difference is applied. + bool const_pot; + /** + * @brief Flag whether the box is neutralized by a homogeneous background. + * If true, use a homogeneous neutralizing background for non-neutral + * systems. Unlike the 3D case, this background adds an additional + * force pointing towards the system center, and the gap size + * enters into the value of the forces, so be careful with this. + */ + bool neutralize; + + /// dielectric contrast in the upper part of the simulation cell. + double delta_mid_top; + /// dielectric contrast in the lower part of the simulation cell. + double delta_mid_bot; + /// @brief Constant potential difference. + double pot_diff; + + /** Layer around the dielectric contrast in which we trick around. */ + double space_layer; + /** The space that is finally left. */ + double space_box; + + /// pairwise contributions from the lowest and top layers + template + void dielectric_layers_contribution(CoulombP3M const &p3m, + Utils::Vector3d const &pos1, + Utils::Vector3d const &pos2, double q1q2, + Kernel &&kernel) const { + if (pos1[2] < space_layer) { + auto const q_eff = delta_mid_bot * q1q2; + auto const d = get_mi_vector(pos2, {pos1[0], pos1[1], -pos1[2]}); + kernel(q_eff, d); + } + if (pos1[2] > (box_h - space_layer)) { + auto const q_eff = delta_mid_top * q1q2; + auto const l = 2. * box_h; + auto const d = get_mi_vector(pos2, {pos1[0], pos1[1], l - pos1[2]}); + kernel(q_eff, d); + } + } + + /// self energies of top and bottom layers with their virtual images + double dielectric_layers_self_energy(CoulombP3M const &p3m, + ParticleRange const &particles) const { + auto energy = 0.; + for (auto const &p : particles) { + dielectric_layers_contribution( + p3m, p.pos(), p.pos(), Utils::sqr(p.q()), + [&](double q1q2, Utils::Vector3d const &d) { + energy += p3m.pair_energy(q1q2, d.norm()); + }); + } + return energy; + } + + /// forces of particles in border layers with themselves + void dielectric_layers_self_forces(CoulombP3M const &p3m, + ParticleRange const &particles) const { + for (auto &p : particles) { + dielectric_layers_contribution( + p3m, p.pos(), p.pos(), Utils::sqr(p.q()), + [&](double q1q2, Utils::Vector3d const &d) { + p.force() += p3m.pair_force(q1q2, d, d.norm()); + }); + } + } + +private: + Utils::Vector3d get_mi_vector(Utils::Vector3d const &a, + Utils::Vector3d const &b) const; +}; + +struct ElectrostaticLayerCorrection + : public Coulomb::Actor { + using BaseSolver = boost::variant< +#ifdef CUDA + std::shared_ptr, +#endif // CUDA + std::shared_ptr>; + + elc_data elc; + + /** Electrostatics solver that is adapted. */ + BaseSolver base_solver; + + ElectrostaticLayerCorrection(elc_data &¶meters, BaseSolver &&solver); + + void on_activation() { + sanity_checks_periodicity(); + sanity_checks_cell_structure(); + sanity_checks_charge_neutrality(); + sanity_checks_dielectric_contrasts(); + /* Most ELC parameters do not depend on the P3M parameters, + * but the P3M parameters depend on the ELC parameters during tuning, + * therefore ELC needs to be tuned before P3M. */ + recalc_box_h(); + recalc_far_cut(); + visit_base_solver([](auto &solver) { solver->on_activation(); }); + /* With dielectric contrasts, the ELC space layer depends + * on the P3M real-space cutoff, and the P3M FFT parameters + * depend on the ELC space layer */ + if (elc.dielectric_contrast_on) { + recalc_space_layer(); + visit_base_solver([](auto &actor) { actor->init(); }); + } + } + /** @brief Recalculate all box-length-dependent parameters. */ + void on_boxl_change() { + visit_base_solver([](auto &actor) { actor->on_boxl_change(); }); + recalc_box_h(); + recalc_far_cut(); + recalc_space_layer(); + } + void on_node_grid_change() const { + visit_base_solver([](auto &solver) { solver->on_node_grid_change(); }); + } + void on_periodicity_change() const { + sanity_checks_periodicity(); + visit_base_solver([](auto &solver) { solver->on_periodicity_change(); }); + } + void on_cell_structure_change() { + sanity_checks_cell_structure(); + visit_base_solver([](auto &solver) { solver->on_cell_structure_change(); }); + recalc_box_h(); + recalc_far_cut(); + if (elc.dielectric_contrast_on) { + recalc_space_layer(); + visit_base_solver([](auto &actor) { actor->init(); }); + } + } + /** @brief Recalculate all derived parameters. */ + void init() { + visit_base_solver([](auto &actor) { actor->init(); }); + recalc_box_h(); + recalc_far_cut(); + recalc_space_layer(); + } + + void sanity_checks() const { + sanity_checks_periodicity(); + sanity_checks_cell_structure(); + sanity_checks_charge_neutrality(); + sanity_checks_dielectric_contrasts(); + visit_base_solver([](auto &actor) { actor->sanity_checks(); }); + } + + /** + * @brief Veto real-space cutoff values that are incompatible with ELC. + * When ELC is used with dielectric contrasts, the short-range cutoff needs + * to be smaller than the gap size to allow placement of the image charges. + */ + boost::optional veto_r_cut(double r_cut) const { + if (elc.dielectric_contrast_on and r_cut >= elc.gap_size) { + return {std::string("conflict with ELC w/ dielectric contrasts")}; + } + return {}; + } + + /** @brief Calculate short-range pair energy correction. */ + double pair_energy_correction(double q1q2, Particle const &p1, + Particle const &p2) const { + double energy = 0.; + if (elc.dielectric_contrast_on) { + energy = boost::apply_visitor( + [this, &p1, &p2, q1q2](auto &p3m_ptr) { + auto const &pos1 = p1.pos(); + auto const &pos2 = p2.pos(); + auto const &p3m = *p3m_ptr; + auto energy = 0.; + elc.dielectric_layers_contribution( + p3m, pos1, pos2, q1q2, + [&](double q_eff, Utils::Vector3d const &d) { + energy += p3m.pair_energy(q_eff, d.norm()); + }); + elc.dielectric_layers_contribution( + p3m, pos2, pos1, q1q2, + [&](double q_eff, Utils::Vector3d const &d) { + energy += p3m.pair_energy(q_eff, d.norm()); + }); + return energy / 2.; + }, + base_solver); + } + return energy; + } + + /** @brief Add short-range pair force corrections. */ + void add_pair_force_corrections(Particle &p1, Particle &p2, + double q1q2) const { + if (elc.dielectric_contrast_on) { + boost::apply_visitor( + [this, &p1, &p2, q1q2](auto &p3m_ptr) { + auto const &pos1 = p1.pos(); + auto const &pos2 = p2.pos(); + auto const &p3m = *p3m_ptr; + elc.dielectric_layers_contribution( + p3m, pos1, pos2, q1q2, + [&](double q_eff, Utils::Vector3d const &d) { + p1.force() += p3m.pair_force(q_eff, d, d.norm()); + }); + elc.dielectric_layers_contribution( + p3m, pos2, pos1, q1q2, + [&](double q_eff, Utils::Vector3d const &d) { + p2.force() += p3m.pair_force(q_eff, d, d.norm()); + }); + }, + base_solver); + } + } + + double long_range_energy(ParticleRange const &particles) const; + void add_long_range_forces(ParticleRange const &particles) const; + +private: + /** Check if a charged particle is in the gap region. */ + void check_gap(Particle const &p) const; + double tune_far_cut() const; + void adapt_solver(); + /** pairwise contributions from the lowest and top layers to the energy */ + double dipole_energy(ParticleRange const &particles) const; + void add_dipole_force(ParticleRange const &particles) const; + double z_energy(ParticleRange const &particles) const; + void add_z_force(ParticleRange const &particles) const; + + void recalc_box_h(); + void recalc_far_cut() { + if (elc.far_calculated) { + elc.far_cut = tune_far_cut(); + } + elc.far_cut2 = Utils::sqr(elc.far_cut); + } + void recalc_space_layer(); + + void sanity_checks_cell_structure() const {} + void sanity_checks_periodicity() const; + void sanity_checks_dielectric_contrasts() const; + + /// the force calculation + void add_force(ParticleRange const &particles) const; + /// the energy calculation + double calc_energy(ParticleRange const &particles) const; + + template void visit_base_solver(Visitor &&visitor) const { + boost::apply_visitor(visitor, base_solver); + } +}; + +#endif // P3M + +#endif diff --git a/src/core/electrostatics/icc.cpp b/src/core/electrostatics/icc.cpp new file mode 100644 index 00000000000..09e64f15cfb --- /dev/null +++ b/src/core/electrostatics/icc.cpp @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** \file + * Functions to compute the electric field acting on the induced charges, + * excluding forces other than the electrostatic ones. Detailed information + * about the ICC* method is included in the corresponding header file + * \ref icc.hpp. + */ + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "icc.hpp" + +#include "Particle.hpp" +#include "ParticleRange.hpp" +#include "actor/visitors.hpp" +#include "cell_system/CellStructure.hpp" +#include "cells.hpp" +#include "communication.hpp" +#include "electrostatics/coulomb.hpp" +#include "electrostatics/coulomb_inline.hpp" +#include "errorhandling.hpp" +#include "event.hpp" +#include "integrate.hpp" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +/** Calculate the electrostatic forces between source charges (= real charges) + * and wall charges. For each electrostatic method, the proper functions + * for short- and long-range parts are called. Long-range parts are calculated + * directly, short-range parts need helper functions according to the particle + * data organisation. This is a modified version of \ref force_calc. + */ +static void force_calc_icc( + CellStructure &cell_structure, ParticleRange const &particles, + ParticleRange const &ghost_particles, + Coulomb::ShortRangeForceKernel::result_type const &coulomb_kernel, + Coulomb::ShortRangeForceCorrectionsKernel::result_type const &elc_kernel) { + // reset forces + for (auto &p : particles) { + p.force() = {}; + } + for (auto &p : ghost_particles) { + p.force() = {}; + } + + // calc ICC forces + cell_structure.non_bonded_loop( + [coulomb_kernel_ptr = coulomb_kernel.get_ptr(), + elc_kernel_ptr = elc_kernel.get_ptr()](Particle &p1, Particle &p2, + Distance const &d) { + auto const q1q2 = p1.q() * p2.q(); + if (q1q2 != 0.) { + auto force = (*coulomb_kernel_ptr)(q1q2, d.vec21, std::sqrt(d.dist2)); + p1.force() += force; + p2.force() -= force; +#ifdef P3M + if (elc_kernel_ptr) { + (*elc_kernel_ptr)(p1, p2, q1q2); + } +#endif // P3M + } + }); + + Coulomb::calc_long_range_force(particles); +} + +void ICCStar::iteration(CellStructure &cell_structure, + ParticleRange const &particles, + ParticleRange const &ghost_particles) { + + try { + sanity_check(); + } catch (std::runtime_error const &err) { + runtimeErrorMsg() << err.what(); + return; + } + + auto const prefactor = + boost::apply_visitor(GetCoulombPrefactor{}, *electrostatics_actor); + auto const pref = 1. / (prefactor * 2. * Utils::pi()); + auto const kernel = Coulomb::pair_force_kernel(); + auto const elc_kernel = Coulomb::pair_force_elc_kernel(); + icc_cfg.citeration = 0; + + auto global_max_rel_diff = 0.; + + for (int j = 0; j < icc_cfg.max_iterations; j++) { + auto charge_density_max = 0.; + + // calculate electrostatic forces (SR+LR) excluding self-interactions + force_calc_icc(cell_structure, particles, ghost_particles, kernel, + elc_kernel); + cell_structure.ghosts_reduce_forces(); + + auto max_rel_diff = 0.; + + for (auto &p : particles) { + auto const pid = p.id(); + if (pid >= icc_cfg.first_id and pid < icc_cfg.n_icc + icc_cfg.first_id) { + auto const id = p.identity() - icc_cfg.first_id; + /* the dielectric-related prefactor: */ + auto const eps_in = icc_cfg.epsilons[id]; + auto const eps_out = icc_cfg.eps_out; + auto const del_eps = (eps_in - eps_out) / (eps_in + eps_out); + /* calculate the electric field at the certain position */ + auto const local_e_field = p.force() / p.q() + icc_cfg.ext_field; + + if (local_e_field.norm2() == 0.) { + runtimeErrorMsg() + << "ICC found zero electric field on a charge. This must " + "never happen"; + } + + auto const charge_density_old = p.q() / icc_cfg.areas[id]; + + charge_density_max = + std::max(charge_density_max, std::abs(charge_density_old)); + + auto const charge_density_update = + del_eps * pref * (local_e_field * icc_cfg.normals[id]) + + 2. * icc_cfg.eps_out / (icc_cfg.eps_out + icc_cfg.epsilons[id]) * + icc_cfg.sigmas[id]; + /* relative variation: never use an estimator which can be negative + * here */ + auto const charge_density_new = + (1. - icc_cfg.relaxation) * charge_density_old + + (icc_cfg.relaxation) * charge_density_update; + + /* Take the largest error to check for convergence */ + auto const relative_difference = + std::abs((charge_density_new - charge_density_old) / + (charge_density_max + + std::abs(charge_density_new + charge_density_old))); + + max_rel_diff = std::max(max_rel_diff, relative_difference); + + p.q() = charge_density_new * icc_cfg.areas[id]; + + /* check if the charge now is more than 1e6, to determine if ICC still + * leads to reasonable results. This is kind of an arbitrary measure + * but does a good job of spotting divergence! */ + if (std::abs(p.q()) > 1e6) { + runtimeErrorMsg() + << "Particle with id " << p.id() << " has a charge (q=" << p.q() + << ") that is too large for the ICC algorithm"; + + max_rel_diff = std::numeric_limits::infinity(); + break; + } + } + } + + /* Update charges on ghosts. */ + cell_structure.ghosts_update(Cells::DATA_PART_PROPERTIES); + + icc_cfg.citeration++; + + boost::mpi::all_reduce(comm_cart, max_rel_diff, global_max_rel_diff, + boost::mpi::maximum()); + + if (global_max_rel_diff < icc_cfg.convergence) + break; + } + + if (global_max_rel_diff > icc_cfg.convergence) { + runtimeErrorMsg() + << "ICC failed to converge in the given number of maximal steps."; + } + + on_particle_charge_change(); +} + +void icc_data::sanity_checks() const { + if (convergence <= 0.) + throw std::domain_error("Parameter 'convergence' must be > 0"); + if (relaxation < 0. or relaxation > 2.) + throw std::domain_error("Parameter 'relaxation' must be >= 0 and <= 2"); + if (max_iterations <= 0) + throw std::domain_error("Parameter 'max_iterations' must be > 0"); + if (first_id < 0) + throw std::domain_error("Parameter 'first_id' must be >= 0"); + if (eps_out <= 0.) + throw std::domain_error("Parameter 'eps_out' must be > 0"); + + assert(n_icc >= 1); + assert(areas.size() == n_icc); + assert(epsilons.size() == n_icc); + assert(sigmas.size() == n_icc); + assert(normals.size() == n_icc); +} + +ICCStar::ICCStar(icc_data data) { + data.sanity_checks(); + icc_cfg = std::move(data); +} + +void ICCStar::on_activation() const { + sanity_check(); + on_particle_charge_change(); +} + +struct SanityChecksICC : public boost::static_visitor { + template + void operator()(std::shared_ptr const &actor) const {} +#ifdef P3M +#ifdef CUDA + void operator()(std::shared_ptr const &actor) const { + throw std::runtime_error("ICC does not work with P3MGPU"); + } +#endif // CUDA + void + operator()(std::shared_ptr const &actor) const { + if (actor->elc.dielectric_contrast_on) { + throw std::runtime_error("ICC conflicts with ELC dielectric contrast"); + } + boost::apply_visitor(*this, actor->base_solver); + } +#endif // P3M + void operator()(std::shared_ptr const &) const { + throw std::runtime_error("ICC does not work with DebyeHueckel."); + } + void operator()(std::shared_ptr const &) const { + throw std::runtime_error("ICC does not work with ReactionField."); + } +}; + +void ICCStar::sanity_check() const { + sanity_checks_active_solver(); +#ifdef NPT + if (integ_switch == INTEG_METHOD_NPT_ISO) { + throw std::runtime_error("ICC does not work in the NPT ensemble"); + } +#endif +} + +void ICCStar::sanity_checks_active_solver() const { + if (electrostatics_actor) { + boost::apply_visitor(SanityChecksICC{}, *electrostatics_actor); + } else { + throw std::runtime_error("An electrostatics solver is needed by ICC"); + } +} + +void update_icc_particles() { + if (electrostatics_extension) { + if (auto icc = boost::get>( + electrostatics_extension.get_ptr())) { + (**icc).iteration(cell_structure, cell_structure.local_particles(), + cell_structure.ghost_particles()); + } + } +} + +#endif // ELECTROSTATICS diff --git a/src/core/electrostatics/icc.hpp b/src/core/electrostatics/icc.hpp new file mode 100644 index 00000000000..4b004ae4107 --- /dev/null +++ b/src/core/electrostatics/icc.hpp @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_ICC_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_ICC_HPP + +/** + * @file + * + * ICC is a method that allows to take into account the influence + * of arbitrarily shaped dielectric interfaces. The dielectric + * properties of a dielectric medium in the bulk of the simulation + * box are taken into account by reproducing the jump in the electric + * field at the interface with charge surface segments. The charge + * density of the surface segments have to be determined + * self-consistently using an iterative scheme. It can at present + * be used with P3M, ELCP3M and MMM1D. For details see: @cite tyagi10a + * + * To set up ICC, first the dielectric boundary has to be modeled + * by ESPResSo particles n_0...n_0+n where n_0 and n have to be passed + * as a parameter to ICC. + * + * For the determination of the induced charges, only the forces acting + * on the induced charges have to be determined. As P3M and the other + * Coulomb solvers calculate all mutual forces, the force calculation + * was modified to avoid the calculation of the short-range part + * of the source-source force calculation. For different particle + * data organisation schemes, this is performed differently. + */ + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "ParticleRange.hpp" +#include "cell_system/CellStructure.hpp" + +#include + +#include + +/** ICC data structure */ +struct icc_data { + /** First id of ICC particle */ + int n_icc; + /** maximum number of iterations */ + int max_iterations; + /** bulk dielectric constant */ + double eps_out; + /** areas of the particles */ + std::vector areas; + /** dielectric constants of the particles */ + std::vector epsilons; + /** surface charge density of the particles */ + std::vector sigmas; + /** convergence criteria */ + double convergence; + /** surface normal vectors */ + std::vector normals; + /** external electric field */ + Utils::Vector3d ext_field; + /** relaxation parameter */ + double relaxation; + /** last number of iterations */ + int citeration; + /** first ICC particle id */ + int first_id; + + void sanity_checks() const; +}; + +struct ICCStar { + /** ICC parameters */ + icc_data icc_cfg; + + ICCStar(icc_data data); + + /** + * The main iterative scheme, where the surface element charges are calculated + * self-consistently. + */ + void iteration(CellStructure &cell_structure, ParticleRange const &particles, + ParticleRange const &ghost_particles); + + void on_activation() const; + void sanity_checks_active_solver() const; + void sanity_check() const; +}; + +void update_icc_particles(); + +#endif // ELECTROSTATICS +#endif diff --git a/src/core/electrostatics_magnetostatics/mmm-common.hpp b/src/core/electrostatics/mmm-common.hpp similarity index 92% rename from src/core/electrostatics_magnetostatics/mmm-common.hpp rename to src/core/electrostatics/mmm-common.hpp index f25f76a7a6d..2158f7a1679 100644 --- a/src/core/electrostatics_magnetostatics/mmm-common.hpp +++ b/src/core/electrostatics/mmm-common.hpp @@ -18,7 +18,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file + +/** @file * Common parts of the MMM family of methods for the electrostatic * interaction: MMM1D and ELC. This file contains the code for the * polygamma expansions used for the near formulas of MMM1D. @@ -27,8 +28,8 @@ * directly from @cite abramowitz65a. For details, see @cite arnold02a. */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_MMM_COMMON_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_MMM_COMMON_HPP +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM_COMMON_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM_COMMON_HPP #include "mmm-modpsi.hpp" diff --git a/src/core/electrostatics_magnetostatics/mmm-modpsi.cpp b/src/core/electrostatics/mmm-modpsi.cpp similarity index 100% rename from src/core/electrostatics_magnetostatics/mmm-modpsi.cpp rename to src/core/electrostatics/mmm-modpsi.cpp diff --git a/src/core/electrostatics_magnetostatics/mmm-modpsi.hpp b/src/core/electrostatics/mmm-modpsi.hpp similarity index 91% rename from src/core/electrostatics_magnetostatics/mmm-modpsi.hpp rename to src/core/electrostatics/mmm-modpsi.hpp index 91ce3d4245b..8a76748f9ce 100644 --- a/src/core/electrostatics_magnetostatics/mmm-modpsi.hpp +++ b/src/core/electrostatics/mmm-modpsi.hpp @@ -18,14 +18,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file + +/** @file * Common parts of the MMM family of methods for the electrostatic * interaction: MMM1D and ELC. This file contains the code for the * CPU and GPU implementations. */ -#ifndef MMM_MODPSI_HPP -#define MMM_MODPSI_HPP +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM_MODPSI_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM_MODPSI_HPP #include diff --git a/src/core/electrostatics_magnetostatics/mmm1d.cpp b/src/core/electrostatics/mmm1d.cpp similarity index 56% rename from src/core/electrostatics_magnetostatics/mmm1d.cpp rename to src/core/electrostatics/mmm1d.cpp index 5b84b9703d1..9d252c789c7 100644 --- a/src/core/electrostatics_magnetostatics/mmm1d.cpp +++ b/src/core/electrostatics/mmm1d.cpp @@ -18,25 +18,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file - * MMM1D algorithm for long range %Coulomb interaction. - * - * For more information about MMM1D, see \ref mmm1d.hpp "mmm1d.hpp". - */ #include "config.hpp" #ifdef ELECTROSTATICS -#include "electrostatics_magnetostatics/mmm1d.hpp" +#include "electrostatics/mmm1d.hpp" -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/mmm-common.hpp" -#include "electrostatics_magnetostatics/mmm-modpsi.hpp" +#include "electrostatics/coulomb.hpp" +#include "electrostatics/mmm-common.hpp" +#include "electrostatics/mmm-modpsi.hpp" +#include "Particle.hpp" #include "cell_system/CellStructureType.hpp" #include "errorhandling.hpp" +#include "event.hpp" #include "grid.hpp" #include "specfunc.hpp" #include "tuning.hpp" @@ -46,18 +42,12 @@ #include #include -#include #include #include #include #include #include -/** Largest numerically stable cutoff for Bessel function. - * Don't change without improving the formulas. - */ -constexpr int MAXIMAL_B_CUT = 30; - /* if you define this feature, the Bessel functions are calculated up * to machine precision, otherwise 10^-14, which should be * definitely enough for daily life. */ @@ -66,41 +56,30 @@ constexpr int MAXIMAL_B_CUT = 30; #define K1 LPK1 #endif -/** @name inverse box dimensions and other constants */ -/**@{*/ -static double uz2, prefuz2, prefL3_i; -/**@}*/ - -MMM1DParameters mmm1d_params = {0.05, 1e-5, 0}; -/** From which distance a certain Bessel cutoff is valid. - * Can't be part of the params since these get broadcasted. - */ -static std::array bessel_radii; - static double far_error(int P, double minrad) { - auto const wavenumber = 2 * Utils::pi() * box_geo.length_inv()[2]; + auto const wavenumber = 2. * Utils::pi() * box_geo.length_inv()[2]; // this uses an upper bound to all force components and the potential auto const rhores = wavenumber * minrad; - auto const pref = 4 * box_geo.length_inv()[2] * std::max(1.0, wavenumber); + auto const pref = 4. * box_geo.length_inv()[2] * std::max(1., wavenumber); - return pref * K1(rhores * P) * exp(rhores) / rhores * (P - 1 + 1 / rhores); + return pref * K1(rhores * P) * exp(rhores) / rhores * (P - 1. + 1. / rhores); } static double determine_minrad(double maxPWerror, int P) { // bisection to search for where the error is maxPWerror - constexpr auto min_rad = 0.01; - double const rgranularity = min_rad * box_geo.length()[2]; - double rmin = rgranularity; - double rmax = std::min(box_geo.length()[0], box_geo.length()[1]); - double const errmin = far_error(P, rmin); - double const errmax = far_error(P, rmax); + auto constexpr min_rad = 0.01; + auto const rgranularity = min_rad * box_geo.length()[2]; + auto rmin = rgranularity; + auto rmax = std::min(box_geo.length()[0], box_geo.length()[1]); + auto const errmin = far_error(P, rmin); + auto const errmax = far_error(P, rmax); if (errmin < maxPWerror) { // we can do almost all radii with this P return rmin; } if (errmax > maxPWerror) { // make sure that this switching radius cannot be reached - return 2 * std::max(box_geo.length()[0], box_geo.length()[1]); + return 2. * std::max(box_geo.length()[0], box_geo.length()[1]); } while (rmax - rmin > rgranularity) { @@ -115,16 +94,16 @@ static double determine_minrad(double maxPWerror, int P) { return 0.5 * (rmin + rmax); } -static void determine_bessel_radii(double maxPWerror) { +void CoulombMMM1D::determine_bessel_radii() { for (int i = 0; i < MAXIMAL_B_CUT; ++i) { bessel_radii[i] = determine_minrad(maxPWerror, i + 1); } } -static void prepare_polygamma_series(double maxPWerror, double maxrad2) { +void CoulombMMM1D::prepare_polygamma_series() { /* polygamma, determine order */ double err; - auto const rhomax2 = uz2 * maxrad2; + auto const rhomax2 = uz2 * far_switch_radius_sq; /* rhomax2 < 1, so rhomax2m2 falls monotonously */ int n = 1; auto rhomax2nm2 = 1.0; @@ -132,65 +111,63 @@ static void prepare_polygamma_series(double maxPWerror, double maxrad2) { create_mod_psi_up_to(n + 1); /* |uz*z| <= 0.5 */ - err = 2 * n * fabs(mod_psi_even(n, 0.5)) * rhomax2nm2; + err = 2. * static_cast(n) * fabs(mod_psi_even(n, 0.5)) * rhomax2nm2; rhomax2nm2 *= rhomax2; n++; } while (err > 0.1 * maxPWerror); } -void MMM1D_set_params(double switch_rad, double maxPWerror) { - mmm1d_params.far_switch_radius_2 = - (switch_rad > 0) ? Utils::sqr(switch_rad) : -1; - mmm1d_params.maxPWerror = maxPWerror; - coulomb.method = COULOMB_MMM1D; - - mpi_bcast_coulomb_params(); +CoulombMMM1D::CoulombMMM1D(double prefactor, double maxPWerror, + double switch_rad, int tune_timings, + bool tune_verbose) + : maxPWerror{maxPWerror}, far_switch_radius{switch_rad}, + tune_timings{tune_timings}, tune_verbose{tune_verbose}, m_is_tuned{false}, + far_switch_radius_sq{-1.}, uz2{0.}, prefuz2{0.}, prefL3_i{0.} { + if (far_switch_radius > 0.) { + far_switch_radius_sq = Utils::sqr(far_switch_radius); + } + set_prefactor(prefactor); } -bool MMM1D_sanity_checks() { +void CoulombMMM1D::sanity_checks_periodicity() const { if (box_geo.periodic(0) || box_geo.periodic(1) || !box_geo.periodic(2)) { - runtimeErrorMsg() << "MMM1D requires periodicity (0, 0, 1)"; - return true; + throw std::runtime_error("MMM1D requires periodicity (0, 0, 1)"); } +} + +void CoulombMMM1D::sanity_checks_cell_structure() const { if (local_geo.cell_structure_type() != CellStructureType::CELL_STRUCTURE_NSQUARE) { - runtimeErrorMsg() << "MMM1D requires the N-square cellsystem"; - return true; + throw std::runtime_error("MMM1D requires the N-square cellsystem"); } - return false; } -int MMM1D_init() { - if (MMM1D_sanity_checks()) - return ES_ERROR; - - if (mmm1d_params.far_switch_radius_2 >= Utils::sqr(box_geo.length()[2])) - mmm1d_params.far_switch_radius_2 = 0.8 * Utils::sqr(box_geo.length()[2]); +void CoulombMMM1D::recalc_boxl_parameters() { + if (far_switch_radius_sq >= Utils::sqr(box_geo.length()[2])) + far_switch_radius_sq = 0.8 * Utils::sqr(box_geo.length()[2]); uz2 = Utils::sqr(box_geo.length_inv()[2]); - prefuz2 = coulomb.prefactor * uz2; + prefuz2 = prefactor * uz2; prefL3_i = prefuz2 * box_geo.length_inv()[2]; - determine_bessel_radii(mmm1d_params.maxPWerror); - prepare_polygamma_series(mmm1d_params.maxPWerror, - mmm1d_params.far_switch_radius_2); - return ES_OK; + determine_bessel_radii(); + prepare_polygamma_series(); } -void add_mmm1d_coulomb_pair_force(double chpref, Utils::Vector3d const &d, - double r, Utils::Vector3d &force) { - constexpr auto c_2pi = 2 * Utils::pi(); - auto const n_modPsi = static_cast(modPsi.size() >> 1); +Utils::Vector3d CoulombMMM1D::pair_force(double q1q2, Utils::Vector3d const &d, + double dist) const { + auto constexpr c_2pi = 2. * Utils::pi(); + auto const n_modPsi = static_cast(modPsi.size()) >> 1; auto const rxy2 = d[0] * d[0] + d[1] * d[1]; auto const rxy2_d = rxy2 * uz2; auto const z_d = d[2] * box_geo.length_inv()[2]; - Utils::Vector3d F; + Utils::Vector3d force; - if (rxy2 <= mmm1d_params.far_switch_radius_2) { + if (rxy2 <= far_switch_radius_sq) { /* polygamma summation */ auto sr = 0.; auto sz = mod_psi_odd(0, z_d); - auto r2nm1 = 1.0; + auto r2nm1 = 1.; for (int n = 1; n < n_modPsi; n++) { auto const deriv = static_cast(2 * n); auto const mpe = mod_psi_even(n, z_d); @@ -200,7 +177,7 @@ void add_mmm1d_coulomb_pair_force(double chpref, Utils::Vector3d const &d, sz += r2n * mpo; sr += deriv * r2nm1 * mpe; - if (fabs(deriv * r2nm1 * mpe) < mmm1d_params.maxPWerror) + if (fabs(deriv * r2nm1 * mpe) < maxPWerror) break; r2nm1 = r2n; @@ -214,7 +191,7 @@ void add_mmm1d_coulomb_pair_force(double chpref, Utils::Vector3d const &d, double pref, rt, rt2, shift_z; - pref = 1. / (r * r * r); + pref = 1. / Utils::int_pow<3>(dist); Fx += pref * d[0]; Fy += pref * d[1]; Fz += pref * d[2]; @@ -235,7 +212,7 @@ void add_mmm1d_coulomb_pair_force(double chpref, Utils::Vector3d const &d, Fy += pref * d[1]; Fz += pref * shift_z; - F = {Fx, Fy, Fz}; + force = {Fx, Fy, Fz}; } else { /* far range formula */ auto const rxy = sqrt(rxy2); @@ -257,90 +234,91 @@ void add_mmm1d_coulomb_pair_force(double chpref, Utils::Vector3d const &d, sr += bp * k1 * cos(fq * z_d); sz += bp * k0 * sin(fq * z_d); } - sr *= uz2 * 4 * c_2pi; - sz *= uz2 * 4 * c_2pi; + sr *= uz2 * 4. * c_2pi; + sz *= uz2 * 4. * c_2pi; - auto const pref = sr / rxy + 2 * box_geo.length_inv()[2] / rxy2; + auto const pref = sr / rxy + 2. * box_geo.length_inv()[2] / rxy2; - F = {pref * d[0], pref * d[1], sz}; + force = {pref * d[0], pref * d[1], sz}; } - force += chpref * F; + return (prefactor * q1q2) * force; } -double mmm1d_coulomb_pair_energy(double const chpref, Utils::Vector3d const &d, - double r) { - if (chpref == 0) +double CoulombMMM1D::pair_energy(double const q1q2, Utils::Vector3d const &d, + double const dist) const { + if (q1q2 == 0.) return 0.; - constexpr auto c_2pi = 2 * Utils::pi(); - auto const n_modPsi = static_cast(modPsi.size() >> 1); + auto constexpr c_2pi = 2. * Utils::pi(); + auto const n_modPsi = static_cast(modPsi.size()) >> 1; auto const rxy2 = d[0] * d[0] + d[1] * d[1]; auto const rxy2_d = rxy2 * uz2; auto const z_d = d[2] * box_geo.length_inv()[2]; - double E; + double energy; - if (rxy2 <= mmm1d_params.far_switch_radius_2) { + if (rxy2 <= far_switch_radius_sq) { /* near range formula */ - E = -2 * Utils::gamma(); + energy = -2. * Utils::gamma(); /* polygamma summation */ double r2n = 1.0; for (int n = 0; n < n_modPsi; n++) { auto const add = mod_psi_even(n, z_d) * r2n; - E -= add; + energy -= add; - if (fabs(add) < mmm1d_params.maxPWerror) + if (fabs(add) < maxPWerror) break; r2n *= rxy2_d; } - E *= box_geo.length_inv()[2]; + energy *= box_geo.length_inv()[2]; /* real space parts */ double rt, shift_z; - E += 1. / r; + energy += 1. / dist; shift_z = d[2] + box_geo.length()[2]; rt = sqrt(rxy2 + shift_z * shift_z); - E += 1. / rt; + energy += 1. / rt; shift_z = d[2] - box_geo.length()[2]; rt = sqrt(rxy2 + shift_z * shift_z); - E += 1. / rt; + energy += 1. / rt; } else { /* far range formula */ auto const rxy = sqrt(rxy2); auto const rxy_d = rxy * box_geo.length_inv()[2]; /* The first Bessel term will compensate a little bit the log term, so add them close together */ - E = -0.25 * log(rxy2_d) + 0.5 * (Utils::ln_2() - Utils::gamma()); + energy = -0.25 * log(rxy2_d) + 0.5 * (Utils::ln_2() - Utils::gamma()); for (int bp = 1; bp < MAXIMAL_B_CUT; bp++) { if (bessel_radii[bp - 1] < rxy) break; auto const fq = c_2pi * bp; - E += K0(fq * rxy_d) * cos(fq * z_d); + energy += K0(fq * rxy_d) * cos(fq * z_d); } - E *= 4 * box_geo.length_inv()[2]; + energy *= 4. * box_geo.length_inv()[2]; } - return chpref * E; + return prefactor * q1q2 * energy; } -int mmm1d_tune(int timings, bool verbose) { - if (MMM1D_sanity_checks()) - return ES_ERROR; +void CoulombMMM1D::tune() { + if (is_tuned()) { + return; + } + recalc_boxl_parameters(); - if (mmm1d_params.far_switch_radius_2 < 0) { + if (far_switch_radius_sq < 0.) { auto const maxrad = box_geo.length()[2]; auto min_time = std::numeric_limits::infinity(); auto min_rad = -1.; double switch_radius; - /* determine besselcutoff and optimal switching radius. Should be around - * 0.33 */ + /* determine optimal switching radius. Should be around 0.33 */ for (switch_radius = 0.2 * maxrad; switch_radius < 0.4 * maxrad; switch_radius += 0.025 * maxrad) { if (switch_radius <= bessel_radii.back()) { @@ -348,46 +326,33 @@ int mmm1d_tune(int timings, bool verbose) { continue; } - mmm1d_params.far_switch_radius_2 = Utils::sqr(switch_radius); - - coulomb.method = COULOMB_MMM1D; - - /* initialize mmm1d temporary structures */ - mpi_bcast_coulomb_params(); + far_switch_radius_sq = Utils::sqr(switch_radius); + on_coulomb_change(); /* perform force calculation test */ - double int_time = time_force_calc(timings); - - /* exit on errors */ - if (int_time < 0) - return ES_ERROR; + auto const int_time = benchmark_integration_step(tune_timings); - if (verbose) { + if (tune_verbose) { std::printf("r= %f t= %f ms\n", switch_radius, int_time); } if (int_time < min_time) { min_time = int_time; min_rad = switch_radius; - } - /* stop if all hope is vain... */ - else if (int_time > 2 * min_time) + } else if (int_time > 2. * min_time) { + // simple heuristic to skip remaining radii when performance is dropping break; + } } switch_radius = min_rad; - mmm1d_params.far_switch_radius_2 = Utils::sqr(switch_radius); - } else if (mmm1d_params.far_switch_radius_2 <= - Utils::sqr(bessel_radii.back())) { + far_switch_radius_sq = Utils::sqr(switch_radius); + } else if (far_switch_radius_sq <= Utils::sqr(bessel_radii.back())) { // this switching radius is too small for our Bessel series - runtimeErrorMsg() << "could not find reasonable bessel cutoff"; - return ES_ERROR; + throw std::runtime_error("MMM1D could not find a reasonable Bessel cutoff"); } - coulomb.method = COULOMB_MMM1D; - - mpi_bcast_coulomb_params(); - - return ES_OK; + m_is_tuned = true; + on_coulomb_change(); } -#endif +#endif // ELECTROSTATICS diff --git a/src/core/electrostatics/mmm1d.hpp b/src/core/electrostatics/mmm1d.hpp new file mode 100644 index 00000000000..8d4aca77182 --- /dev/null +++ b/src/core/electrostatics/mmm1d.hpp @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file + * MMM1D algorithm for long-range %Coulomb interactions on the CPU. + * Implementation of the MMM1D method for the calculation of the electrostatic + * interaction in one-dimensionally periodic systems. For details on the + * method see MMM in general. The MMM1D method works only with the N-squared + * cell system since neither the near nor far formula can be decomposed. + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM1D_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM1D_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "electrostatics/actor.hpp" + +#include "Particle.hpp" + +#include + +#include + +/** @brief Parameters for the MMM1D electrostatic interaction */ +struct CoulombMMM1D : public Coulomb::Actor { + /** + * @brief Maximal allowed pairwise error for the potential and force. + * This error ignores prefactors, i.e. this is for a pure lattice 1/r-sum. + */ + double maxPWerror; + /** + * @brief Far switch radius. Represents the xy-distance at which + * the calculation switches from the far to the near formula. + */ + double far_switch_radius; + int tune_timings; + bool tune_verbose; + + CoulombMMM1D(double prefactor, double maxPWerror, double switch_rad, + int tune_timings, bool tune_verbose); + + /** Compute the pair force. + * @param[in] q1q2 Product of the charges on p1 and p2. + * @param[in] d Vector pointing from p1 to p2. + * @param[in] dist Distance between p1 and p2. + */ + Utils::Vector3d pair_force(double q1q2, Utils::Vector3d const &d, + double dist) const; + + /** Compute the pair energy. + * @param[in] q1q2 Product of the charges on p1 and p2. + * @param[in] d Vector pointing from p1 to p2. + * @param[in] dist Distance between p1 and p2. + */ + double pair_energy(double q1q2, Utils::Vector3d const &d, double dist) const; + + void tune(); + bool is_tuned() const { return m_is_tuned; } + + void on_activation() { + sanity_checks(); + tune(); + } + /** @brief Recalculate all box-length-dependent parameters. */ + void on_boxl_change() { recalc_boxl_parameters(); } + void on_node_grid_change() const {} + void on_periodicity_change() const { sanity_checks_periodicity(); } + void on_cell_structure_change() { sanity_checks_cell_structure(); } + /** @brief Recalculate all derived parameters. */ + void init() { recalc_boxl_parameters(); } + + void sanity_checks() const { + sanity_checks_periodicity(); + sanity_checks_cell_structure(); + sanity_checks_charge_neutrality(); + } + +private: + bool m_is_tuned; + /** @brief Square of the far switch radius. */ + double far_switch_radius_sq; + /** @brief Squared inverse box length in z-direction. */ + double uz2; + /** @brief Squared inverse box length in z-direction times prefactor. */ + double prefuz2; + /** @brief Cubed inverse box length in z-direction times prefactor. */ + double prefL3_i; + /** + * @brief Largest numerically stable cutoff for Bessel function. + * Don't change without improving the formulas. + */ + static constexpr auto MAXIMAL_B_CUT = 30; + /** @brief From which distance a certain Bessel cutoff is valid. */ + std::array bessel_radii; + + void determine_bessel_radii(); + void prepare_polygamma_series(); + void recalc_boxl_parameters(); + void sanity_checks_periodicity() const; + void sanity_checks_cell_structure() const; +}; + +#endif // ELECTROSTATICS +#endif diff --git a/src/core/actor/Mmm1dgpuForce.cpp b/src/core/electrostatics/mmm1d_gpu.cpp similarity index 50% rename from src/core/actor/Mmm1dgpuForce.cpp rename to src/core/electrostatics/mmm1d_gpu.cpp index 5c6183ba295..a104d1ca53f 100644 --- a/src/core/actor/Mmm1dgpuForce.cpp +++ b/src/core/electrostatics/mmm1d_gpu.cpp @@ -16,53 +16,62 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "config.hpp" #ifdef MMM1D_GPU -#include "actor/Mmm1dgpuForce.hpp" - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" +#include "electrostatics/mmm1d_gpu.hpp" +#include "EspressoSystemInterface.hpp" #include "cell_system/CellStructureType.hpp" -#include "energy.hpp" -#include "forces.hpp" +#include "communication.hpp" +#include "event.hpp" #include "grid.hpp" #include -Mmm1dgpuForce::Mmm1dgpuForce(SystemInterface &s) { - s.requestFGpu(); - s.requestRGpu(); - s.requestQGpu(); +CoulombMMM1DGpu::CoulombMMM1DGpu(double prefactor, double maxPWerror, + double far_switch_radius, int bessel_cutoff) + : maxPWerror{maxPWerror}, far_switch_radius{far_switch_radius}, + far_switch_radius_sq{-1.}, bessel_cutoff{bessel_cutoff}, m_is_tuned{ + false} { + + set_prefactor(prefactor); + if (far_switch_radius >= 0. and far_switch_radius > box_geo.length()[2]) { + throw std::domain_error( + "switching radius must not be larger than box length"); + } + + if (this_node == 0) { + auto &system = EspressoSystemInterface::Instance(); + system.requestFGpu(); + system.requestRGpu(); + system.requestQGpu(); + modpsi_init(); + } +} - // system sanity checks +void CoulombMMM1DGpu::sanity_checks_periodicity() const { if (box_geo.periodic(0) || box_geo.periodic(1) || !box_geo.periodic(2)) { throw std::runtime_error("MMM1D requires periodicity (0, 0, 1)"); } +} + +void CoulombMMM1DGpu::sanity_checks_cell_structure() const { if (local_geo.cell_structure_type() != CellStructureType::CELL_STRUCTURE_NSQUARE) { throw std::runtime_error("MMM1D requires the N-square cellsystem"); } - - modpsi_init(); } -void Mmm1dgpuForce::activate() { - coulomb.method = COULOMB_MMM1D_GPU; - mpi_bcast_coulomb_params(); - - forceActors.push_back(this); - energyActors.push_back(this); -} - -void Mmm1dgpuForce::deactivate() { - coulomb.method = COULOMB_NONE; - mpi_bcast_coulomb_params(); - - forceActors.remove(this); - energyActors.remove(this); +void CoulombMMM1DGpu::tune() { + EspressoSystemInterface::Instance().init(); + if (this_node == 0) { + setup(); + tune(maxPWerror, far_switch_radius, bessel_cutoff); + } + m_is_tuned = true; } #endif diff --git a/src/core/electrostatics/mmm1d_gpu.hpp b/src/core/electrostatics/mmm1d_gpu.hpp new file mode 100644 index 00000000000..56d27edf82e --- /dev/null +++ b/src/core/electrostatics/mmm1d_gpu.hpp @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file + * MMM1D algorithm for long-range %Coulomb interactions on the GPU. + * Implementation of the MMM1D method for the calculation of the electrostatic + * interaction in one-dimensionally periodic systems. For details on the + * method see MMM in general. The MMM1D method works only with the N-squared + * cell system since neither the near nor far formula can be decomposed. + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM1D_GPU_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_MMM1D_GPU_HPP + +#include "config.hpp" + +#ifdef MMM1D_GPU + +#include "electrostatics/actor.hpp" + +class CoulombMMM1DGpu : public Coulomb::Actor { +public: + double maxPWerror; + double far_switch_radius; + double far_switch_radius_sq; + int bessel_cutoff; + + CoulombMMM1DGpu(double prefactor, double maxPWerror, double far_switch_radius, + int bessel_cutoff); + ~CoulombMMM1DGpu(); + + // interface methods + void add_long_range_forces(); + void add_long_range_energy(); + + void on_activation() { + sanity_checks(); + tune(); + } + void on_boxl_change() { setup(); } + void on_node_grid_change() const {} + void on_periodicity_change() const { sanity_checks_periodicity(); } + void on_cell_structure_change() { sanity_checks_cell_structure(); } + void init() const {} + + void sanity_checks() const { + sanity_checks_periodicity(); + sanity_checks_cell_structure(); + sanity_checks_charge_neutrality(); + } + + void tune(); + bool is_tuned() const { return m_is_tuned; } + +private: + bool m_is_tuned; + + // CUDA parameters + unsigned int numThreads = 64u; + unsigned int numBlocks() const; + + // the box length currently set on the GPU + // Needed to make sure it hasn't been modified after inter coulomb was used. + float host_boxz = 0.f; + // the number of particles we had during the last run. Needed to check if we + // have to realloc dev_forcePairs + unsigned int host_npart = 0u; + + // pairs==-1: un-initialized device memory + // pairs==0: return forces using atomicAdd + // pairs==2: return forces using a global memory reduction + int pairs = -1; + // variables for forces and energies calculated pre-reduction + float *dev_forcePairs = nullptr; + float *dev_energyBlocks = nullptr; + + // run a single force calculation and return the time it takes using + // high-precision CUDA timers + float force_benchmark(); + + void setup(); + void modpsi_init(); + void set_params(double boxz, double prefactor, double maxPWerror, + double far_switch_radius, int bessel_cutoff); + void tune(double maxPWerror, double far_switch_radius, int bessel_cutoff); + void sanity_checks_periodicity() const; + void sanity_checks_cell_structure() const; +}; + +#endif // MMM1D_GPU +#endif diff --git a/src/core/actor/Mmm1dgpuForce_cuda.cu b/src/core/electrostatics/mmm1d_gpu_cuda.cu similarity index 63% rename from src/core/actor/Mmm1dgpuForce_cuda.cu rename to src/core/electrostatics/mmm1d_gpu_cuda.cu index 0755b319686..cb578e9ea17 100644 --- a/src/core/actor/Mmm1dgpuForce_cuda.cu +++ b/src/core/electrostatics/mmm1d_gpu_cuda.cu @@ -16,7 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file + +/** @file * This file contains the code for the polygamma expansions used for the * near formulas of MMM1D on GPU, as well as the force kernels. */ @@ -25,20 +26,18 @@ #ifdef MMM1D_GPU +#include "electrostatics/mmm-modpsi.hpp" +#include "electrostatics/mmm1d_gpu.hpp" +#include "electrostatics/specfunc.cuh" + #include "EspressoSystemInterface.hpp" -#include "actor/Mmm1dgpuForce.hpp" -#include "actor/specfunc_cuda.hpp" #include "cuda_utils.cuh" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/mmm-modpsi.hpp" -#include "electrostatics_magnetostatics/mmm1d.hpp" #include #include #include -#include #include #include #include @@ -55,7 +54,7 @@ constexpr int deviceCount = 1; #undef cudaSetDevice #define cudaSetDevice(d) -__constant__ float far_switch_radius_2[1] = {0.05f * 0.05f}; +__constant__ float far_switch_radius_sq[1] = {0.05f * 0.05f}; __constant__ float boxz[1]; __constant__ float uz[1]; __constant__ float coulomb_prefactor[1] = {1.0f}; @@ -74,6 +73,8 @@ __constant__ unsigned int device_linModPsi_offsets[2 * modpsi_order]; __constant__ unsigned int device_linModPsi_lengths[2 * modpsi_order]; __constant__ float device_linModPsi[modpsi_constant_size]; +static EspressoSystemInterface *es_system = nullptr; + __device__ float dev_mod_psi_even(int n, float x) { return evaluateAsTaylorSeriesAt( &device_linModPsi[device_linModPsi_offsets[2 * n]], @@ -86,7 +87,7 @@ __device__ float dev_mod_psi_odd(int n, float x) { static_cast(device_linModPsi_lengths[2 * n + 1]), x * x); } -void Mmm1dgpuForce::modpsi_init() { +void CoulombMMM1DGpu::modpsi_init() { create_mod_psi_up_to(modpsi_order); // linearized array on host @@ -131,24 +132,26 @@ void Mmm1dgpuForce::modpsi_init() { } } -void Mmm1dgpuForce::setup(SystemInterface &s) { - auto const box_z = static_cast(s.box()[2]); - if (need_tune && s.npart_gpu() > 0) { - set_params(box_z, static_cast(coulomb.prefactor), maxPWerror, - far_switch_radius, bessel_cutoff); - tune(s, maxPWerror, far_switch_radius, bessel_cutoff); +void CoulombMMM1DGpu::setup() { + es_system = &EspressoSystemInterface::Instance(); + auto const box_z = static_cast(es_system->box()[2]); + auto const n_part = es_system->npart_gpu(); + if (not m_is_tuned and n_part != 0) { + set_params(box_z, prefactor, maxPWerror, far_switch_radius, bessel_cutoff); + tune(maxPWerror, far_switch_radius, bessel_cutoff); } if (box_z != host_boxz) { set_params(box_z, 0, -1, -1, -1); } - if (s.npart_gpu() == host_npart) { // unchanged + // skip device memory reallocation if device memory is already + // allocated with the correct vector lengths + if (n_part == host_npart and pairs != -1) { return; } - - // For all but the largest systems, it is faster to store force pairs and then - // sum them up. Atomics are just so slow: so unless we're limited by memory, - // do the latter. - auto const part_mem_size = 3 * Utils::sqr(s.npart_gpu()) * sizeof(float); + // For all but the largest systems, it is faster to store force pairs + // and then sum them up. Atomics are slow, so we only use them when + // we're limited by device memory, do the latter. + auto const part_mem_size = 3ul * Utils::sqr(n_part) * sizeof(float); pairs = 2; for (int d = 0; d < deviceCount; d++) { cudaSetDevice(d); @@ -171,19 +174,19 @@ void Mmm1dgpuForce::setup(SystemInterface &s) { if (dev_energyBlocks) cudaFree(dev_energyBlocks); cuda_safe_mem( - cudaMalloc((void **)&dev_energyBlocks, numBlocks(s) * sizeof(float))); - host_npart = static_cast(s.npart_gpu()); + cudaMalloc((void **)&dev_energyBlocks, numBlocks() * sizeof(float))); + host_npart = static_cast(n_part); } -unsigned int Mmm1dgpuForce::numBlocks(SystemInterface const &s) const { - auto b = 1 + static_cast(Utils::sqr(s.npart_gpu()) / +unsigned int CoulombMMM1DGpu::numBlocks() const { + auto b = 1 + static_cast(Utils::sqr(es_system->npart_gpu()) / static_cast(numThreads)); if (b > 65535) b = 65535; return b; } -Mmm1dgpuForce::~Mmm1dgpuForce() { cudaFree(dev_forcePairs); } +CoulombMMM1DGpu::~CoulombMMM1DGpu() { cudaFree(dev_forcePairs); } __forceinline__ __device__ float sqpow(float x) { return x * x; } __forceinline__ __device__ float cbpow(float x) { return x * x * x; } @@ -239,112 +242,106 @@ __global__ void besselTuneKernel(int *result, float far_switch_radius, result[0] = P; } -void Mmm1dgpuForce::tune(SystemInterface &s, float _maxPWerror, - float _far_switch_radius, int _bessel_cutoff) { - float far_switch_radius = _far_switch_radius; - int bessel_cutoff = _bessel_cutoff; - float maxrad = host_boxz; +void CoulombMMM1DGpu::tune(double maxPWerror, double far_switch_radius, + int bessel_cutoff) { - if (_far_switch_radius < 0 && _bessel_cutoff < 0) { + if (far_switch_radius < 0.0 && bessel_cutoff < 0) { // autodetermine switching radius and Bessel cutoff - float bestrad = 0, besttime = INFINITY; + auto const maxrad = host_boxz; + auto bestrad = 0.0; + float besttime = INFINITY; // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) - for (far_switch_radius = 0.05f * maxrad; far_switch_radius < maxrad; - far_switch_radius += 0.05f * maxrad) { - set_params(0, 0, _maxPWerror, far_switch_radius, bessel_cutoff); - tune(s, _maxPWerror, far_switch_radius, -2); // tune Bessel cutoff - auto const runtime = force_benchmark(s); + for (auto radius = 0.05 * maxrad; radius < maxrad; + radius += 0.05 * maxrad) { + set_params(0, 0, maxPWerror, radius, bessel_cutoff); + tune(maxPWerror, radius, -2); // tune Bessel cutoff + auto const runtime = force_benchmark(); if (runtime < besttime) { besttime = runtime; - bestrad = far_switch_radius; + bestrad = radius; } } - far_switch_radius = bestrad; - - set_params(0, 0, _maxPWerror, far_switch_radius, bessel_cutoff); - tune(s, _maxPWerror, far_switch_radius, -2); // tune Bessel cutoff - } else if (_bessel_cutoff < 0) { + set_params(0, 0, maxPWerror, bestrad, bessel_cutoff); + tune(maxPWerror, bestrad, -2); // tune Bessel cutoff + } else if (bessel_cutoff < 0) { // autodetermine Bessel cutoff + auto const far_switch_radius_f = static_cast(far_switch_radius); int *dev_cutoff; constexpr auto maxCut = 30; cuda_safe_mem(cudaMalloc((void **)&dev_cutoff, sizeof(int))); besselTuneKernel<<>>( - dev_cutoff, far_switch_radius, maxCut); - cuda_safe_mem(cudaMemcpy(&bessel_cutoff, dev_cutoff, sizeof(int), + dev_cutoff, far_switch_radius_f, maxCut); + int best_cutoff = 0; + cuda_safe_mem(cudaMemcpy(&best_cutoff, dev_cutoff, sizeof(int), cudaMemcpyDeviceToHost)); cudaFree(dev_cutoff); - if (_bessel_cutoff != -2 && bessel_cutoff >= maxCut) { + if (bessel_cutoff != -2 && best_cutoff >= maxCut) { // we already had our switching radius and only needed to // determine the cutoff, i.e. this was the final tuning round throw std::runtime_error( "No reasonable Bessel cutoff could be determined."); } - set_params(0, 0, _maxPWerror, far_switch_radius, bessel_cutoff); + set_params(0, 0, maxPWerror, far_switch_radius, best_cutoff); } } -void Mmm1dgpuForce::set_params(float _boxz, float _coulomb_prefactor, - float _maxPWerror, float _far_switch_radius, - int _bessel_cutoff) { - if (_boxz > 0 && _far_switch_radius > _boxz) { +void CoulombMMM1DGpu::set_params(double boxz, double prefactor, + double maxPWerror, double far_switch_radius, + int bessel_cutoff) { + if (boxz > 0.0 && far_switch_radius > boxz) { throw std::runtime_error( "switching radius must not be larger than box length"); } - auto const _far_switch_radius_2 = Utils::sqr(_far_switch_radius); - auto const _uz = 1.0f / _boxz; + for (int d = 0; d < deviceCount; d++) { - // double colons are needed to access the constant memory variables because - // they are file globals and we have identically named class variables cudaSetDevice(d); - if (_far_switch_radius >= 0) { - mmm1d_params.far_switch_radius_2 = _far_switch_radius_2; - cuda_safe_mem(cudaMemcpyToSymbol(::far_switch_radius_2, - &_far_switch_radius_2, sizeof(float))); - far_switch_radius = _far_switch_radius; + if (far_switch_radius >= 0.0) { + this->far_switch_radius = far_switch_radius; + far_switch_radius_sq = Utils::sqr(far_switch_radius); + auto const far_switch_radius_sq_f = + static_cast(far_switch_radius_sq); + cuda_safe_mem(cudaMemcpyToSymbol(::far_switch_radius_sq, + &far_switch_radius_sq_f, sizeof(float))); } - if (_boxz > 0) { - host_boxz = _boxz; - cuda_safe_mem(cudaMemcpyToSymbol(::boxz, &_boxz, sizeof(float))); - cuda_safe_mem(cudaMemcpyToSymbol(::uz, &_uz, sizeof(float))); + if (boxz > 0.0) { + host_boxz = static_cast(boxz); + auto const uz = 1.0f / host_boxz; + cuda_safe_mem(cudaMemcpyToSymbol(::boxz, &host_boxz, sizeof(float))); + cuda_safe_mem(cudaMemcpyToSymbol(::uz, &uz, sizeof(float))); } - if (_coulomb_prefactor != 0) { - cuda_safe_mem(cudaMemcpyToSymbol(::coulomb_prefactor, &_coulomb_prefactor, - sizeof(float))); - coulomb_prefactor = _coulomb_prefactor; + if (prefactor != 0.0) { + this->prefactor = prefactor; + auto const prefactor_f = static_cast(prefactor); + cuda_safe_mem( + cudaMemcpyToSymbol(::coulomb_prefactor, &prefactor_f, sizeof(float))); } - if (_bessel_cutoff > 0) { - mmm1d_params.bessel_cutoff = _bessel_cutoff; + if (bessel_cutoff > 0) { + this->bessel_cutoff = bessel_cutoff; cuda_safe_mem( - cudaMemcpyToSymbol(::bessel_cutoff, &_bessel_cutoff, sizeof(int))); - bessel_cutoff = _bessel_cutoff; + cudaMemcpyToSymbol(::bessel_cutoff, &bessel_cutoff, sizeof(int))); } - if (_maxPWerror > 0) { - mmm1d_params.maxPWerror = _maxPWerror; + if (maxPWerror > 0.0) { + this->maxPWerror = maxPWerror; + auto const maxPWerror_f = static_cast(maxPWerror); cuda_safe_mem( - cudaMemcpyToSymbol(::maxPWerror, &_maxPWerror, sizeof(float))); - maxPWerror = _maxPWerror; + cudaMemcpyToSymbol(::maxPWerror, &maxPWerror_f, sizeof(float))); } } - need_tune = true; - - // The changed parameters in @ref mmm1d_params do not need to be broadcast: - // they are only accessed by the CUDA code which only runs on the head node. - // We couldn't broadcast from here anyway because @ref set_params() is - // called from the @ref computeForces() and @ref tune() functions. + m_is_tuned = false; } __global__ void forcesKernel(const float *__restrict__ r, const float *__restrict__ q, float *__restrict__ force, std::size_t N, - int pairs, std::size_t tStart) { + int pairs) { - constexpr auto c_2pif = 2 * Utils::pi(); + constexpr auto c_2pif = 2.f * Utils::pi(); auto const tStop = Utils::sqr(N); - for (std::size_t tid = threadIdx.x + blockIdx.x * blockDim.x + tStart; - tid < tStop; tid += blockDim.x * gridDim.x) { + for (std::size_t tid = threadIdx.x + blockIdx.x * blockDim.x; tid < tStop; + tid += blockDim.x * gridDim.x) { auto const p1 = tid % N, p2 = tid / N; auto x = r[3 * p2 + 0] - r[3 * p1 + 0]; auto y = r[3 * p2 + 1] - r[3 * p1 + 1]; @@ -354,17 +351,15 @@ __global__ void forcesKernel(const float *__restrict__ r, auto sum_r = 0.f; auto sum_z = 0.f; - // if (*boxz <= 0.0) return; // in case we are not initialized yet - - while (fabs(z) > *boxz / 2) // make sure we take the shortest distance - z -= (z > 0 ? 1.f : -1.f) * *boxz; + while (fabs(z) > *boxz / 2.f) // make sure we take the shortest distance + z -= (z > 0.f ? 1.f : -1.f) * *boxz; - if (p1 == p2) // particle exerts no force on itself - { + if (p1 == p2) { + // particle exerts no force on itself rxy = 1.f; // so the division at the end doesn't fail with NaN // (sum_r is 0 anyway) - } else if (rxy2 <= *far_switch_radius_2) // near formula - { + } else if (rxy2 <= *far_switch_radius_sq) { + // near formula auto const uzz = *uz * z; auto const uzr = *uz * rxy; sum_z = dev_mod_psi_odd(0, uzz); @@ -394,29 +389,29 @@ __global__ void forcesKernel(const float *__restrict__ r, sum_z += (z + *boxz) * cbpow(rsqrt(rxy2 + sqpow(z + *boxz))); sum_z += (z - *boxz) * cbpow(rsqrt(rxy2 + sqpow(z - *boxz))); - if (rxy == 0) // particles at the same radial position only exert a force - // in z direction - { + if (rxy == 0.f) { + // particles at the same radial position only exert a force + // in z direction rxy = 1.f; // so the division at the end doesn't fail with NaN // (sum_r is 0 anyway) } - } else // far formula - { + } else { + // far formula for (int p = 1; p < *bessel_cutoff; p++) { float arg = c_2pif * *uz * static_cast(p); sum_r += static_cast(p) * dev_K1(arg * rxy) * cos(arg * z); sum_z += static_cast(p) * dev_K0(arg * rxy) * sin(arg * z); } - sum_r *= sqpow(*uz) * 4 * c_2pif; - sum_z *= sqpow(*uz) * 4 * c_2pif; - sum_r += 2 * *uz / rxy; + sum_r *= sqpow(*uz) * 4.f * c_2pif; + sum_z *= sqpow(*uz) * 4.f * c_2pif; + sum_r += 2.f * *uz / rxy; } auto const pref = *coulomb_prefactor * q[p1] * q[p2]; if (pairs) { - force[3 * (p1 + p2 * N - tStart) + 0] = pref * sum_r / rxy * x; - force[3 * (p1 + p2 * N - tStart) + 1] = pref * sum_r / rxy * y; - force[3 * (p1 + p2 * N - tStart) + 2] = pref * sum_z; + force[3 * (p1 + p2 * N) + 0] = pref * sum_r / rxy * x; + force[3 * (p1 + p2 * N) + 1] = pref * sum_r / rxy * y; + force[3 * (p1 + p2 * N) + 2] = pref * sum_z; } else { atomicAdd(&force[3 * p2 + 0], pref * sum_r / rxy * x); atomicAdd(&force[3 * p2 + 1], pref * sum_r / rxy * y); @@ -428,9 +423,9 @@ __global__ void forcesKernel(const float *__restrict__ r, __global__ void energiesKernel(const float *__restrict__ r, const float *__restrict__ q, float *__restrict__ energy, std::size_t N, - int pairs, std::size_t tStart) { + int pairs) { - constexpr auto c_2pif = 2 * Utils::pi(); + constexpr auto c_2pif = 2.f * Utils::pi(); constexpr auto c_gammaf = Utils::gamma(); auto const tStop = Utils::sqr(N); @@ -439,8 +434,8 @@ __global__ void energiesKernel(const float *__restrict__ r, partialsums[threadIdx.x] = 0; __syncthreads(); } - for (std::size_t tid = threadIdx.x + blockIdx.x * blockDim.x + tStart; - tid < tStop; tid += blockDim.x * gridDim.x) { + for (std::size_t tid = threadIdx.x + blockIdx.x * blockDim.x; tid < tStop; + tid += blockDim.x * gridDim.x) { auto const p1 = tid % N, p2 = tid / N; auto z = r[3 * p2 + 2] - r[3 * p1 + 2]; auto const rxy2 = sqpow(r[3 * p2 + 0] - r[3 * p1 + 0]) + @@ -448,14 +443,12 @@ __global__ void energiesKernel(const float *__restrict__ r, auto rxy = sqrt(rxy2); auto sum_e = 0.f; - // if (*boxz <= 0.0) return; // in case we are not initialized yet - - while (fabs(z) > *boxz / 2) // make sure we take the shortest distance - z -= (z > 0 ? 1.f : -1.f) * *boxz; + while (fabs(z) > *boxz / 2.f) // make sure we take the shortest distance + z -= (z > 0.f ? 1.f : -1.f) * *boxz; if (p1 == p2) // particle exerts no force on itself { - } else if (rxy2 <= *far_switch_radius_2) // near formula + } else if (rxy2 <= *far_switch_radius_sq) // near formula { auto const uzz = *uz * z; auto const uzr2 = sqpow(*uz * rxy); @@ -471,23 +464,23 @@ __global__ void energiesKernel(const float *__restrict__ r, break; } - sum_e *= -1 * *uz; - sum_e -= 2 * *uz * c_gammaf; + sum_e *= -1.f * *uz; + sum_e -= 2.f * *uz * c_gammaf; sum_e += rsqrt(rxy2 + sqpow(z)); sum_e += rsqrt(rxy2 + sqpow(z + *boxz)); sum_e += rsqrt(rxy2 + sqpow(z - *boxz)); } else // far formula { - sum_e = -(log(rxy * *uz / 2) + c_gammaf) / 2; + sum_e = -(log(rxy * *uz / 2.f) + c_gammaf) / 2.f; for (int p = 1; p < *bessel_cutoff; p++) { auto const arg = c_2pif * *uz * static_cast(p); sum_e += dev_K0(arg * rxy) * cos(arg * z); } - sum_e *= *uz * 4; + sum_e *= *uz * 4.f; } if (pairs) { - energy[p1 + p2 * N - tStart] = *coulomb_prefactor * q[p1] * q[p2] * sum_e; + energy[p1 + p2 * N] = *coulomb_prefactor * q[p1] * q[p2] * sum_e; } else { partialsums[threadIdx.x] += *coulomb_prefactor * q[p1] * q[p2] * sum_e; } @@ -498,14 +491,14 @@ __global__ void energiesKernel(const float *__restrict__ r, } __global__ void vectorReductionKernel(float const *src, float *dst, - std::size_t N, std::size_t tStart) { + std::size_t N) { auto const tStop = Utils::sqr(N); for (std::size_t tid = threadIdx.x + blockIdx.x * blockDim.x; tid < N; tid += blockDim.x * gridDim.x) { - auto const offset = (tid + (tStart % N)) % N; - for (std::size_t i = 0; tid + i * N < (tStop - tStart); i++) { + auto const offset = tid % N; + for (std::size_t i = 0; tid + i * N < tStop; i++) { #pragma unroll 3 for (std::size_t d = 0; d < 3; d++) { dst[3 * offset + d] -= src[3 * (tid + i * N) + d]; @@ -514,26 +507,27 @@ __global__ void vectorReductionKernel(float const *src, float *dst, } } -void Mmm1dgpuForce::computeForces(SystemInterface &s) { - assert(coulomb.method == COULOMB_MMM1D_GPU); - setup(s); +void CoulombMMM1DGpu::add_long_range_forces() { + setup(); if (pairs < 0) { throw std::runtime_error("MMM1D was not initialized correctly"); } - if (pairs) // if we calculate force pairs, we need to reduce them to forces - { + if (pairs) { + // if we calculate force pairs, we need to reduce them to forces auto const blocksRed = - 1 + static_cast(s.npart_gpu() / + 1 + static_cast(es_system->npart_gpu() / static_cast(numThreads)); - KERNELCALL(forcesKernel, numBlocks(s), numThreads, s.rGpuBegin(), - s.qGpuBegin(), dev_forcePairs, s.npart_gpu(), pairs, 0) + KERNELCALL(forcesKernel, numBlocks(), numThreads, es_system->rGpuBegin(), + es_system->qGpuBegin(), dev_forcePairs, es_system->npart_gpu(), + pairs) KERNELCALL(vectorReductionKernel, blocksRed, numThreads, dev_forcePairs, - s.fGpuBegin(), s.npart_gpu(), 0) + es_system->fGpuBegin(), es_system->npart_gpu()) } else { - KERNELCALL(forcesKernel, numBlocks(s), numThreads, s.rGpuBegin(), - s.qGpuBegin(), s.fGpuBegin(), s.npart_gpu(), pairs, 0) + KERNELCALL(forcesKernel, numBlocks(), numThreads, es_system->rGpuBegin(), + es_system->qGpuBegin(), es_system->fGpuBegin(), + es_system->npart_gpu(), pairs) } } @@ -545,39 +539,38 @@ __global__ void scaleAndAddKernel(float *dst, float const *src, std::size_t N, } } -void Mmm1dgpuForce::computeEnergy(SystemInterface &s) { - assert(coulomb.method == COULOMB_MMM1D_GPU); - setup(s); +void CoulombMMM1DGpu::add_long_range_energy() { + setup(); if (pairs < 0) { throw std::runtime_error("MMM1D was not initialized correctly"); } auto const shared = numThreads * static_cast(sizeof(float)); - KERNELCALL_shared(energiesKernel, numBlocks(s), numThreads, shared, - s.rGpuBegin(), s.qGpuBegin(), dev_energyBlocks, - s.npart_gpu(), 0, 0); + KERNELCALL_shared(energiesKernel, numBlocks(), numThreads, shared, + es_system->rGpuBegin(), es_system->qGpuBegin(), + dev_energyBlocks, es_system->npart_gpu(), 0); KERNELCALL_shared(sumKernel, 1, numThreads, shared, dev_energyBlocks, - numBlocks(s)); + numBlocks()); + // we count every interaction twice, so halve the total energy + auto constexpr factor = 0.5f; KERNELCALL(scaleAndAddKernel, 1, 1, - &(reinterpret_cast(s.eGpu())->coulomb), - &dev_energyBlocks[0], 1, - 0.5f); // we have counted every interaction twice, so halve the - // total energy + &(reinterpret_cast(es_system->eGpu())->coulomb), + &dev_energyBlocks[0], 1, factor); } -float Mmm1dgpuForce::force_benchmark(SystemInterface &s) { +float CoulombMMM1DGpu::force_benchmark() { cudaEvent_t eventStart, eventStop; float elapsedTime; float *dev_f_benchmark; - cuda_safe_mem( - cudaMalloc((void **)&dev_f_benchmark, 3 * s.npart_gpu() * sizeof(float))); + cuda_safe_mem(cudaMalloc((void **)&dev_f_benchmark, + 3ul * es_system->npart_gpu() * sizeof(float))); cuda_safe_mem(cudaEventCreate(&eventStart)); cuda_safe_mem(cudaEventCreate(&eventStop)); cuda_safe_mem(cudaEventRecord(eventStart, stream[0])); - KERNELCALL(forcesKernel, numBlocks(s), numThreads, s.rGpuBegin(), - s.qGpuBegin(), dev_f_benchmark, s.npart_gpu(), 0, 0) + KERNELCALL(forcesKernel, numBlocks(), numThreads, es_system->rGpuBegin(), + es_system->qGpuBegin(), dev_f_benchmark, es_system->npart_gpu(), 0) cuda_safe_mem(cudaEventRecord(eventStop, stream[0])); cuda_safe_mem(cudaEventSynchronize(eventStop)); cuda_safe_mem(cudaEventElapsedTime(&elapsedTime, eventStart, eventStop)); @@ -588,4 +581,4 @@ float Mmm1dgpuForce::force_benchmark(SystemInterface &s) { return elapsedTime; } -#endif /* MMM1D_GPU */ +#endif // MMM1D_GPU diff --git a/src/core/electrostatics/p3m.cpp b/src/core/electrostatics/p3m.cpp new file mode 100644 index 00000000000..d6d035f7ea6 --- /dev/null +++ b/src/core/electrostatics/p3m.cpp @@ -0,0 +1,790 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** @file + * + * The corresponding header file is @ref p3m.hpp. + */ + +#include "electrostatics/p3m.hpp" + +#ifdef P3M + +#include "electrostatics/coulomb.hpp" +#include "electrostatics/elc.hpp" +#include "electrostatics/p3m_gpu.hpp" +#include "electrostatics/p3m_gpu_error.hpp" + +#include "p3m/TuningAlgorithm.hpp" +#include "p3m/TuningLogger.hpp" +#include "p3m/fft.hpp" +#include "p3m/influence_function.hpp" + +#include "Particle.hpp" +#include "ParticleRange.hpp" +#include "actor/visitors.hpp" +#include "cell_system/CellStructureType.hpp" +#include "cells.hpp" +#include "communication.hpp" +#include "errorhandling.hpp" +#include "event.hpp" +#include "grid.hpp" +#include "integrate.hpp" +#include "tuning.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +void CoulombP3M::count_charged_particles() { + auto local_n = 0; + auto local_q2 = 0.0; + auto local_q = 0.0; + + for (auto const &p : cell_structure.local_particles()) { + if (p.q() != 0.0) { + local_n++; + local_q2 += Utils::sqr(p.q()); + local_q += p.q(); + } + } + + boost::mpi::all_reduce(comm_cart, local_n, p3m.sum_qpart, std::plus<>()); + boost::mpi::all_reduce(comm_cart, local_q2, p3m.sum_q2, std::plus<>()); + boost::mpi::all_reduce(comm_cart, local_q, p3m.square_sum_q, std::plus<>()); + p3m.square_sum_q = Utils::sqr(p3m.square_sum_q); +} + +/** Calculate the optimal influence function of @cite hockney88a. + * (optimised for force calculations) + * + * Each node calculates only the values for its domain in k-space + * (see fft.plan[3].mesh and fft.plan[3].start). + * + * See also: @cite hockney88a eq. 8-22 (p. 275). Note the somewhat + * different convention for the prefactors, which is described in + * @cite deserno98a @cite deserno98b. + */ +void CoulombP3M::calc_influence_function_force() { + auto const start = Utils::Vector3i{p3m.fft.plan[3].start}; + auto const size = Utils::Vector3i{p3m.fft.plan[3].new_mesh}; + + p3m.g_force = grid_influence_function<1>(p3m.params, start, start + size, + box_geo.length()); +} + +/** Calculate the influence function optimized for the energy and the + * self energy correction. + */ +void CoulombP3M::calc_influence_function_energy() { + auto const start = Utils::Vector3i{p3m.fft.plan[3].start}; + auto const size = Utils::Vector3i{p3m.fft.plan[3].new_mesh}; + + p3m.g_energy = grid_influence_function<0>(p3m.params, start, start + size, + box_geo.length()); +} + +/** Aliasing sum used by @ref p3m_k_space_error. */ +static void p3m_tune_aliasing_sums(int nx, int ny, int nz, + Utils::Vector3i const &mesh, + Utils::Vector3d const &mesh_i, int cao, + double alpha_L_i, double *alias1, + double *alias2) { + using Utils::sinc; + + auto const factor1 = Utils::sqr(Utils::pi() * alpha_L_i); + + *alias1 = *alias2 = 0.0; + for (int mx = -P3M_BRILLOUIN; mx <= P3M_BRILLOUIN; mx++) { + auto const nmx = nx + mx * mesh[0]; + auto const fnmx = mesh_i[0] * nmx; + for (int my = -P3M_BRILLOUIN; my <= P3M_BRILLOUIN; my++) { + auto const nmy = ny + my * mesh[1]; + auto const fnmy = mesh_i[1] * nmy; + for (int mz = -P3M_BRILLOUIN; mz <= P3M_BRILLOUIN; mz++) { + auto const nmz = nz + mz * mesh[2]; + auto const fnmz = mesh_i[2] * nmz; + + auto const nm2 = Utils::sqr(nmx) + Utils::sqr(nmy) + Utils::sqr(nmz); + auto const ex = exp(-factor1 * nm2); + auto const ex2 = Utils::sqr(ex); + + auto const U2 = pow(sinc(fnmx) * sinc(fnmy) * sinc(fnmz), 2.0 * cao); + + *alias1 += ex2 / nm2; + *alias2 += U2 * ex * (nx * nmx + ny * nmy + nz * nmz) / nm2; + } + } + } +} + +/** Calculate the real space contribution to the rms error in the force (as + * described by Kolafa and Perram). + * \param pref Prefactor of Coulomb interaction. + * \param r_cut_iL rescaled real space cutoff for p3m method. + * \param n_c_part number of charged particles in the system. + * \param sum_q2 sum of square of charges in the system + * \param alpha_L rescaled Ewald splitting parameter. + * \return real space error + */ +static double p3m_real_space_error(double pref, double r_cut_iL, int n_c_part, + double sum_q2, double alpha_L) { + return (2. * pref * sum_q2 * exp(-Utils::sqr(r_cut_iL * alpha_L))) / + sqrt(static_cast(n_c_part) * r_cut_iL * box_geo.length()[0] * + box_geo.volume()); +} + +/** Calculate the analytic expression of the error estimate for the + * P3M method in @cite hockney88a (eq. 8-23 p. 275) in + * order to obtain the rms error in the force for a system of N + * randomly distributed particles in a cubic box (k-space part). + * \param pref Prefactor of Coulomb interaction. + * \param mesh number of mesh points in one direction. + * \param cao charge assignment order. + * \param n_c_part number of charged particles in the system. + * \param sum_q2 sum of square of charges in the system + * \param alpha_L rescaled Ewald splitting parameter. + * \return reciprocal (k) space error + */ +static double p3m_k_space_error(double pref, Utils::Vector3i const &mesh, + int cao, int n_c_part, double sum_q2, + double alpha_L) { + auto const mesh_i = + Utils::hadamard_division(Utils::Vector3d::broadcast(1.), mesh); + auto const alpha_L_i = 1. / alpha_L; + auto he_q = 0.; + + for (int nx = -mesh[0] / 2; nx < mesh[0] / 2; nx++) { + auto const ctan_x = p3m_analytic_cotangent_sum(nx, mesh_i[0], cao); + for (int ny = -mesh[1] / 2; ny < mesh[1] / 2; ny++) { + auto const ctan_y = + ctan_x * p3m_analytic_cotangent_sum(ny, mesh_i[1], cao); + for (int nz = -mesh[2] / 2; nz < mesh[2] / 2; nz++) { + if ((nx != 0) || (ny != 0) || (nz != 0)) { + auto const n2 = Utils::sqr(nx) + Utils::sqr(ny) + Utils::sqr(nz); + auto const cs = + p3m_analytic_cotangent_sum(nz, mesh_i[2], cao) * ctan_y; + double alias1, alias2; + p3m_tune_aliasing_sums(nx, ny, nz, mesh, mesh_i, cao, alpha_L_i, + &alias1, &alias2); + + auto const d = alias1 - Utils::sqr(alias2 / cs) / n2; + /* at high precision, d can become negative due to extinction; + also, don't take values that have no significant digits left*/ + if (d > 0 && (fabs(d / alias1) > ROUND_ERROR_PREC)) + he_q += d; + } + } + } + } + return 2. * pref * sum_q2 * sqrt(he_q / static_cast(n_c_part)) / + (box_geo.length()[1] * box_geo.length()[2]); +} + +#ifdef CUDA +static double p3mgpu_k_space_error(double prefactor, + Utils::Vector3i const &mesh, int cao, + int npart, double sum_q2, double alpha_L) { + auto ks_err = 0.; + if (this_node == 0) { + ks_err = p3m_k_space_error_gpu(prefactor, mesh.data(), cao, npart, sum_q2, + alpha_L, box_geo.length().data()); + } + boost::mpi::broadcast(comm_cart, ks_err, 0); + return ks_err; +} +#endif + +void CoulombP3M::init() { + assert(p3m.params.mesh >= Utils::Vector3i::broadcast(1)); + assert(p3m.params.cao >= 1 and p3m.params.cao <= 7); + assert(p3m.params.alpha > 0.); + + p3m.params.cao3 = Utils::int_pow<3>(p3m.params.cao); + p3m.params.recalc_a_ai_cao_cut(box_geo.length()); + + sanity_checks(); + + double elc_layer = 0.; + if (auto elc_actor = get_actor_by_type( + electrostatics_actor)) { + elc_layer = elc_actor->elc.space_layer; + } + + p3m.local_mesh.calc_local_ca_mesh(p3m.params, local_geo, skin, elc_layer); + p3m.sm.resize(comm_cart, p3m.local_mesh); + + int ca_mesh_size = + fft_init(p3m.local_mesh.dim, p3m.local_mesh.margin, p3m.params.mesh, + p3m.params.mesh_off, p3m.ks_pnum, p3m.fft, node_grid, comm_cart); + p3m.rs_mesh.resize(ca_mesh_size); + + for (auto &e : p3m.E_mesh) { + e.resize(ca_mesh_size); + } + + p3m.calc_differential_operator(); + + /* fix box length dependent constants */ + scaleby_box_l(); + + count_charged_particles(); +} + +CoulombP3M::CoulombP3M(P3MParameters &¶meters, double prefactor, + int tune_timings, bool tune_verbose) + : p3m{std::move(parameters)}, tune_timings{tune_timings}, + tune_verbose{tune_verbose} { + + m_is_tuned = !p3m.params.tuning; + p3m.params.tuning = false; + set_prefactor(prefactor); +} + +namespace { +template struct AssignCharge { + void operator()(p3m_data_struct &p3m, double q, + Utils::Vector3d const &real_pos, + p3m_interpolation_cache &inter_weights) { + auto const w = p3m_calculate_interpolation_weights( + real_pos, p3m.params.ai, p3m.local_mesh); + + inter_weights.store(w); + + p3m_interpolate(p3m.local_mesh, w, [q, &p3m](int ind, double w) { + p3m.rs_mesh[ind] += w * q; + }); + } + + void operator()(p3m_data_struct &p3m, double q, + Utils::Vector3d const &real_pos) { + p3m_interpolate( + p3m.local_mesh, + p3m_calculate_interpolation_weights(real_pos, p3m.params.ai, + p3m.local_mesh), + [q, &p3m](int ind, double w) { p3m.rs_mesh[ind] += w * q; }); + } + + void operator()(p3m_data_struct &p3m, ParticleRange const &particles) { + for (auto &p : particles) { + if (p.q() != 0.0) { + this->operator()(p3m, p.q(), p.pos(), p3m.inter_weights); + } + } + } +}; +} // namespace + +void CoulombP3M::charge_assign(ParticleRange const &particles) { + p3m.inter_weights.reset(p3m.params.cao); + + /* prepare local FFT mesh */ + for (int i = 0; i < p3m.local_mesh.size; i++) + p3m.rs_mesh[i] = 0.0; + + Utils::integral_parameter(p3m.params.cao, p3m, particles); +} + +void CoulombP3M::assign_charge(double q, Utils::Vector3d const &real_pos, + p3m_interpolation_cache &inter_weights) { + Utils::integral_parameter(p3m.params.cao, p3m, q, + real_pos, inter_weights); +} + +void CoulombP3M::assign_charge(double q, Utils::Vector3d const &real_pos) { + Utils::integral_parameter(p3m.params.cao, p3m, q, + real_pos); +} + +namespace { +template struct AssignForces { + void operator()(p3m_data_struct &p3m, double force_prefac, + ParticleRange const &particles) const { + using Utils::make_const_span; + using Utils::Span; + using Utils::Vector; + + assert(cao == p3m.inter_weights.cao()); + + /* charged particle counter */ + auto p_index = std::size_t{0ul}; + + for (auto &p : particles) { + if (p.q() != 0.0) { + auto const pref = p.q() * force_prefac; + auto const w = p3m.inter_weights.load(p_index); + + Utils::Vector3d force{}; + p3m_interpolate(p3m.local_mesh, w, [&force, &p3m](int ind, double w) { + force += w * Utils::Vector3d{p3m.E_mesh[0][ind], p3m.E_mesh[1][ind], + p3m.E_mesh[2][ind]}; + }); + + p.force() -= pref * force; + ++p_index; + } + } + } +}; + +auto dipole_moment(Particle const &p, BoxGeometry const &box) { + return p.q() * unfolded_position(p.pos(), p.image_box(), box.length()); +} + +auto calc_dipole_moment(boost::mpi::communicator const &comm, + ParticleRange const &particles, + BoxGeometry const &box) { + auto const local_dip = boost::accumulate( + particles, Utils::Vector3d{}, [&box](Utils::Vector3d dip, auto const &p) { + return dip + dipole_moment(p, box); + }); + + return boost::mpi::all_reduce(comm, local_dip, std::plus<>()); +} +} // namespace + +/** @details Calculate the long range electrostatics part of the pressure + * tensor. This is part \f$\Pi_{\textrm{dir}, \alpha, \beta}\f$ eq. (2.6) + * in @cite essmann95a. The part \f$\Pi_{\textrm{corr}, \alpha, \beta}\f$ + * eq. (2.8) is not present here since M is the empty set in our simulations. + */ +Utils::Vector9d CoulombP3M::p3m_calc_kspace_pressure_tensor() { + using namespace detail::FFT_indexing; + + Utils::Vector9d node_k_space_pressure_tensor{}; + + if (p3m.sum_q2 > 0.) { + p3m.sm.gather_grid(p3m.rs_mesh.data(), comm_cart, p3m.local_mesh.dim); + fft_perform_forw(p3m.rs_mesh.data(), p3m.fft, comm_cart); + + auto diagonal = 0.; + int ind = 0; + int j[3]; + auto const half_alpha_inv_sq = Utils::sqr(1. / 2. / p3m.params.alpha); + for (j[0] = 0; j[0] < p3m.fft.plan[3].new_mesh[RX]; j[0]++) { + for (j[1] = 0; j[1] < p3m.fft.plan[3].new_mesh[RY]; j[1]++) { + for (j[2] = 0; j[2] < p3m.fft.plan[3].new_mesh[RZ]; j[2]++) { + auto const kx = 2. * Utils::pi() * + p3m.d_op[RX][j[KX] + p3m.fft.plan[3].start[KX]] * + box_geo.length_inv()[RX]; + auto const ky = 2. * Utils::pi() * + p3m.d_op[RY][j[KY] + p3m.fft.plan[3].start[KY]] * + box_geo.length_inv()[RY]; + auto const kz = 2. * Utils::pi() * + p3m.d_op[RZ][j[KZ] + p3m.fft.plan[3].start[KZ]] * + box_geo.length_inv()[RZ]; + auto const sqk = Utils::sqr(kx) + Utils::sqr(ky) + Utils::sqr(kz); + + if (sqk != 0.) { + auto const node_k_space_energy = + p3m.g_energy[ind] * (Utils::sqr(p3m.rs_mesh[2 * ind]) + + Utils::sqr(p3m.rs_mesh[2 * ind + 1])); + auto const vterm = -2. * (1. / sqk + half_alpha_inv_sq); + auto const pref = node_k_space_energy * vterm; + node_k_space_pressure_tensor[0] += pref * kx * kx; /* sigma_xx */ + node_k_space_pressure_tensor[1] += pref * kx * ky; /* sigma_xy */ + node_k_space_pressure_tensor[2] += pref * kx * kz; /* sigma_xz */ + node_k_space_pressure_tensor[3] += pref * ky * kx; /* sigma_yx */ + node_k_space_pressure_tensor[4] += pref * ky * ky; /* sigma_yy */ + node_k_space_pressure_tensor[5] += pref * ky * kz; /* sigma_yz */ + node_k_space_pressure_tensor[6] += pref * kz * kx; /* sigma_zx */ + node_k_space_pressure_tensor[7] += pref * kz * ky; /* sigma_zy */ + node_k_space_pressure_tensor[8] += pref * kz * kz; /* sigma_zz */ + diagonal += node_k_space_energy; + } + ind++; + } + } + } + node_k_space_pressure_tensor[0] += diagonal; + node_k_space_pressure_tensor[4] += diagonal; + node_k_space_pressure_tensor[8] += diagonal; + } + + return node_k_space_pressure_tensor * prefactor / (2. * box_geo.volume()); +} + +double CoulombP3M::long_range_kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles) { + /* Gather information for FFT grid inside the nodes domain (inner local mesh) + * and perform forward 3D FFT (Charge Assignment Mesh). */ + p3m.sm.gather_grid(p3m.rs_mesh.data(), comm_cart, p3m.local_mesh.dim); + fft_perform_forw(p3m.rs_mesh.data(), p3m.fft, comm_cart); + + // Note: after these calls, the grids are in the order yzx and not xyz + // anymore!!! + /* The dipole moment is only needed if we don't have metallic boundaries. */ + auto const box_dipole = (p3m.params.epsilon != P3M_EPSILON_METALLIC) + ? boost::make_optional(calc_dipole_moment( + comm_cart, particles, box_geo)) + : boost::none; + auto const volume = box_geo.volume(); + auto const pref = 4. * Utils::pi() / volume / (2. * p3m.params.epsilon + 1.); + + /* === k-space force calculation === */ + if (force_flag) { + /* sqrt(-1)*k differentiation */ + int j[3]; + int ind = 0; + for (j[0] = 0; j[0] < p3m.fft.plan[3].new_mesh[0]; j[0]++) { + for (j[1] = 0; j[1] < p3m.fft.plan[3].new_mesh[1]; j[1]++) { + for (j[2] = 0; j[2] < p3m.fft.plan[3].new_mesh[2]; j[2]++) { + auto const rho_hat = std::complex(p3m.rs_mesh[2 * ind + 0], + p3m.rs_mesh[2 * ind + 1]); + auto const phi_hat = p3m.g_force[ind] * rho_hat; + + for (int d = 0; d < 3; d++) { + /* direction in r-space: */ + int d_rs = (d + p3m.ks_pnum) % 3; + /* directions */ + auto const k = 2. * Utils::pi() * + p3m.d_op[d_rs][j[d] + p3m.fft.plan[3].start[d]] * + box_geo.length_inv()[d_rs]; + + /* i*k*(Re+i*Im) = - Im*k + i*Re*k (i=sqrt(-1)) */ + p3m.E_mesh[d_rs][2 * ind + 0] = -k * phi_hat.imag(); + p3m.E_mesh[d_rs][2 * ind + 1] = +k * phi_hat.real(); + } + + ind++; + } + } + } + + /* Back FFT force component mesh */ + auto const check_complex = !p3m.params.tuning; + for (int d = 0; d < 3; d++) { + fft_perform_back(p3m.E_mesh[d].data(), check_complex, p3m.fft, comm_cart); + } + + /* redistribute force component mesh */ + std::array E_fields = { + {p3m.E_mesh[0].data(), p3m.E_mesh[1].data(), p3m.E_mesh[2].data()}}; + p3m.sm.spread_grid(Utils::make_span(E_fields), comm_cart, + p3m.local_mesh.dim); + + auto const force_prefac = prefactor / volume; + Utils::integral_parameter(p3m.params.cao, p3m, + force_prefac, particles); + + // add dipole forces + if (p3m.params.epsilon != P3M_EPSILON_METALLIC) { + auto const dm = prefactor * pref * box_dipole.value(); + for (auto &p : particles) { + p.force() -= p.q() * dm; + } + } + } + + /* === k-space energy calculation === */ + if (energy_flag) { + auto node_energy = 0.; + for (int i = 0; i < p3m.fft.plan[3].new_size; i++) { + // Use the energy optimized influence function for energy! + node_energy += p3m.g_energy[i] * (Utils::sqr(p3m.rs_mesh[2 * i]) + + Utils::sqr(p3m.rs_mesh[2 * i + 1])); + } + node_energy /= 2. * volume; + + auto energy = 0.; + boost::mpi::reduce(comm_cart, node_energy, energy, std::plus<>(), 0); + if (this_node == 0) { + /* self energy correction */ + energy -= p3m.sum_q2 * p3m.params.alpha * Utils::sqrt_pi_i(); + /* net charge correction */ + energy -= p3m.square_sum_q * Utils::pi() / + (2. * volume * Utils::sqr(p3m.params.alpha)); + /* dipole correction */ + if (p3m.params.epsilon != P3M_EPSILON_METALLIC) { + energy += pref * box_dipole.value().norm2(); + } + } + return prefactor * energy; + } + + return 0.; +} + +class CoulombTuningAlgorithm : public TuningAlgorithm { + p3m_data_struct &p3m; + double m_mesh_density_min = -1., m_mesh_density_max = -1.; + // indicates if mesh should be tuned + bool m_tune_mesh = false; + +public: + CoulombTuningAlgorithm(p3m_data_struct &input_p3m, double prefactor, + int timings) + : TuningAlgorithm{prefactor, timings}, p3m{input_p3m} {} + + P3MParameters &get_params() override { return p3m.params; } + + void on_solver_change() const override { on_coulomb_change(); } + + void setup_logger(bool verbose) override { +#ifdef CUDA + auto const on_gpu = has_actor_of_type(electrostatics_actor); +#else + auto const on_gpu = false; +#endif + m_logger = std::make_unique( + verbose and this_node == 0, (on_gpu) ? "CoulombP3MGPU" : "CoulombP3M", + TuningLogger::Mode::Coulomb); + m_logger->tuning_goals(p3m.params.accuracy, m_prefactor, + box_geo.length()[0], p3m.sum_qpart, p3m.sum_q2); + m_logger->log_tuning_start(); + } + + boost::optional + layer_correction_veto_r_cut(double r_cut) const override { + if (auto elc_actor = get_actor_by_type( + electrostatics_actor)) { + return elc_actor->veto_r_cut(r_cut); + } + return {}; + } + + std::tuple + calculate_accuracy(Utils::Vector3i const &mesh, int cao, + double r_cut_iL) const override { + + double alpha_L, rs_err, ks_err; + + /* calc maximal real space error for setting */ + rs_err = p3m_real_space_error(m_prefactor, r_cut_iL, p3m.sum_qpart, + p3m.sum_q2, 0.); + + if (Utils::sqrt_2() * rs_err > p3m.params.accuracy) { + /* assume rs_err = ks_err -> rs_err = accuracy/sqrt(2.0) -> alpha_L */ + alpha_L = + sqrt(log(Utils::sqrt_2() * rs_err / p3m.params.accuracy)) / r_cut_iL; + } else { + /* even alpha=0 is ok, however, we cannot choose it since it kills the + k-space error formula. + Anyways, this very likely NOT the optimal solution */ + alpha_L = 0.1; + } + + /* calculate real-space and k-space error for this alpha_L */ + rs_err = p3m_real_space_error(m_prefactor, r_cut_iL, p3m.sum_qpart, + p3m.sum_q2, alpha_L); +#ifdef CUDA + if (has_actor_of_type(electrostatics_actor)) { + ks_err = p3mgpu_k_space_error(m_prefactor, mesh, cao, p3m.sum_qpart, + p3m.sum_q2, alpha_L); + } else +#endif + ks_err = p3m_k_space_error(m_prefactor, mesh, cao, p3m.sum_qpart, + p3m.sum_q2, alpha_L); + + return {Utils::Vector2d{rs_err, ks_err}.norm(), rs_err, ks_err, alpha_L}; + } + + void determine_mesh_limits() override { + auto const mesh_density = + static_cast(p3m.params.mesh[0]) * box_geo.length_inv()[0]; + + if (p3m.params.mesh == Utils::Vector3i::broadcast(-1)) { + /* avoid using more than 1 GB of FFT arrays */ + auto const normalized_box_dim = std::cbrt(box_geo.volume()); + auto const max_npart_per_dim = 512.; + /* simple heuristic to limit the tried meshes if the accuracy cannot + be obtained with smaller meshes, but normally not all these + meshes have to be tested */ + auto const min_npart_per_dim = std::min( + max_npart_per_dim, std::cbrt(static_cast(p3m.sum_qpart))); + m_mesh_density_min = min_npart_per_dim / normalized_box_dim; + m_mesh_density_max = max_npart_per_dim / normalized_box_dim; + m_tune_mesh = true; + } else { + m_mesh_density_min = m_mesh_density_max = mesh_density; + assert(p3m.params.mesh[0] >= 1); + if (p3m.params.mesh[1] == -1 and p3m.params.mesh[2] == -1) { + // determine the two missing values by rescaling by the box length + for (int i : {1, 2}) { + p3m.params.mesh[i] = + static_cast(std::round(mesh_density * box_geo.length()[i])); + // make the mesh even in all directions + p3m.params.mesh[i] += p3m.params.mesh[i] % 2; + } + } + m_logger->report_fixed_mesh(p3m.params.mesh); + } + } + + TuningAlgorithm::Parameters get_time() override { + auto tuned_params = TuningAlgorithm::Parameters{}; + auto time_best = time_sentinel; + for (auto mesh_density = m_mesh_density_min; + mesh_density <= m_mesh_density_max; mesh_density += 0.1) { + auto trial_params = TuningAlgorithm::Parameters{}; + if (m_tune_mesh) { + for (int i : {0, 1, 2}) { + trial_params.mesh[i] = + static_cast(std::round(box_geo.length()[i] * mesh_density)); + // make the mesh even in all directions + trial_params.mesh[i] += trial_params.mesh[i] % 2; + } + } else { + trial_params.mesh = p3m.params.mesh; + } + trial_params.cao = cao_best; + + auto const trial_time = + get_m_time(trial_params.mesh, trial_params.cao, trial_params.r_cut_iL, + trial_params.alpha_L, trial_params.accuracy); + + /* this mesh does not work at all */ + if (trial_time < 0.) + continue; + + /* the optimum r_cut for this mesh is the upper limit for higher meshes, + everything else is slower */ + if (has_actor_of_type(electrostatics_actor)) { + m_r_cut_iL_max = trial_params.r_cut_iL; + } + + if (trial_time < time_best) { + /* new optimum */ + reset_n_trials(); + tuned_params = trial_params; + time_best = tuned_params.time = trial_time; + } else if (trial_time > time_best + time_granularity or + get_n_trials() > max_n_consecutive_trials) { + /* no hope of further optimisation */ + break; + } + } + return tuned_params; + } +}; + +void CoulombP3M::tune() { + if (p3m.params.alpha_L == 0. and p3m.params.alpha != 0.) { + p3m.params.alpha_L = p3m.params.alpha * box_geo.length()[0]; + } + if (p3m.params.r_cut_iL == 0. and p3m.params.r_cut != 0.) { + p3m.params.r_cut_iL = p3m.params.r_cut * box_geo.length_inv()[0]; + } + if (not is_tuned()) { + count_charged_particles(); + if (p3m.sum_qpart == 0) { + throw std::runtime_error( + "CoulombP3M: no charged particles in the system"); + } + try { + CoulombTuningAlgorithm parameters(p3m, prefactor, tune_timings); + parameters.setup_logger(tune_verbose); + // parameter ranges + parameters.determine_mesh_limits(); + parameters.determine_r_cut_limits(); + parameters.determine_cao_limits(7); + // run tuning algorithm + parameters.tune(); + m_is_tuned = true; + on_coulomb_change(); + } catch (...) { + p3m.params.tuning = false; + throw; + } + } + init(); +} + +void CoulombP3M::sanity_checks_boxl() const { + for (int i = 0; i < 3; i++) { + /* check k-space cutoff */ + if (p3m.params.cao_cut[i] >= box_geo.length_half()[i]) { + std::stringstream msg; + msg << "P3M_init: k-space cutoff " << p3m.params.cao_cut[i] + << " is larger than half of box dimension " << box_geo.length()[i]; + throw std::runtime_error(msg.str()); + } + if (p3m.params.cao_cut[i] >= local_geo.length()[i]) { + std::stringstream msg; + msg << "P3M_init: k-space cutoff " << p3m.params.cao_cut[i] + << " is larger than local box dimension " << local_geo.length()[i]; + throw std::runtime_error(msg.str()); + } + } + + if (p3m.params.epsilon != P3M_EPSILON_METALLIC) { + if (!((box_geo.length()[0] == box_geo.length()[1]) and + (box_geo.length()[1] == box_geo.length()[2])) or + !((p3m.params.mesh[0] == p3m.params.mesh[1]) and + (p3m.params.mesh[1] == p3m.params.mesh[2]))) { + throw std::runtime_error( + "CoulombP3M: non-metallic epsilon requires cubic box"); + } + } +} + +void CoulombP3M::sanity_checks_periodicity() const { + if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { + throw std::runtime_error("CoulombP3M: requires periodicity (1 1 1)"); + } +} + +void CoulombP3M::sanity_checks_cell_structure() const { + if (local_geo.cell_structure_type() != + CellStructureType::CELL_STRUCTURE_REGULAR) { + throw std::runtime_error( + "CoulombP3M: requires the regular decomposition cell system"); + } +} + +void CoulombP3M::sanity_checks_node_grid() const { + if (node_grid[0] < node_grid[1] || node_grid[1] < node_grid[2]) { + throw std::runtime_error( + "CoulombP3M: node grid must be sorted, largest first"); + } +} + +void CoulombP3M::scaleby_box_l() { + p3m.params.r_cut = p3m.params.r_cut_iL * box_geo.length()[0]; + p3m.params.alpha = p3m.params.alpha_L * box_geo.length_inv()[0]; + p3m.params.recalc_a_ai_cao_cut(box_geo.length()); + p3m.local_mesh.recalc_ld_pos(p3m.params); + sanity_checks_boxl(); + calc_influence_function_force(); + calc_influence_function_energy(); +} + +#endif // P3M diff --git a/src/core/electrostatics/p3m.hpp b/src/core/electrostatics/p3m.hpp new file mode 100644 index 00000000000..0e570445a09 --- /dev/null +++ b/src/core/electrostatics/p3m.hpp @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** @file + * P3M algorithm for long-range Coulomb interaction. + * + * We use a P3M (Particle-Particle Particle-Mesh) method based on the + * Ewald summation. Details of the used method can be found in + * @cite hockney88a and @cite deserno98a @cite deserno98b. + * + * Further reading: @cite ewald21a, @cite hockney88a, @cite deserno98a, + * @cite deserno98b, @cite deserno00e, @cite deserno00b, @cite cerda08d. + * + * Implementation in p3m.cpp. + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_HPP + +#include "config.hpp" + +#ifdef P3M + +#include "electrostatics/actor.hpp" + +#include "p3m/common.hpp" +#include "p3m/data_struct.hpp" +#include "p3m/fft.hpp" +#include "p3m/interpolation.hpp" +#include "p3m/send_mesh.hpp" + +#include "ParticleRange.hpp" + +#include +#include +#include + +#include +#include + +struct p3m_data_struct : public p3m_data_struct_base { + explicit p3m_data_struct(P3MParameters &¶meters) + : p3m_data_struct_base{std::move(parameters)} {} + + /** local mesh. */ + P3MLocalMesh local_mesh; + /** real space mesh (local) for CA/FFT. */ + fft_vector rs_mesh; + /** mesh (local) for the electric field. */ + std::array, 3> E_mesh; + + /** number of charged particles (only on head node). */ + int sum_qpart = 0; + /** Sum of square of charges (only on head node). */ + double sum_q2 = 0.; + /** square of sum of charges (only on head node). */ + double square_sum_q = 0.; + + p3m_interpolation_cache inter_weights; + + /** send/recv mesh sizes */ + p3m_send_mesh sm; + + fft_data_struct fft; +}; + +/** @brief P3M solver. */ +struct CoulombP3M : public Coulomb::Actor { + /** P3M parameters. */ + p3m_data_struct p3m; + + int tune_timings; + bool tune_verbose; + +private: + bool m_is_tuned; + +public: + CoulombP3M(P3MParameters &¶meters, double prefactor, int tune_timings, + bool tune_verbose); + + bool is_tuned() const { return m_is_tuned; } + + /** Compute the k-space part of forces and energies. */ + double kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles); + + /** @brief Recalculate all derived parameters. */ + void init(); + void on_activation() { + sanity_checks(); + tune(); + } + /** @brief Recalculate all box-length-dependent parameters. */ + void on_boxl_change() { scaleby_box_l(); } + void on_node_grid_change() const { sanity_checks_node_grid(); } + void on_periodicity_change() const { sanity_checks_periodicity(); } + void on_cell_structure_change() { + sanity_checks_cell_structure(); + init(); + } + void sanity_checks() const { + sanity_checks_boxl(); + sanity_checks_node_grid(); + sanity_checks_periodicity(); + sanity_checks_cell_structure(); + sanity_checks_charge_neutrality(); + } + + /** + * Count the number of charged particles and calculate + * the sum of the squared charges. + */ + void count_charged_particles(); + + /** + * @brief Tune P3M parameters to desired accuracy. + * + * The parameters + * @ref P3MParameters::mesh "mesh", + * @ref P3MParameters::cao "cao", + * @ref P3MParameters::r_cut_iL "r_cut_iL" and + * @ref P3MParameters::alpha_L "alpha_L" are tuned to obtain the target + * @ref P3MParameters::accuracy "accuracy" in optimal time. + * These parameters are stored in the @ref p3m object. + * + * The function utilizes the analytic expression of the error estimate + * for the P3M method in @cite hockney88a (eq. (8.23)) in + * order to obtain the rms error in the force for a system of N randomly + * distributed particles in a cubic box. + * For the real space error the estimate of Kolafa/Perram is used. + * + * Parameter ranges if not given explicitly in the constructor: + * - @p mesh explores the range 0-512 (biased toward values less than 128) + * - @p cao explores all possible values + * - @p alpha_L is tuned for each tuple (@p r_cut_iL, @p mesh, @p cao) and + * calculated assuming that the error contributions of real and reciprocal + * space should be equal + * + * After checking if the total error lies below the target accuracy, + * the time needed for one force calculation is measured. Parameters + * that minimize the runtime are kept. + * + * The function is based on routines of the program HE_Q.cpp written by M. + * Deserno. + */ + void tune(); + + /** Assign the physical charges using the tabulated charge assignment + * function. + */ + void charge_assign(ParticleRange const &particles); + + /** + * @brief Assign a single charge into the current charge grid. + * + * @param[in] q %Particle charge + * @param[in] real_pos %Particle position in real space + * @param[out] inter_weights Cached interpolation weights to be used. + */ + void assign_charge(double q, Utils::Vector3d const &real_pos, + p3m_interpolation_cache &inter_weights); + /** @overload */ + void assign_charge(double q, Utils::Vector3d const &real_pos); + + /** Calculate real-space contribution of p3m Coulomb pair forces. */ + Utils::Vector3d pair_force(double q1q2, Utils::Vector3d const &d, + double dist) const { + if ((q1q2 == 0.) || dist >= p3m.params.r_cut || dist <= 0.) { + return {}; + } + auto const adist = p3m.params.alpha * dist; + auto const exp_adist_sq = exp(-adist * adist); + auto const dist_sq = dist * dist; + auto const two_a_sqrt_pi_i = 2.0 * p3m.params.alpha * Utils::sqrt_pi_i(); +#if USE_ERFC_APPROXIMATION + auto const erfc_part_ri = Utils::AS_erfc_part(adist) / dist; + auto const fac = exp_adist_sq * (erfc_part_ri + two_a_sqrt_pi_i) / dist_sq; +#else + auto const erfc_part_ri = erfc(adist) / dist; + auto const fac = (erfc_part_ri + two_a_sqrt_pi_i * exp_adist_sq) / dist_sq; +#endif + return (fac * prefactor * q1q2) * d; + } + + /** Calculate real-space contribution of Coulomb pair energy. */ + double pair_energy(double q1q2, double dist) const { + if ((q1q2 == 0.) || dist >= p3m.params.r_cut || dist <= 0.) { + return {}; + } + auto const adist = p3m.params.alpha * dist; +#if USE_ERFC_APPROXIMATION + auto const erfc_part_ri = Utils::AS_erfc_part(adist) / dist; + return prefactor * q1q2 * erfc_part_ri * exp(-adist * adist); +#else + auto const erfc_part_ri = erfc(adist) / dist; + return prefactor * q1q2 * erfc_part_ri; +#endif + } + + /** Compute the k-space part of the pressure tensor */ + Utils::Vector9d p3m_calc_kspace_pressure_tensor(); + + /** Compute the k-space part of energies. */ + double long_range_energy(ParticleRange const &particles) { + return long_range_kernel(false, true, particles); + } + + /** Compute the k-space part of forces. */ + void add_long_range_forces(ParticleRange const &particles) { + long_range_kernel(true, false, particles); + } + + /** Compute the k-space part of forces and energies. */ + double long_range_kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles); + +private: + void calc_influence_function_force(); + void calc_influence_function_energy(); + + /** Checks for correctness of the k-space cutoff. */ + void sanity_checks_boxl() const; + void sanity_checks_node_grid() const; + void sanity_checks_periodicity() const; + void sanity_checks_cell_structure() const; + + void scaleby_box_l(); +}; + +#endif // P3M + +#endif diff --git a/src/core/electrostatics/p3m_gpu.cpp b/src/core/electrostatics/p3m_gpu.cpp new file mode 100644 index 00000000000..400f0d154f3 --- /dev/null +++ b/src/core/electrostatics/p3m_gpu.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef P3M +#ifdef CUDA + +#include "electrostatics/p3m_gpu.hpp" +#include "electrostatics/p3m_gpu_cuda.cuh" + +#include "actor/visitors.hpp" +#include "electrostatics/coulomb.hpp" + +#include "EspressoSystemInterface.hpp" +#include "ParticleRange.hpp" + +#include "communication.hpp" + +void CoulombP3MGPU::add_long_range_forces(ParticleRange const &) { + if (this_node == 0) { + p3m_gpu_add_farfield_force(prefactor); + } +} + +void CoulombP3MGPU::init() { + if (has_actor_of_type(electrostatics_actor)) { + init_cpu_kernels(); + } + p3m_gpu_init(p3m.params.cao, p3m.params.mesh.data(), p3m.params.alpha); +} + +void CoulombP3MGPU::init_cpu_kernels() { CoulombP3M::init(); } + +void CoulombP3MGPU::request_gpu() const { + auto &system = EspressoSystemInterface::Instance(); + system.requestParticleStructGpuParallel(); +} + +#endif // CUDA +#endif // P3M diff --git a/src/core/electrostatics/p3m_gpu.hpp b/src/core/electrostatics/p3m_gpu.hpp new file mode 100644 index 00000000000..09f7bb1d522 --- /dev/null +++ b/src/core/electrostatics/p3m_gpu.hpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_GPU_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_GPU_HPP + +/** + * @file + * P3M electrostatics on GPU. + */ + +#include "config.hpp" + +#ifdef P3M +#ifdef CUDA + +#include "electrostatics/p3m.hpp" + +#include "ParticleRange.hpp" + +struct CoulombP3MGPU : public CoulombP3M { + using CoulombP3M::CoulombP3M; + + void init(); + void on_activation() { + CoulombP3M::on_activation(); + if (is_tuned()) { + init_cpu_kernels(); + } + } + + void add_long_range_forces(ParticleRange const &); + void request_gpu() const; + +private: + /** + * @brief Initialize the CPU kernels. + * This operation is time-consuming and sets up data members + * that are only relevant for ELC force corrections. + */ + void init_cpu_kernels(); +}; + +#endif // CUDA +#endif // P3M + +#endif diff --git a/src/core/electrostatics_magnetostatics/p3m_gpu_cuda.cu b/src/core/electrostatics/p3m_gpu_cuda.cu similarity index 93% rename from src/core/electrostatics_magnetostatics/p3m_gpu_cuda.cu rename to src/core/electrostatics/p3m_gpu_cuda.cu index 8ae7a05e8bf..1c8926c4ad3 100644 --- a/src/core/electrostatics_magnetostatics/p3m_gpu_cuda.cu +++ b/src/core/electrostatics/p3m_gpu_cuda.cu @@ -17,11 +17,12 @@ * along with this program. If not, see . */ -/** \file +/** + * @file * - * P3M electrostatics on GPU. + * P3M electrostatics on GPU. * - * The corresponding header file is p3m_gpu.hpp. + * The corresponding header file is @ref p3m_gpu_cuda.cuh. */ #include "config.hpp" @@ -49,12 +50,11 @@ #define FFT_PLAN_BACK_FLAG CUFFT_Z2D #endif -#include "electrostatics_magnetostatics/p3m_gpu.hpp" +#include "electrostatics/p3m_gpu_cuda.cuh" #include "EspressoSystemInterface.hpp" #include "cuda_interface.hpp" #include "cuda_utils.cuh" -#include "electrostatics_magnetostatics/coulomb.hpp" #include #include @@ -403,7 +403,7 @@ void assign_charges(const CUDA_particle_data *const pdata, const P3MGpuData p) { template __global__ void assign_forces_kernel(const CUDA_particle_data *const pdata, const P3MGpuData par, - float *lb_particle_force_gpu, + float *particle_force_gpu, REAL_TYPE prefactor, int parts_per_block) { auto const part_in_block = static_cast(threadIdx.x) / cao; auto const cao_id_x = static_cast(threadIdx.x) - part_in_block * cao; @@ -424,9 +424,9 @@ __global__ void assign_forces_kernel(const CUDA_particle_data *const pdata, m_pos[1] = p.p[1] * par.hi[1] - par.pos_shift; m_pos[2] = p.p[2] * par.hi[2] - par.pos_shift; - nmp_x = static_cast(floorf(m_pos[0] + 0.5f)); - nmp_y = static_cast(floorf(m_pos[1] + 0.5f)); - nmp_z = static_cast(floorf(m_pos[2] + 0.5f)); + nmp_x = static_cast(floorf(m_pos[0] + REAL_TYPE{0.5})); + nmp_y = static_cast(floorf(m_pos[1] + REAL_TYPE{0.5})); + nmp_z = static_cast(floorf(m_pos[2] + REAL_TYPE{0.5})); m_pos[0] -= static_cast(nmp_x); m_pos[1] -= static_cast(nmp_y); @@ -464,13 +464,13 @@ __global__ void assign_forces_kernel(const CUDA_particle_data *const pdata, const REAL_TYPE *force_mesh_y = (REAL_TYPE *)par.force_mesh_y; const REAL_TYPE *force_mesh_z = (REAL_TYPE *)par.force_mesh_z; - atomicAdd(&(lb_particle_force_gpu[3 * id + 0]), c * force_mesh_x[index]); - atomicAdd(&(lb_particle_force_gpu[3 * id + 1]), c * force_mesh_y[index]); - atomicAdd(&(lb_particle_force_gpu[3 * id + 2]), c * force_mesh_z[index]); + atomicAdd(&(particle_force_gpu[3 * id + 0]), c * force_mesh_x[index]); + atomicAdd(&(particle_force_gpu[3 * id + 1]), c * force_mesh_y[index]); + atomicAdd(&(particle_force_gpu[3 * id + 2]), c * force_mesh_z[index]); } void assign_forces(const CUDA_particle_data *const pdata, const P3MGpuData p, - float *lb_particle_force_gpu, REAL_TYPE prefactor) { + float *particle_force_gpu, REAL_TYPE prefactor) { auto const cao = p.cao; auto const cao3 = int_pow<3>(cao); int parts_per_block = 1, n_blocks = 1; @@ -502,36 +502,36 @@ void assign_forces(const CUDA_particle_data *const pdata, const P3MGpuData p, switch (cao) { case 1: (assign_forces_kernel<1, false>)<<>>( - pdata, p, lb_particle_force_gpu, prefactor, parts_per_block); + pdata, p, particle_force_gpu, prefactor, parts_per_block); break; case 2: (assign_forces_kernel<2, false>)<<>>( - pdata, p, lb_particle_force_gpu, prefactor, parts_per_block); + pdata, p, particle_force_gpu, prefactor, parts_per_block); break; case 3: (assign_forces_kernel< 3, true>)<<>>( - pdata, p, lb_particle_force_gpu, prefactor, parts_per_block); + pdata, p, particle_force_gpu, prefactor, parts_per_block); break; case 4: (assign_forces_kernel< 4, true>)<<>>( - pdata, p, lb_particle_force_gpu, prefactor, parts_per_block); + pdata, p, particle_force_gpu, prefactor, parts_per_block); break; case 5: (assign_forces_kernel< 5, true>)<<>>( - pdata, p, lb_particle_force_gpu, prefactor, parts_per_block); + pdata, p, particle_force_gpu, prefactor, parts_per_block); break; case 6: (assign_forces_kernel< 6, true>)<<>>( - pdata, p, lb_particle_force_gpu, prefactor, parts_per_block); + pdata, p, particle_force_gpu, prefactor, parts_per_block); break; case 7: (assign_forces_kernel< 7, true>)<<>>( - pdata, p, lb_particle_force_gpu, prefactor, parts_per_block); + pdata, p, particle_force_gpu, prefactor, parts_per_block); break; default: break; @@ -550,7 +550,6 @@ void p3m_gpu_init(int cao, const int mesh[3], double alpha) { throw std::runtime_error("P3M: invalid mesh size"); auto &espresso_system = EspressoSystemInterface::Instance(); - espresso_system.requestParticleStructGpu(); bool do_reinit = false, mesh_changed = false; p3m_gpu_data.n_part = static_cast(gpu_get_particle_pointer().size()); @@ -675,14 +674,14 @@ void p3m_gpu_init(int cao, const int mesh[3], double alpha) { } /** - * \brief The long range part of the P3M algorithm. + * \brief The long-range part of the P3M algorithm. */ -void p3m_gpu_add_farfield_force() { +void p3m_gpu_add_farfield_force(double prefactor) { auto device_particles = gpu_get_particle_pointer(); - CUDA_particle_data *lb_particle_gpu = device_particles.data(); + CUDA_particle_data *particles_gpu = device_particles.data(); p3m_gpu_data.n_part = static_cast(device_particles.size()); - float *lb_particle_force_gpu = gpu_get_particle_force_pointer(); + float *particle_force_gpu = gpu_get_particle_force_pointer(); if (p3m_gpu_data.n_part == 0) return; @@ -691,15 +690,16 @@ void p3m_gpu_add_farfield_force() { static_cast(p3m_gpu_data.mesh[1]), 1u); dim3 threadsConv(static_cast(p3m_gpu_data.mesh[2] / 2 + 1), 1u, 1u); - auto const volume = Utils::product(Utils::Vector3d(p3m_gpu_data.box)); - auto const prefactor = REAL_TYPE(coulomb.prefactor / (volume * 2.0)); + auto const volume = + Utils::product(Utils::Vector3(p3m_gpu_data.box)); + auto const pref = static_cast(prefactor) / (volume * REAL_TYPE{2}); cuda_safe_mem(cudaMemset(p3m_gpu_data.charge_mesh, 0, static_cast(p3m_gpu_data.mesh_size) * sizeof(REAL_TYPE))); /* Interpolate the charges to the mesh */ - assign_charges(lb_particle_gpu, p3m_gpu_data); + assign_charges(particles_gpu, p3m_gpu_data); /* Do forward FFT of the charge mesh */ if (FFT_FORW_FFT(p3m_gpu_fft_plans.forw_plan, @@ -724,8 +724,7 @@ void p3m_gpu_add_farfield_force() { (REAL_TYPE *)p3m_gpu_data.force_mesh_z); /* Assign the forces from the mesh back to the particles */ - assign_forces(lb_particle_gpu, p3m_gpu_data, lb_particle_force_gpu, - prefactor); + assign_forces(particles_gpu, p3m_gpu_data, particle_force_gpu, pref); } -#endif /* ELECTROSTATICS */ +#endif // ELECTROSTATICS diff --git a/src/core/actor/ActorList.hpp b/src/core/electrostatics/p3m_gpu_cuda.cuh similarity index 74% rename from src/core/actor/ActorList.hpp rename to src/core/electrostatics/p3m_gpu_cuda.cuh index d004069b912..610f2bcf99c 100644 --- a/src/core/actor/ActorList.hpp +++ b/src/core/electrostatics/p3m_gpu_cuda.cuh @@ -16,17 +16,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CORE_ACTOR_ACTORLIST_HPP -#define CORE_ACTOR_ACTORLIST_HPP -#include "Actor.hpp" +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_GPU_CUDA_CUH +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_GPU_CUDA_CUH -#include +void p3m_gpu_init(int cao, const int mesh[3], double alpha); +void p3m_gpu_add_farfield_force(double prefactor); -class ActorList : public std::vector { -public: - void add(Actor *actor); - void remove(Actor *actor); -}; - -#endif /* CORE_ACTOR_ACTORLIST_HPP */ +#endif diff --git a/src/core/electrostatics_magnetostatics/p3m_gpu_error.hpp b/src/core/electrostatics/p3m_gpu_error.hpp similarity index 88% rename from src/core/electrostatics_magnetostatics/p3m_gpu_error.hpp rename to src/core/electrostatics/p3m_gpu_error.hpp index 1212eef6094..24e3569a170 100644 --- a/src/core/electrostatics_magnetostatics/p3m_gpu_error.hpp +++ b/src/core/electrostatics/p3m_gpu_error.hpp @@ -16,8 +16,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef P3M_ERROR_GPU_HPP -#define P3M_ERROR_GPU_HPP + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_GPU_ERROR_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_P3M_GPU_ERROR_HPP + /** @file * P3M electrostatics on GPU. * @@ -30,5 +32,5 @@ double p3m_k_space_error_gpu(double prefactor, const int *mesh, int cao, int npart, double sum_q2, double alpha_L, const double *box); -#endif +#endif // CUDA #endif diff --git a/src/core/electrostatics_magnetostatics/p3m_gpu_error_cuda.cu b/src/core/electrostatics/p3m_gpu_error_cuda.cu similarity index 100% rename from src/core/electrostatics_magnetostatics/p3m_gpu_error_cuda.cu rename to src/core/electrostatics/p3m_gpu_error_cuda.cu diff --git a/src/core/electrostatics/reaction_field.hpp b/src/core/electrostatics/reaction_field.hpp new file mode 100644 index 00000000000..31fee31f2f9 --- /dev/null +++ b/src/core/electrostatics/reaction_field.hpp @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file + * Calculate the Reaction Field energy and force + * for a particle pair @cite neumann85b, @cite tironi95a. + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_REACTION_FIELD_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_REACTION_FIELD_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "electrostatics/actor.hpp" + +#include "Particle.hpp" + +#include +#include + +#include + +/** @brief Reaction Field parameters. */ +struct ReactionField : public Coulomb::Actor { + /** @brief Ionic strength. */ + double kappa; + /** @brief Continuum dielectric constant inside the cavity. */ + double epsilon1; + /** @brief Continuum dielectric constant outside the cavity. */ + double epsilon2; + /** @brief Interaction cutoff. */ + double r_cut; + /** @brief Interaction prefactor. Corresponds to the quantity + * @f$ 1 + B_1 @f$ from eq. 22 in @cite tironi95a. + */ + double B; + ReactionField(double prefactor, double kappa, double epsilon1, + double epsilon2, double r_cut) { + if (kappa < 0.0) { + throw std::domain_error("Parameter 'kappa' must be >= 0"); + } + if (epsilon1 < 0.0) { + throw std::domain_error("Parameter 'epsilon1' must be >= 0"); + } + if (epsilon2 < 0.0) { + throw std::domain_error("Parameter 'epsilon2' must be >= 0"); + } + if (r_cut < 0.0) { + throw std::domain_error("Parameter 'r_cut' must be >= 0"); + } + + set_prefactor(prefactor); + this->kappa = kappa; + this->epsilon1 = epsilon1; + this->epsilon2 = epsilon2; + this->r_cut = r_cut; + B = (2. * (epsilon1 - epsilon2) * (1. + kappa * r_cut) - + epsilon2 * kappa * kappa * r_cut * r_cut) / + ((epsilon1 + 2. * epsilon2) * (1. + kappa * r_cut) + + epsilon2 * kappa * kappa * r_cut * r_cut); + } + + void on_activation() const { sanity_checks(); } + void on_boxl_change() const {} + void on_node_grid_change() const {} + void on_periodicity_change() const {} + void on_cell_structure_change() const {} + void init() const {} + + void sanity_checks() const { sanity_checks_charge_neutrality(); } + + /** @brief Compute the Reaction Field pair force. + * @param[in] q1q2 Product of the charges on p1 and p2. + * @param[in] d Vector pointing from p1 to p2. + * @param[in] dist Distance between p1 and p2. + */ + Utils::Vector3d pair_force(double const q1q2, Utils::Vector3d const &d, + double const dist) const { + if (dist >= r_cut) { + return {}; + } + auto fac = 1. / Utils::int_pow<3>(dist) + B / Utils::int_pow<3>(r_cut); + return (prefactor * q1q2 * fac) * d; + } + + /** @brief Compute the Reaction Field pair energy. + * @param q1q2 Product of the charges on p1 and p2. + * @param dist Distance between p1 and p2. + */ + double pair_energy(double const q1q2, double const dist) const { + if (dist >= r_cut) { + return 0.; + } + auto fac = 1. / dist - (B * dist * dist) / (2. * Utils::int_pow<3>(r_cut)); + // remove discontinuity at dist = r_cut + fac -= (1. - B / 2.) / r_cut; + return prefactor * q1q2 * fac; + } +}; + +#endif // ELECTROSTATICS + +#endif diff --git a/src/core/electrostatics/registration.hpp b/src/core/electrostatics/registration.hpp new file mode 100644 index 00000000000..cfa1303077d --- /dev/null +++ b/src/core/electrostatics/registration.hpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_COULOMB_REGISTRATION_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_COULOMB_REGISTRATION_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "electrostatics/coulomb.hpp" + +#include "actor/registration.hpp" +#include "actor/visitors.hpp" + +#include "event.hpp" + +#include +#include + +#include +#include +#include + +namespace Coulomb { + +template ::value> * = nullptr> +void add_actor(std::shared_ptr const &actor) { + if (::electrostatics_actor) { + auto const name = get_actor_name(*::electrostatics_actor); + throw std::runtime_error("An electrostatics solver is already active (" + + name + ")"); + } + add_actor(::electrostatics_actor, actor, ::on_coulomb_change, + detail::flag_all_reduce); +} + +template ::value> * = nullptr> +void remove_actor(std::shared_ptr const &actor) { + if (not is_already_stored(actor, electrostatics_actor)) { + throw std::runtime_error( + "The given electrostatics solver is not currently active"); + } + remove_actor(::electrostatics_actor, actor, ::on_coulomb_change); +} + +template ::value> * = nullptr> +void add_actor(std::shared_ptr const &actor) { + if (::electrostatics_extension) { + auto const name = get_actor_name(*::electrostatics_extension); + throw std::runtime_error("An electrostatics extension is already active (" + + name + ")"); + } + add_actor(::electrostatics_extension, actor, ::on_coulomb_change, + detail::flag_all_reduce); +} + +template ::value> * = nullptr> +void remove_actor(std::shared_ptr const &actor) { + if (not is_already_stored(actor, ::electrostatics_extension)) { + throw std::runtime_error( + "The given electrostatics extension is not currently active"); + } + remove_actor(::electrostatics_extension, actor, ::on_coulomb_change); +} + +} // namespace Coulomb + +#endif // ELECTROSTATICS +#endif diff --git a/src/core/electrostatics/scafacos.hpp b/src/core/electrostatics/scafacos.hpp new file mode 100644 index 00000000000..47db47a19d4 --- /dev/null +++ b/src/core/electrostatics/scafacos.hpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_SCAFACOS_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_SCAFACOS_HPP + +#include "config.hpp" + +#ifdef SCAFACOS + +#include "electrostatics/actor.hpp" + +#include "scafacos/ScafacosContextBase.hpp" + +#include + +#include +#include + +struct CoulombScafacos : virtual public ScafacosContextBase, + public Coulomb::Actor { + ~CoulombScafacos() override = default; + + void on_activation() { + update_system_params(); + sanity_checks(); + tune(); + } + /** @brief Recalculate all box-length-dependent parameters. */ + void on_boxl_change() { update_system_params(); } + void on_node_grid_change() const {} + void on_periodicity_change() { update_system_params(); } + void on_cell_structure_change() const {} + void init() const {} + + void sanity_checks() const override { sanity_checks_charge_neutrality(); } + + bool is_tuned() const { return m_is_tuned; } + void tune() { + if (not is_tuned()) { + tune_impl(); + } + } + virtual double get_r_cut() const = 0; + virtual double get_pair_force(double dist) const = 0; + virtual double get_pair_energy(double dist) const = 0; + virtual void set_near_field_delegation(bool delegate) = 0; + virtual bool get_near_field_delegation() const = 0; + + /** @brief Calculate near-field pair force. */ + Utils::Vector3d pair_force(double q1q2, Utils::Vector3d const &d, + double dist) const { + if (dist > get_r_cut()) + return {}; + + return d * (-q1q2 * prefactor * get_pair_force(dist) / dist); + } + + /** @brief Calculate near-field pair energy. */ + double pair_energy(double q1q2, double dist) const { + if (dist > get_r_cut()) + return 0.; + + return q1q2 * prefactor * get_pair_energy(dist); + } + +protected: + virtual void tune_impl() = 0; + +private: + bool m_is_tuned = false; +}; + +std::shared_ptr +make_coulomb_scafacos(std::string const &method, std::string const ¶meters); + +#endif // SCAFACOS +#endif diff --git a/src/core/electrostatics/scafacos_impl.cpp b/src/core/electrostatics/scafacos_impl.cpp new file mode 100644 index 00000000000..d4cec083c9a --- /dev/null +++ b/src/core/electrostatics/scafacos_impl.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef SCAFACOS + +#include "electrostatics/scafacos.hpp" +#include "electrostatics/scafacos_impl.hpp" + +#include "cells.hpp" +#include "communication.hpp" +#include "event.hpp" +#include "grid.hpp" +#include "integrate.hpp" +#include "particle_data.hpp" +#include "tuning.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include + +std::shared_ptr +make_coulomb_scafacos(std::string const &method, + std::string const ¶meters) { + return std::make_shared(comm_cart, method, parameters); +} + +void CoulombScafacosImpl::update_particle_data() { + positions.clear(); + charges.clear(); + + for (auto const &p : cell_structure.local_particles()) { + auto const pos = folded_position(p.pos(), box_geo); + positions.push_back(pos[0]); + positions.push_back(pos[1]); + positions.push_back(pos[2]); + charges.push_back(p.q()); + } +} + +void CoulombScafacosImpl::update_particle_forces() const { + if (positions.empty()) + return; + + auto it_fields = fields.begin(); + for (auto &p : cell_structure.local_particles()) { + p.force() += prefactor * p.q() * + Utils::Vector3d(Utils::Span(&*it_fields, 3)); + it_fields += 3; + } + + /* Check that the particle number did not change */ + assert(it_fields == fields.end()); +} + +double CoulombScafacosImpl::time_r_cut(double r_cut) { + set_r_cut_and_tune(r_cut); + return benchmark_integration_step(10); +} + +void CoulombScafacosImpl::tune_r_cut() { + auto constexpr convergence_threshold = 1e-3; + + auto const min_box_l = *boost::min_element(box_geo.length()); + auto const min_local_box_l = *boost::min_element(local_geo.length()); + + /* The bisection code breaks down when r_min < 1 for several methods + * (e.g. p2nfft, p3m, ewald) if the mesh size is not fixed (ScaFaCoS + * either hangs or allocates too much memory) */ + auto r_min = 1.0; + auto r_max = std::min(min_local_box_l, min_box_l / 2.0) - skin; + assert(r_max >= r_min); + auto t_min = 0.0; + auto t_max = std::numeric_limits::max(); + auto r_opt = -1.0; + + /* Run bisection */ + while (std::fabs(r_min - r_max) > convergence_threshold) { + r_opt = (r_max + r_min) / 2.; + auto const dr = 0.5 * (r_max - r_min); + auto const t_mid = time_r_cut(r_min + dr); + t_min = time_r_cut(r_min); + t_max = time_r_cut(r_max); + + if (t_min <= 0.0 or t_max <= 0.0) { + break; + } + + if (t_mid > t_min) { + r_max = r_min += dr; + } else { + r_min += dr; + } + } + assert(r_opt >= 0.); + set_r_cut(r_opt); +} + +void CoulombScafacosImpl::tune_impl() { + update_particle_data(); + + // Check whether we have to do a bisection for the short-range cutoff + // Check if there is a user-supplied cutoff + if (ScafacosContext::get_near_field_delegation() and + ScafacosContext::r_cut() <= 0.0) { + tune_r_cut(); + } else { + // ESPResSo is not affected by a short-range cutoff -> tune in parallel + ScafacosContext::tune(charges, positions); + } + on_coulomb_change(); +} + +#endif // SCAFACOS diff --git a/src/core/electrostatics/scafacos_impl.hpp b/src/core/electrostatics/scafacos_impl.hpp new file mode 100644 index 00000000000..cbe9a2798da --- /dev/null +++ b/src/core/electrostatics/scafacos_impl.hpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_SCAFACOS_IMPL_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_SCAFACOS_IMPL_HPP + +#include "config.hpp" + +#ifdef SCAFACOS + +#include "electrostatics/scafacos.hpp" + +#include "scafacos/ScafacosContext.hpp" + +#include + +#include + +#include + +struct CoulombScafacosImpl : public CoulombScafacos, + public ScafacosContext<::Scafacos::Coulomb> { + using ScafacosContext = ScafacosContext<::Scafacos::Coulomb>; + using CoulombScafacos::CoulombScafacos; + using ScafacosContext::ScafacosContext; + + void update_particle_data() override; + void update_particle_forces() const override; + + double long_range_energy() override { + update_particle_data(); + run(charges, positions, fields, potentials); + return 0.5 * prefactor * boost::inner_product(charges, potentials, 0.); + } + + void add_long_range_forces() override { + update_particle_data(); + run(charges, positions, fields, potentials); + update_particle_forces(); + } + + double get_pair_force(double dist) const override { + return ScafacosContext::pair_force(dist); + } + + double get_pair_energy(double dist) const override { + return ScafacosContext::pair_energy(dist); + } + + void set_near_field_delegation(bool delegate) override { + return ScafacosContext::set_near_field_delegation(delegate); + } + + bool get_near_field_delegation() const override { + return ScafacosContext::get_near_field_delegation(); + } + + double get_r_cut() const override { return ScafacosContext::r_cut(); } + +private: + /** Inputs */ + std::vector positions, charges; + /** Outputs */ + std::vector fields, potentials; + + /** Determine runtime for a specific cutoff */ + double time_r_cut(double r_cut); + + /** Determine the optimal cutoff by bisection */ + void tune_r_cut(); + void tune_impl() override; + void set_r_cut_and_tune(double r_cut) { + update_particle_data(); + set_r_cut(r_cut); + ScafacosContext::tune(charges, positions); + } +}; + +#endif // SCAFACOS +#endif diff --git a/src/core/electrostatics_magnetostatics/specfunc.cpp b/src/core/electrostatics/specfunc.cpp similarity index 99% rename from src/core/electrostatics_magnetostatics/specfunc.cpp rename to src/core/electrostatics/specfunc.cpp index 496fbc32eeb..1ba465d4924 100644 --- a/src/core/electrostatics_magnetostatics/specfunc.cpp +++ b/src/core/electrostatics/specfunc.cpp @@ -41,9 +41,10 @@ /* Original Author: G. Jungman */ -/** \file - * Implementation of \ref specfunc.hpp. +/** @file + * Implementation of @ref specfunc.hpp. */ + #include "specfunc.hpp" #include diff --git a/src/core/actor/specfunc_cuda.hpp b/src/core/electrostatics/specfunc.cuh similarity index 98% rename from src/core/actor/specfunc_cuda.hpp rename to src/core/electrostatics/specfunc.cuh index ca5770cdb1a..868b2f5e27b 100644 --- a/src/core/actor/specfunc_cuda.hpp +++ b/src/core/electrostatics/specfunc.cuh @@ -39,13 +39,14 @@ /* Original Author: G. Jungman */ -/** \file +/** @file * This file contains implementations for the modified Bessel functions of * first and second kind. The implementations are based on the GSL code (see * the original GSL header above) and are duplicated from \ref specfunc.cpp. */ -#ifndef ESPRESSO_CORE_ACTOR_SPECFUNC_CUDA_HPP -#define ESPRESSO_CORE_ACTOR_SPECFUNC_CUDA_HPP + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_SPECFUNC_CUH +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_SPECFUNC_CUH #include "config.hpp" diff --git a/src/core/electrostatics_magnetostatics/specfunc.hpp b/src/core/electrostatics/specfunc.hpp similarity index 95% rename from src/core/electrostatics_magnetostatics/specfunc.hpp rename to src/core/electrostatics/specfunc.hpp index 3df2ae621b2..02ca459e44e 100644 --- a/src/core/electrostatics_magnetostatics/specfunc.hpp +++ b/src/core/electrostatics/specfunc.hpp @@ -18,22 +18,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file + +/** @file * This file contains implementations for some special functions which are * needed by the MMM family of algorithms. This are the modified Hurwitz zeta * function and the modified Bessel functions of first and second kind. The - * implementations are based on the GSL code (see \ref specfunc.cpp for the + * implementations are based on the GSL code (see @ref specfunc.cpp for the * original GSL header). * * The Hurwitz zeta function is evaluated using the Euler-Maclaurin summation * formula, the Bessel functions are evaluated using several different * Chebychev expansions. Both achieve a precision of nearly machine precision, * which is no problem for the Hurwitz zeta function, which is only used when - * determining the coefficients for the modified polygamma functions (see \ref + * determining the coefficients for the modified polygamma functions (see @ref * mmm-common.hpp). */ -#ifndef SPECFUNC_H -#define SPECFUNC_H + +#ifndef ESPRESSO_SRC_CORE_ELECTROSTATICS_SPECFUNC_HPP +#define ESPRESSO_SRC_CORE_ELECTROSTATICS_SPECFUNC_HPP #include diff --git a/src/core/electrostatics_magnetostatics/CMakeLists.txt b/src/core/electrostatics_magnetostatics/CMakeLists.txt deleted file mode 100644 index fcd3037dfbf..00000000000 --- a/src/core/electrostatics_magnetostatics/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -target_sources( - EspressoCore - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/common.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/debye_hueckel.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/elc.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/icc.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/magnetic_non_p3m_methods.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/mdlc_correction.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/mmm1d.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/mmm-modpsi.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/p3m-common.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/p3m_send_mesh.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/p3m.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/p3m-dipolar.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/p3m_gpu.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/scafacos.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/ScafacosContext.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/fft.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/coulomb.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/dipole.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/reaction_field.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/specfunc.cpp) diff --git a/src/core/electrostatics_magnetostatics/ScafacosContext.cpp b/src/core/electrostatics_magnetostatics/ScafacosContext.cpp deleted file mode 100644 index d927d34f503..00000000000 --- a/src/core/electrostatics_magnetostatics/ScafacosContext.cpp +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2010-2020 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** @file - * Provide a C-like interface for ScaFaCoS. - */ - -#include "config.hpp" - -#if defined(SCAFACOS) - -#include "electrostatics_magnetostatics/ScafacosContext.hpp" - -#include "cells.hpp" -#include "communication.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" -#include "electrostatics_magnetostatics/scafacos.hpp" -#include "grid.hpp" -#include "integrate.hpp" -#include "particle_data.hpp" -#include "tuning.hpp" - -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Scafacos { - -std::string ScafacosContext::get_method_and_parameters() { - std::string representation = get_method() + " " + get_parameters(); - std::replace(representation.begin(), representation.end(), ',', ' '); - return representation; -} - -void ScafacosContext::update_system_params() { - int per[3] = {box_geo.periodic(0) != 0, box_geo.periodic(1) != 0, - box_geo.periodic(2) != 0}; - - auto const n_part = boost::mpi::all_reduce( - comm_cart, cell_structure.local_particles().size(), std::plus<>()); - set_common_parameters(box_geo.length().data(), per, n_part); -} - -void ScafacosContextCoulomb::update_particle_data() { - positions.clear(); - charges.clear(); - - for (auto const &p : cell_structure.local_particles()) { - auto const pos = folded_position(p.r.p, box_geo); - positions.push_back(pos[0]); - positions.push_back(pos[1]); - positions.push_back(pos[2]); - charges.push_back(p.p.q); - } -} - -#ifdef SCAFACOS_DIPOLES -void ScafacosContextDipoles::update_particle_data() { - positions.clear(); - dipoles.clear(); - - for (auto const &p : cell_structure.local_particles()) { - auto const pos = folded_position(p.r.p, box_geo); - positions.push_back(pos[0]); - positions.push_back(pos[1]); - positions.push_back(pos[2]); - auto const dip = p.calc_dip(); - dipoles.push_back(dip[0]); - dipoles.push_back(dip[1]); - dipoles.push_back(dip[2]); - } -} -#endif - -void ScafacosContextCoulomb::update_particle_forces() const { - if (positions.empty()) - return; - - int it = 0; - for (auto &p : cell_structure.local_particles()) { - p.f.f += coulomb.prefactor * p.p.q * - Utils::Vector3d(Utils::Span(&(fields[it]), 3)); - it += 3; - } - - /* Check that the particle number did not change */ - assert(it == fields.size()); -} - -#ifdef SCAFACOS_DIPOLES -void ScafacosContextDipoles::update_particle_forces() const { - if (positions.empty()) - return; - - int it = 0; - for (auto &p : cell_structure.local_particles()) { - // Indices - // 3 "potential" values per particles (see below) - int const it_p = 3 * it; - // 6 "field" values per particles (see below) - int const it_f = 6 * it; - - // The scafacos term "potential" here in fact refers to the magnetic - // field. So, the torques are given by m \times B - auto const dip = p.calc_dip(); - auto const t = vector_product( - dip, - Utils::Vector3d(Utils::Span(&(potentials[it_p]), 3))); - // The force is given by G m, where G is a matrix - // which comes from the "fields" output of scafacos like this - // 0 1 2 - // 1 3 4 - // 2 4 5 - // where the numbers refer to indices in the "field" output from scafacos - auto const G = Utils::Matrix{ - {fields[it_f + 0], fields[it_f + 1], fields[it_f + 2]}, - {fields[it_f + 1], fields[it_f + 3], fields[it_f + 4]}, - {fields[it_f + 2], fields[it_f + 4], fields[it_f + 5]}}; - auto const f = G * dip; - - // Add to particles - p.f.f += dipole.prefactor * f; - p.f.torque += dipole.prefactor * t; - it++; - } - - /* Check that the particle number did not change */ - assert(it == positions.size() / 3); -} -#endif - -double ScafacosContextCoulomb::long_range_energy() { - update_particle_data(); - run(charges, positions, fields, potentials); - return 0.5 * coulomb.prefactor * - boost::inner_product(charges, potentials, 0.0); -} - -#ifdef SCAFACOS_DIPOLES -double ScafacosContextDipoles::long_range_energy() { - update_particle_data(); - run_dipolar(dipoles, positions, fields, potentials); - return -0.5 * dipole.prefactor * - boost::inner_product(dipoles, potentials, 0.0); -} -#endif - -static void set_r_cut_and_tune_local(double r_cut) { - set_r_cut_and_tune(r_cut); -} - -REGISTER_CALLBACK(set_r_cut_and_tune_local) - -/** Determine runtime for a specific cutoff */ -static double time_r_cut(double r_cut) { - /* Set cutoff to time */ - mpi_call_all(set_r_cut_and_tune_local, r_cut); - - return time_force_calc(10); -} - -/** Determine the optimal cutoff by bisection */ -static void tune_r_cut() { - const double tune_limit = 1e-3; - - auto const min_box_l = *boost::min_element(box_geo.length()); - auto const min_local_box_l = *boost::min_element(local_geo.length()); - - /* scafacos p3m and Ewald do not accept r_cut 0 for no good reason */ - double r_min = 1.0; - double r_max = std::min(min_local_box_l, min_box_l / 2.0) - skin; - double t_min = 0; - double t_max = std::numeric_limits::max(); - - /* Run bisection */ - while (std::fabs(r_min - r_max) > tune_limit) { - const double dr = 0.5 * (r_max - r_min); - const double t_mid = time_r_cut(r_min + dr); - t_min = time_r_cut(r_min); - t_max = time_r_cut(r_max); - - if (t_min <= 0.0 or t_max <= 0.0) { - break; - } - - if (t_mid > t_min) { - r_max = r_min += dr; - } else { - r_min += dr; - } - } -} - -void ScafacosContextCoulomb::tune() { - update_particle_data(); - - // Check whether we have to do a bisection for the short range cutoff - // Check if there is a user-supplied cutoff - if (has_near && r_cut() <= 0.0) { - // run tuning on the head node - if (this_node == 0) { - tune_r_cut(); - } - } else { - // ESPResSo is not affected by a short range cutoff -> tune in parallel - Scafacos::tune(charges, positions); - } -} - -void ScafacosContextCoulomb::set_r_cut_and_tune(double r_cut) { - update_particle_data(); - set_r_cut(r_cut); - Scafacos::tune(charges, positions); -} - -} // namespace Scafacos -#endif /* SCAFACOS */ diff --git a/src/core/electrostatics_magnetostatics/ScafacosContext.hpp b/src/core/electrostatics_magnetostatics/ScafacosContext.hpp deleted file mode 100644 index dd9293c7f2b..00000000000 --- a/src/core/electrostatics_magnetostatics/ScafacosContext.hpp +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2010-2020 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef SRC_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOSCONTEXT_HPP -#define SRC_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOSCONTEXT_HPP -/** - * @file - * @ref Scafacos::ScafacosContextBase implements the interface of the - * ScaFaCoS bridge. It is further derived for the coulombic and dipolar - * versions of ScaFaCoS. - */ - -#include "config.hpp" - -#if defined(SCAFACOS) - -#include "Scafacos.hpp" -#include "electrostatics_magnetostatics/ScafacosContextBase.hpp" - -#include - -#include -#include - -#if defined(SCAFACOS_DIPOLES) && !defined(FCS_ENABLE_DIPOLES) -#error \ - "SCAFACOS_DIPOLES requires dipoles support in scafacos library (FCS_ENABLE_DIPOLES)." -#endif - -namespace Scafacos { - -/** Encapsulation for the particle data needed by ScaFaCoS */ -struct ScafacosContext : ScafacosContextBase, Scafacos { - using Scafacos::Scafacos; - using ScafacosContextBase::ScafacosContextBase; - ~ScafacosContext() override = default; - void update_system_params() override; - double get_pair_force(double dist) const override { - return Scafacos::pair_force(dist); - } - double get_pair_energy(double dist) const override { - return Scafacos::pair_energy(dist); - } - std::string get_method_and_parameters() override; - double get_r_cut() const override { return Scafacos::r_cut(); } - -protected: - /** Outputs */ - std::vector fields, potentials; -}; - -struct ScafacosContextCoulomb : ScafacosContext { - using ScafacosContext::ScafacosContext; - void update_particle_data() override; - void update_particle_forces() const override; - double long_range_energy() override; - void add_long_range_force() override { - update_particle_data(); - run(charges, positions, fields, potentials); - update_particle_forces(); - } - void tune(); - void set_r_cut_and_tune(double r_cut); - -private: - /** Inputs */ - std::vector positions, charges; -}; - -#ifdef SCAFACOS_DIPOLES -struct ScafacosContextDipoles : ScafacosContext { - using ScafacosContext::ScafacosContext; - void update_particle_data() override; - void update_particle_forces() const override; - double long_range_energy() override; - void add_long_range_force() override { - update_particle_data(); - run_dipolar(dipoles, positions, fields, potentials); - update_particle_forces(); - } - -private: - /** Inputs */ - std::vector positions, dipoles; -}; -#endif - -} // namespace Scafacos -#endif // SCAFACOS -#endif // SRC_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOSCONTEXT_HPP diff --git a/src/core/electrostatics_magnetostatics/coulomb.cpp b/src/core/electrostatics_magnetostatics/coulomb.cpp deleted file mode 100644 index 2e955867a6e..00000000000 --- a/src/core/electrostatics_magnetostatics/coulomb.cpp +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "config.hpp" - -#ifdef ELECTROSTATICS - -#include "electrostatics_magnetostatics/coulomb.hpp" - -#include "ParticleRange.hpp" -#include "cells.hpp" -#include "communication.hpp" -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/debye_hueckel.hpp" -#include "electrostatics_magnetostatics/elc.hpp" -#include "electrostatics_magnetostatics/icc.hpp" -#include "electrostatics_magnetostatics/mmm1d.hpp" -#include "electrostatics_magnetostatics/p3m-common.hpp" -#include "electrostatics_magnetostatics/p3m.hpp" -#include "electrostatics_magnetostatics/p3m_gpu.hpp" -#include "electrostatics_magnetostatics/reaction_field.hpp" -#include "electrostatics_magnetostatics/scafacos.hpp" -#include "errorhandling.hpp" -#include "grid_based_algorithms/electrokinetics.hpp" -#include "integrate.hpp" -#include "npt.hpp" - -#include -#include - -#include - -#include -#include -#include -#include -#include - -Coulomb_parameters coulomb; - -namespace Coulomb { -Utils::Vector9d calc_pressure_long_range(const ParticleRange &particles) { - switch (coulomb.method) { -#ifdef P3M - case COULOMB_ELC_P3M: - fprintf(stderr, - "WARNING: pressure calculated, but ELC pressure not implemented\n"); - break; - case COULOMB_P3M_GPU: - fprintf( - stderr, - "WARNING: pressure calculated, but GPU P3M pressure not implemented\n"); - break; - case COULOMB_P3M: { - p3m_charge_assign(particles); - return p3m_calc_kspace_pressure_tensor(); - } -#endif - case COULOMB_MMM1D: - case COULOMB_MMM1D_GPU: - fprintf( - stderr, - "WARNING: pressure calculated, but MMM1D pressure not implemented\n"); - break; - default: - break; - } - return {}; -} - -bool sanity_checks() { - bool failed = false; - switch (coulomb.method) { - case COULOMB_MMM1D: - failed |= MMM1D_sanity_checks(); - break; -#ifdef P3M - case COULOMB_ELC_P3M: - try { - ELC_sanity_checks(elc_params); - } catch (std::runtime_error const &err) { - runtimeErrorMsg() << err.what(); - failed = true; - } - // fall through - case COULOMB_P3M_GPU: - case COULOMB_P3M: - failed |= p3m_sanity_checks(); - break; -#endif - default: - break; - } - return failed; -} - -double cutoff(const Utils::Vector3d &box_l) { - switch (coulomb.method) { - case COULOMB_MMM1D: - return std::numeric_limits::infinity(); -#ifdef P3M - case COULOMB_ELC_P3M: - return std::max(elc_params.space_layer, p3m.params.r_cut_iL * box_l[0]); - case COULOMB_P3M_GPU: - case COULOMB_P3M: - /* do not use precalculated r_cut here, might not be set yet */ - return p3m.params.r_cut_iL * box_l[0]; -#endif - case COULOMB_DH: - return dh_params.r_cut; - case COULOMB_RF: - return rf_params.r_cut; -#ifdef SCAFACOS - case COULOMB_SCAFACOS: - return Scafacos::fcs_coulomb()->get_r_cut(); -#endif - default: - return -1.0; - } -} - -void deactivate() { - switch (coulomb.method) { -#ifdef P3M - case COULOMB_ELC_P3M: - case COULOMB_P3M_GPU: - case COULOMB_P3M: - break; -#endif - case COULOMB_DH: - dh_params = {}; - break; - case COULOMB_RF: - rf_params = {}; - break; - case COULOMB_MMM1D: - mmm1d_params.maxPWerror = 1e40; - break; - default: - break; - } -} - -void update_dependent_particles() { - icc_iteration(cell_structure, cell_structure.local_particles(), - cell_structure.ghost_particles()); -} - -void on_observable_calc() { - switch (coulomb.method) { -#ifdef P3M - case COULOMB_ELC_P3M: - case COULOMB_P3M_GPU: - case COULOMB_P3M: - p3m_count_charged_particles(); - break; -#endif - default: - break; - } -} - -void on_coulomb_change() { - switch (coulomb.method) { - case COULOMB_DH: - break; -#ifdef P3M -#ifdef CUDA - case COULOMB_P3M_GPU: - if (this_node == 0) { - try { - p3m_gpu_init(p3m.params.cao, p3m.params.mesh, p3m.params.alpha); - } catch (std::runtime_error const &err) { - runtimeErrorMsg() << err.what(); - } - } - break; -#endif - case COULOMB_ELC_P3M: - ELC_init(); - // fall through - case COULOMB_P3M: - p3m_init(); - break; -#endif - case COULOMB_MMM1D: - MMM1D_init(); - break; - default: - break; - } -} - -void on_boxl_change() { - switch (coulomb.method) { -#ifdef P3M - case COULOMB_ELC_P3M: - ELC_init(); - // fall through - case COULOMB_P3M_GPU: - case COULOMB_P3M: - p3m_scaleby_box_l(); - break; -#endif - case COULOMB_MMM1D: - MMM1D_init(); - break; -#ifdef SCAFACOS - case COULOMB_SCAFACOS: - Scafacos::fcs_coulomb()->update_system_params(); - break; -#endif - default: - break; - } -} - -void init() { - switch (coulomb.method) { - case COULOMB_DH: - break; -#ifdef P3M - case COULOMB_ELC_P3M: - ELC_init(); - // fall through - case COULOMB_P3M: - p3m_init(); - break; - case COULOMB_P3M_GPU: - break; -#endif - case COULOMB_MMM1D: - MMM1D_init(); - break; - default: - break; - } -} - -void calc_long_range_force(const ParticleRange &particles) { - switch (coulomb.method) { -#ifdef P3M - case COULOMB_ELC_P3M: - if (elc_params.dielectric_contrast_on) { - ELC_P3M_modify_p3m_sums_both(particles); - ELC_p3m_charge_assign_both(particles); - ELC_P3M_self_forces(particles); - } else - p3m_charge_assign(particles); - - p3m_calc_kspace_forces(true, false, particles); - - if (elc_params.dielectric_contrast_on) - ELC_P3M_restore_p3m_sums(particles); - - ELC_add_force(particles); - - break; -#endif -#ifdef CUDA - case COULOMB_P3M_GPU: - if (this_node == 0) { - p3m_gpu_add_farfield_force(); - } - /* there is no NPT handling here as long as we cannot compute energies. - This is checked in integrator_npt_sanity_checks() when integration - starts. */ - break; -#endif -#ifdef P3M - case COULOMB_P3M: - p3m_charge_assign(particles); -#ifdef NPT - if (integ_switch == INTEG_METHOD_NPT_ISO) { - auto const energy = p3m_calc_kspace_forces(true, true, particles); - npt_add_virial_contribution(energy); - } else -#endif - p3m_calc_kspace_forces(true, false, particles); - break; -#endif -#ifdef SCAFACOS - case COULOMB_SCAFACOS: - Scafacos::fcs_coulomb()->add_long_range_force(); - break; -#endif - default: - break; - } - - /* Add fields from EK if enabled */ -#ifdef ELECTROKINETICS - if (this_node == 0) { - ek_calculate_electrostatic_coupling(); - } -#endif -} - -double calc_energy_long_range(const ParticleRange &particles) { - double energy = 0.0; - switch (coulomb.method) { -#ifdef P3M - case COULOMB_P3M_GPU: - runtimeWarningMsg() - << "long range energy calculation not implemented for GPU P3M"; - break; - case COULOMB_P3M: - p3m_charge_assign(particles); - energy = p3m_calc_kspace_forces(false, true, particles); - break; - case COULOMB_ELC_P3M: - // assign the original charges first - // they may not have been assigned yet - p3m_charge_assign(particles); - if (!elc_params.dielectric_contrast_on) - energy = p3m_calc_kspace_forces(false, true, particles); - else { - energy = 0.5 * p3m_calc_kspace_forces(false, true, particles); - energy += 0.5 * coulomb.prefactor * - ELC_P3M_dielectric_layers_energy_self(particles); - - // assign both original and image charges now - ELC_p3m_charge_assign_both(particles); - ELC_P3M_modify_p3m_sums_both(particles); - - energy += 0.5 * p3m_calc_kspace_forces(false, true, particles); - - // assign only the image charges now - ELC_p3m_charge_assign_image(particles); - ELC_P3M_modify_p3m_sums_image(particles); - - energy -= 0.5 * p3m_calc_kspace_forces(false, true, particles); - - // restore modified sums - ELC_P3M_restore_p3m_sums(particles); - } - energy += ELC_energy(particles); - break; -#endif -#ifdef SCAFACOS - case COULOMB_SCAFACOS: - energy += Scafacos::fcs_coulomb()->long_range_energy(); - break; -#endif - default: - break; - } - return energy; -} - -int icc_sanity_check() { - switch (coulomb.method) { -#ifdef P3M - case COULOMB_ELC_P3M: { - if (elc_params.dielectric_contrast_on) { - runtimeErrorMsg() << "ICC conflicts with ELC dielectric contrast"; - return 1; - } - break; - } -#endif - case COULOMB_DH: { - runtimeErrorMsg() << "ICC does not work with Debye-Hueckel."; - return 1; - } - case COULOMB_RF: { - runtimeErrorMsg() << "ICC does not work with COULOMB_RF."; - return 1; - } - default: - break; - } - -#ifdef NPT - if (integ_switch == INTEG_METHOD_NPT_ISO) { - runtimeErrorMsg() << "ICC does not work in the NPT ensemble"; - return 1; - } -#endif - - return 0; -} - -void bcast_coulomb_params() { - switch (coulomb.method) { - case COULOMB_NONE: - case COULOMB_SCAFACOS: - break; -#ifdef P3M - case COULOMB_ELC_P3M: - MPI_Bcast(&elc_params, sizeof(ELCParameters), MPI_BYTE, 0, comm_cart); - // fall through - case COULOMB_P3M_GPU: - case COULOMB_P3M: - MPI_Bcast(&p3m.params, sizeof(P3MParameters), MPI_BYTE, 0, comm_cart); - break; -#endif - case COULOMB_DH: - MPI_Bcast(&dh_params, sizeof(DebyeHueckelParameters), MPI_BYTE, 0, - comm_cart); - break; - case COULOMB_MMM1D: - case COULOMB_MMM1D_GPU: - MPI_Bcast(&mmm1d_params, sizeof(MMM1DParameters), MPI_BYTE, 0, comm_cart); - break; - case COULOMB_RF: - MPI_Bcast(&rf_params, sizeof(ReactionFieldParameters), MPI_BYTE, 0, - comm_cart); - break; - default: - break; - } -} - -void set_prefactor(double prefactor) { - if (prefactor < 0.0) { - throw std::domain_error("Coulomb prefactor has to be >= 0"); - } - - coulomb.prefactor = prefactor; - mpi_bcast_coulomb_params(); -} - -void deactivate_method() { - coulomb.prefactor = 0; - - Coulomb::deactivate(); - - mpi_bcast_coulomb_params(); - coulomb.method = COULOMB_NONE; - mpi_bcast_coulomb_params(); -} -} // namespace Coulomb -#endif // ELECTROSTATICS diff --git a/src/core/electrostatics_magnetostatics/coulomb.hpp b/src/core/electrostatics_magnetostatics/coulomb.hpp deleted file mode 100644 index afd99bccaa6..00000000000 --- a/src/core/electrostatics_magnetostatics/coulomb.hpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef ESPRESSO_COULOMB_HPP -#define ESPRESSO_COULOMB_HPP - -#include "config.hpp" - -#include - -#ifdef ELECTROSTATICS - -#include "ParticleRange.hpp" - -#include - -/** Type codes for the type of %Coulomb interaction. - * Enumeration of implemented methods for the electrostatic interaction. - */ -enum CoulombMethod { - COULOMB_NONE, ///< %Coulomb interaction switched off - COULOMB_DH, ///< %Coulomb method is Debye-Hueckel - COULOMB_P3M, ///< %Coulomb method is P3M - COULOMB_P3M_GPU, ///< %Coulomb method is P3M with GPU-based long-range part - COULOMB_ELC_P3M, ///< %Coulomb method is P3M plus ELC - COULOMB_MMM1D, ///< %Coulomb method is one-dimensional MMM - COULOMB_RF, ///< %Coulomb method is Reaction-Field - COULOMB_MMM1D_GPU, ///< %Coulomb method is one-dimensional MMM running on GPU - COULOMB_SCAFACOS, ///< %Coulomb method is ScaFaCoS -}; - -/** Interaction parameters for the %Coulomb interaction. */ -struct Coulomb_parameters { - /** Bjerrum length times temperature. */ - double prefactor = 0.; - - double field_induced = 0.; - double field_applied = 0.; - - /** Method to treat %Coulomb interaction. */ - CoulombMethod method = COULOMB_NONE; -}; - -/** Structure containing the %Coulomb parameters. */ -extern Coulomb_parameters coulomb; - -namespace Coulomb { -Utils::Vector9d calc_pressure_long_range(const ParticleRange &particles); - -bool sanity_checks(); -double cutoff(const Utils::Vector3d &box_l); -void deactivate(); - -void on_observable_calc(); -void on_coulomb_change(); -void on_boxl_change(); -void init(); - -void calc_long_range_force(const ParticleRange &particles); - -double calc_energy_long_range(const ParticleRange &particles); - -int icc_sanity_check(); - -void bcast_coulomb_params(); - -/** @brief Set the electrostatics prefactor */ -void set_prefactor(double prefactor); - -/** @brief Deactivates the current %Coulomb method. */ -void deactivate_method(); - -/** @brief Update particles with properties depending on other particles, - * namely ICC charges. - */ -void update_dependent_particles(); -} // namespace Coulomb -#else // ELECTROSTATICS -namespace Coulomb { -constexpr std::size_t pressure_n() { return 0; } -constexpr std::size_t energy_n() { return 0; } -} // namespace Coulomb -#endif // ELECTROSTATICS -#endif // ESPRESSO_COULOMB_HPP diff --git a/src/core/electrostatics_magnetostatics/coulomb_inline.hpp b/src/core/electrostatics_magnetostatics/coulomb_inline.hpp deleted file mode 100644 index d62e6e699ac..00000000000 --- a/src/core/electrostatics_magnetostatics/coulomb_inline.hpp +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef ESPRESSO_COULOMB_INLINE_HPP -#define ESPRESSO_COULOMB_INLINE_HPP - -#include "config.hpp" - -#ifdef ELECTROSTATICS - -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/debye_hueckel.hpp" -#include "electrostatics_magnetostatics/elc.hpp" -#include "electrostatics_magnetostatics/mmm1d.hpp" -#include "electrostatics_magnetostatics/p3m.hpp" -#include "electrostatics_magnetostatics/reaction_field.hpp" -#include "electrostatics_magnetostatics/scafacos.hpp" - -#include -#include -#include - -#include -#include - -namespace Coulomb { -inline Utils::Vector3d central_force(double const q1q2, - Utils::Vector3d const &d, double dist) { - Utils::Vector3d f{}; - - switch (coulomb.method) { -#ifdef P3M - case COULOMB_P3M_GPU: - case COULOMB_P3M: - case COULOMB_ELC_P3M: - p3m_add_pair_force(q1q2, d, dist, f); - break; -#endif - case COULOMB_MMM1D: - add_mmm1d_coulomb_pair_force(q1q2, d, dist, f); - break; - case COULOMB_DH: - add_dh_coulomb_pair_force(q1q2, d, dist, f); - break; - case COULOMB_RF: - add_rf_coulomb_pair_force(q1q2, d, dist, f); - break; -#ifdef SCAFACOS - case COULOMB_SCAFACOS: - Scafacos::fcs_coulomb()->add_pair_force(q1q2, d, dist, f); - break; -#endif - default: - break; - } - - return coulomb.prefactor * f; -} - -inline std::tuple -pair_force(Particle const &p1, Particle const &p2, Utils::Vector3d const &d, - double dist) { - auto const q1q2 = p1.p.q * p2.p.q; - - if (q1q2 == 0) { - return {}; - } - - auto const force = central_force(q1q2, d, dist); - -#ifdef P3M - if ((coulomb.method == COULOMB_ELC_P3M) && - (elc_params.dielectric_contrast_on)) { - - auto const f1 = - coulomb.prefactor * - ELC_P3M_dielectric_layers_force_contribution(p2.r.p, p1.r.p, q1q2); - auto const f2 = - coulomb.prefactor * - ELC_P3M_dielectric_layers_force_contribution(p1.r.p, p2.r.p, q1q2); - - return {force, f1, f2}; - } -#endif - - return {force, {}, {}}; -} - -/** - * @brief Pair contribution to the pressure tensor. - * - * If supported by the method, this returns the virial - * contribution to the pressure tensor for this pair. - * - * @param p1 particle - * @param p2 particle - * @param d distance vector - * @param dist distance norm - * @return Contribution to the pressure tensor. - */ -inline Utils::Matrix pair_pressure(Particle const &p1, - Particle const &p2, - Utils::Vector3d const &d, - double dist) { - switch (coulomb.method) { - case COULOMB_NONE: - break; -#ifdef P3M - case COULOMB_P3M_GPU: - case COULOMB_P3M: -#endif - case COULOMB_MMM1D: - case COULOMB_DH: - case COULOMB_RF: { - auto const force = central_force(p1.p.q * p2.p.q, d, dist); - - return Utils::tensor_product(force, d); - } - default: - fprintf(stderr, "calculating pressure for electrostatics method that " - "doesn't have it implemented\n"); - break; - } - - return {}; -} - -// energy_inline -inline double pair_energy(Particle const &p1, Particle const &p2, - double const q1q2, Utils::Vector3d const &d, - double dist) { - /* real space Coulomb */ - auto E = [&]() { - switch (coulomb.method) { -#ifdef P3M - case COULOMB_P3M_GPU: - case COULOMB_P3M: - return p3m_pair_energy(q1q2, dist); - case COULOMB_ELC_P3M: - if (elc_params.dielectric_contrast_on) { - return 0.5 * ELC_P3M_dielectric_layers_energy_contribution(p1, p2) + - p3m_pair_energy(q1q2, dist); - } else { - return p3m_pair_energy(q1q2, dist); - } -#endif -#ifdef SCAFACOS - case COULOMB_SCAFACOS: - return Scafacos::fcs_coulomb()->pair_energy(q1q2, dist); -#endif - case COULOMB_DH: - return dh_coulomb_pair_energy(q1q2, dist); - case COULOMB_RF: - return rf_coulomb_pair_energy(q1q2, dist); - case COULOMB_MMM1D: - return mmm1d_coulomb_pair_energy(q1q2, d, dist); - default: - return 0.; - } - }(); - return coulomb.prefactor * E; -} -} // namespace Coulomb - -#endif -#endif // ESPRESSO_COULOMB_INLINE_HPP diff --git a/src/core/electrostatics_magnetostatics/debye_hueckel.hpp b/src/core/electrostatics_magnetostatics/debye_hueckel.hpp deleted file mode 100644 index b5480fafeb4..00000000000 --- a/src/core/electrostatics_magnetostatics/debye_hueckel.hpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef DEBYE_HUECKEL_H -#define DEBYE_HUECKEL_H -/** \file - * Routines to calculate the Debye-Hückel energy and force - * for a particle pair. - */ -#include "config.hpp" - -#ifdef ELECTROSTATICS - -#include - -#include - -/** Debye-Hückel parameters. */ -struct DebyeHueckelParameters { - /** Interaction cutoff. */ - double r_cut; - /** Ionic strength. */ - double kappa; -}; - -/** Global state of the Debye-Hückel method. */ -extern DebyeHueckelParameters dh_params; - -void dh_set_params(double kappa, double r_cut); - -/** Compute the Debye-Hueckel pair force. - * @param[in] q1q2 Product of the charges on p1 and p2. - * @param[in] d Vector pointing from p1 to p2. - * @param[in] dist Distance between p1 and p2. - * @param[out] force Calculated force on p1. - */ -inline void add_dh_coulomb_pair_force(double const q1q2, - Utils::Vector3d const &d, - double const dist, - Utils::Vector3d &force) { - if (dist < dh_params.r_cut) { - // pure Coulomb case - auto fac = q1q2 / (dist * dist * dist); - if (dh_params.kappa > 0.0) { - // Debye-Hueckel case - auto const kappa_dist = dh_params.kappa * dist; - fac *= exp(-kappa_dist) * (1.0 + kappa_dist); - } - force += fac * d; - } -} - -/** Compute the Debye-Hueckel pair energy. - * @param q1q2 Product of the charges on p1 and p2. - * @param dist Distance between p1 and p2. - */ -inline double dh_coulomb_pair_energy(double const q1q2, double const dist) { - if (dist < dh_params.r_cut) { - if (dh_params.kappa > 0.0) - return q1q2 * exp(-dh_params.kappa * dist) / dist; - - return q1q2 / dist; - } - return 0.0; -} -#endif - -#endif diff --git a/src/core/electrostatics_magnetostatics/dipole.cpp b/src/core/electrostatics_magnetostatics/dipole.cpp deleted file mode 100644 index 1e98a62146a..00000000000 --- a/src/core/electrostatics_magnetostatics/dipole.cpp +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "config.hpp" - -#ifdef DIPOLES - -#include "electrostatics_magnetostatics/dipole.hpp" - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp" -#include "electrostatics_magnetostatics/mdlc_correction.hpp" -#include "electrostatics_magnetostatics/p3m-common.hpp" -#include "electrostatics_magnetostatics/p3m-dipolar.hpp" -#include "electrostatics_magnetostatics/scafacos.hpp" - -#include "ParticleRange.hpp" -#include "errorhandling.hpp" -#include "grid.hpp" -#include "integrate.hpp" -#include "npt.hpp" - -#include -#include - -#include - -#include -#include -#include - -Dipole_parameters dipole = { - 0.0, - DIPOLAR_NONE, -}; - -namespace Dipole { -void calc_pressure_long_range() { - switch (dipole.method) { - case DIPOLAR_NONE: - return; - default: - runtimeWarningMsg() - << "WARNING: pressure calculated, but pressure not implemented.\n"; - return; - } -} - -bool sanity_checks() { - bool failed = false; -#ifdef DP3M - try { - switch (dipole.method) { - case DIPOLAR_MDLC_P3M: - mdlc_sanity_checks(); - // fall through - case DIPOLAR_P3M: - failed |= dp3m_sanity_checks(node_grid); - break; - case DIPOLAR_MDLC_DS: - mdlc_sanity_checks(); - // fall through - case DIPOLAR_DS: - mdds_sanity_checks(); - break; - default: - break; - } - } catch (std::runtime_error const &err) { - runtimeErrorMsg() << err.what(); - failed = true; - } -#endif - return failed; -} - -double cutoff(const Utils::Vector3d &box_l) { - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - // fall through - case DIPOLAR_P3M: { - /* do not use precalculated r_cut here, might not be set yet */ - return dp3m.params.r_cut_iL * box_l[0]; - } -#endif /*ifdef DP3M */ - default: - return -1.; - } -} - -void on_observable_calc() { - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - // fall through - case DIPOLAR_P3M: - dp3m_count_magnetic_particles(); - break; -#endif - default: - break; - } -} - -void on_coulomb_change() { - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - // fall through - case DIPOLAR_P3M: - dp3m_init(); - break; -#endif - default: - break; - } -} - -void on_boxl_change() { - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - // fall through - case DIPOLAR_P3M: - dp3m_scaleby_box_l(); - break; -#endif -#ifdef SCAFACOS_DIPOLES - case DIPOLAR_SCAFACOS: - Scafacos::fcs_dipoles()->update_system_params(); - break; -#endif - default: - break; - } -} - -void init() { - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - // fall through - case DIPOLAR_P3M: - dp3m_init(); - break; -#endif - default: - break; - } -} - -void calc_long_range_force(const ParticleRange &particles) { - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - add_mdlc_force_corrections(particles); - // fall through - case DIPOLAR_P3M: - dp3m_dipole_assign(particles); -#ifdef NPT - if (integ_switch == INTEG_METHOD_NPT_ISO) { - auto const energy = dp3m_calc_kspace_forces(true, true, particles); - npt_add_virial_contribution(energy); - fprintf(stderr, "dipolar_P3M at this moment is added to p_vir[0]\n"); - } else -#endif - dp3m_calc_kspace_forces(true, false, particles); - - break; -#endif - case DIPOLAR_ALL_WITH_ALL_AND_NO_REPLICA: - dawaanr_calculations(true, false, particles); - break; -#ifdef DP3M - case DIPOLAR_MDLC_DS: - add_mdlc_force_corrections(particles); - // fall through -#endif - case DIPOLAR_DS: - magnetic_dipolar_direct_sum_calculations(true, false, particles); - break; - case DIPOLAR_DS_GPU: // NOLINT(bugprone-branch-clone) - // do nothing: it's an actor - break; -#ifdef DIPOLAR_BARNES_HUT - case DIPOLAR_BH_GPU: - // do nothing: it's an actor - break; -#endif -#ifdef SCAFACOS_DIPOLES - case DIPOLAR_SCAFACOS: - Scafacos::fcs_dipoles()->add_long_range_force(); -#endif - case DIPOLAR_NONE: - break; - default: - runtimeErrorMsg() << "unknown dipolar method"; - break; - } -} - -double calc_energy_long_range(const ParticleRange &particles) { - double energy = 0.; - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_P3M: - dp3m_dipole_assign(particles); - energy = dp3m_calc_kspace_forces(false, true, particles); - break; - case DIPOLAR_MDLC_P3M: - dp3m_dipole_assign(particles); - energy = dp3m_calc_kspace_forces(false, true, particles); - energy += add_mdlc_energy_corrections(particles); - break; -#endif - case DIPOLAR_ALL_WITH_ALL_AND_NO_REPLICA: - energy = dawaanr_calculations(false, true, particles); - break; -#ifdef DP3M - case DIPOLAR_MDLC_DS: - energy = magnetic_dipolar_direct_sum_calculations(false, true, particles); - energy += add_mdlc_energy_corrections(particles); - break; -#endif - case DIPOLAR_DS: - energy = magnetic_dipolar_direct_sum_calculations(false, true, particles); - break; - case DIPOLAR_DS_GPU: // NOLINT(bugprone-branch-clone) - // do nothing: it's an actor - break; -#ifdef DIPOLAR_BARNES_HUT - case DIPOLAR_BH_GPU: - // do nothing: it's an actor. - break; -#endif -#ifdef SCAFACOS_DIPOLES - case DIPOLAR_SCAFACOS: - energy = Scafacos::fcs_dipoles()->long_range_energy(); - break; -#endif - case DIPOLAR_NONE: - break; - default: - runtimeErrorMsg() - << "energy calculation not implemented for dipolar method."; - break; - } - return energy; -} - -void bcast_params(const boost::mpi::communicator &comm) { - namespace mpi = boost::mpi; - - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - mpi::broadcast(comm, dlc_params, 0); - // fall through - case DIPOLAR_P3M: - mpi::broadcast(comm, dp3m.params, 0); - break; -#endif - default: - break; - } -} - -void set_Dprefactor(double prefactor) { - if (prefactor < 0.0) { - throw std::invalid_argument("Dipolar prefactor has to be >= 0"); - } - dipole.prefactor = prefactor; - mpi_bcast_coulomb_params(); -} - -double get_Dprefactor() { return dipole.prefactor; } - -void set_method_local(DipolarInteraction method) { dipole.method = method; } - -void disable_method_local() { dipole.method = DIPOLAR_NONE; } - -} // namespace Dipole -#endif // DIPOLES diff --git a/src/core/electrostatics_magnetostatics/dipole.hpp b/src/core/electrostatics_magnetostatics/dipole.hpp deleted file mode 100644 index a6cdb49ebf9..00000000000 --- a/src/core/electrostatics_magnetostatics/dipole.hpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef ESPRESSO_DIPOLE_HPP -#define ESPRESSO_DIPOLE_HPP - -#include "config.hpp" - -#include - -#ifdef DIPOLES - -#include "ParticleRange.hpp" - -#include - -#include - -/** Type codes for the type of dipolar interaction. - * Enumeration of implemented methods for the magnetostatic interaction. - */ -enum DipolarInteraction { - /** Dipolar interaction switched off. */ - DIPOLAR_NONE = 0, - /** Dipolar method is P3M. */ - DIPOLAR_P3M, - /** Dipolar method is P3M plus DLC. */ - DIPOLAR_MDLC_P3M, - /** Dipolar method is all with all and no replicas. */ - DIPOLAR_ALL_WITH_ALL_AND_NO_REPLICA, - /** Dipolar method is magnetic dipolar direct summation. */ - DIPOLAR_DS, - /** Dipolar method is direct summation plus DLC. */ - DIPOLAR_MDLC_DS, - /** Dipolar method is direct summation on GPU. */ - DIPOLAR_DS_GPU, -#ifdef DIPOLAR_BARNES_HUT - /** Dipolar method is direct summation on GPU by Barnes-Hut algorithm. */ - DIPOLAR_BH_GPU, -#endif - /** Dipolar method is ScaFaCoS. */ - DIPOLAR_SCAFACOS -}; - -/** Interaction parameters for the %dipole interaction. */ -struct Dipole_parameters { - double prefactor; - - DipolarInteraction method; -}; - -/** Structure containing the %dipole parameters. */ -extern Dipole_parameters dipole; - -namespace Dipole { -void calc_pressure_long_range(); - -bool sanity_checks(); -double cutoff(const Utils::Vector3d &box_l); - -void on_observable_calc(); -void on_coulomb_change(); -void on_boxl_change(); -void init(); - -void calc_long_range_force(const ParticleRange &particles); - -double calc_energy_long_range(const ParticleRange &particles); - -void bcast_params(const boost::mpi::communicator &comm); - -/** @brief Set the dipolar prefactor */ -void set_Dprefactor(double prefactor); -/** @brief Get the dipolar prefactor */ -double get_Dprefactor(); - -void set_method_local(DipolarInteraction method); -void disable_method_local(); -} // namespace Dipole -#else // DIPOLES -namespace Dipole { -constexpr std::size_t pressure_n() { return 0; } -constexpr std::size_t energy_n() { return 0; } -} // namespace Dipole -#endif // DIPOLES -#endif // ESPRESSO_DIPOLE_HPP diff --git a/src/core/electrostatics_magnetostatics/dipole_inline.hpp b/src/core/electrostatics_magnetostatics/dipole_inline.hpp deleted file mode 100644 index 20ea9dcb0f6..00000000000 --- a/src/core/electrostatics_magnetostatics/dipole_inline.hpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef ESPRESSO_DIPOLE_INLINE_HPP -#define ESPRESSO_DIPOLE_INLINE_HPP - -#include "config.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" -#include "electrostatics_magnetostatics/p3m-dipolar.hpp" - -namespace Dipole { -// forces_inline -inline ParticleForce pair_force(Particle const &p1, Particle const &p2, - Utils::Vector3d const &d, double dist, - double dist2) { - ParticleForce pf{}; - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - // fall trough - case DIPOLAR_P3M: { - pf = dp3m_pair_force(p1, p2, d, dist2, dist); - break; - } -#endif /*ifdef DP3M */ - default: - break; - } - return pf; -} - -// energy_inline -inline double pair_energy(Particle const &p1, Particle const &p2, - Utils::Vector3d const &d, double dist, double dist2) { - double energy = 0; - if (dipole.method != DIPOLAR_NONE) { - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - // fall trough - case DIPOLAR_P3M: - energy = dp3m_pair_energy(p1, p2, d, dist2, dist); - break; -#endif - default: - energy = 0; - } - } - return energy; -} - -} // namespace Dipole - -#endif // ESPRESSO_DIPOLE_INLINE_HPP diff --git a/src/core/electrostatics_magnetostatics/elc.cpp b/src/core/electrostatics_magnetostatics/elc.cpp deleted file mode 100644 index 36b2cd0cd9d..00000000000 --- a/src/core/electrostatics_magnetostatics/elc.cpp +++ /dev/null @@ -1,1369 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - * Implementation of \ref elc.hpp. - */ - -#include "config.hpp" - -#ifdef P3M - -#include "electrostatics_magnetostatics/elc.hpp" - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/mmm-common.hpp" -#include "electrostatics_magnetostatics/p3m.hpp" - -#include "Particle.hpp" -#include "ParticleRange.hpp" -#include "communication.hpp" -#include "errorhandling.hpp" -#include "grid.hpp" - -#include -#include -#include - -#include - -#include -#include -#include -#include - -ELCParameters elc_params = {1e100, 10, 1, 0, true, true, false, 1, - 1, false, 0, 0, 0, 0, 0.0}; - -/** \name Product decomposition data organization - * For the cell blocks it is assumed that the lower blocks part is in the - * lower half. This has to have positive sign, so that has to be first. - */ -/**@{*/ -#define POQESP 0 -#define POQECP 1 -#define POQESM 2 -#define POQECM 3 - -#define PQESSP 0 -#define PQESCP 1 -#define PQECSP 2 -#define PQECCP 3 -#define PQESSM 4 -#define PQESCM 5 -#define PQECSM 6 -#define PQECCM 7 -/**@}*/ - -/** ELC axes (x and y directions)*/ -enum class PoQ : int { P, Q }; - -/** temporary buffers for product decomposition */ -static std::vector partblk; -/** collected data from the other cells */ -static double gblcblk[8]; - -/** structure for caching sin and cos values */ -struct SCCache { - double s, c; -}; - -/** Cached sin/cos values along the x-axis and y-axis */ -/**@{*/ -static std::vector scxcache; -static std::vector scycache; -/**@}*/ - -/**************************************** - * LOCAL FUNCTIONS - ****************************************/ - -static void distribute(std::size_t size); -static void add_dipole_force(const ParticleRange &particles); -static double dipole_energy(const ParticleRange &particles); -static double z_energy(const ParticleRange &particles); -static void add_z_force(const ParticleRange &particles); - -/** - * @brief Calculate cached sin/cos values for one direction. - * - * @tparam dir Index of the dimension to consider (e.g. 0 for x ...). - * - * @param particles Particle to calculate values for - * @param n_freq Number of frequencies to calculate per particle - * @param u Inverse box length - * @return Calculated values. - */ -template -static std::vector calc_sc_cache(const ParticleRange &particles, - std::size_t n_freq, double u) { - constexpr double c_2pi = 2 * Utils::pi(); - auto const n_part = particles.size(); - std::vector ret(n_freq * n_part); - - for (std::size_t freq = 1; freq <= n_freq; freq++) { - auto const pref = c_2pi * u * static_cast(freq); - - std::size_t o = (freq - 1) * n_part; - for (auto const &part : particles) { - auto const arg = pref * part.r.p[dir]; - ret[o++] = {sin(arg), cos(arg)}; - } - } - - return ret; -} - -static void prepare_sc_cache(const ParticleRange &particles, - std::size_t n_freq_x, double u_x, - std::size_t n_freq_y, double u_y) { - scxcache = calc_sc_cache<0>(particles, n_freq_x, u_x); - scycache = calc_sc_cache<1>(particles, n_freq_y, u_y); -} - -/*****************************************************************/ -/* data distribution */ -/*****************************************************************/ - -inline void clear_vec(double *pdc, std::size_t size) { - for (std::size_t i = 0; i < size; i++) - pdc[i] = 0; -} - -inline void copy_vec(double *pdc_d, double const *pdc_s, std::size_t size) { - for (std::size_t i = 0; i < size; i++) - pdc_d[i] = pdc_s[i]; -} - -inline void add_vec(double *pdc_d, double const *pdc_s1, double const *pdc_s2, - std::size_t size) { - for (std::size_t i = 0; i < size; i++) - pdc_d[i] = pdc_s1[i] + pdc_s2[i]; -} - -inline void addscale_vec(double *pdc_d, double scale, double const *pdc_s1, - double const *pdc_s2, std::size_t size) { - for (std::size_t i = 0; i < size; i++) - pdc_d[i] = scale * pdc_s1[i] + pdc_s2[i]; -} - -inline void scale_vec(double scale, double *pdc, std::size_t size) { - for (std::size_t i = 0; i < size; i++) - pdc[i] *= scale; -} - -inline double *block(double *p, std::size_t index, std::size_t size) { - return &p[index * size]; -} - -void distribute(std::size_t size) { - assert(size <= 8); - double send_buf[8]; - copy_vec(send_buf, gblcblk, size); - MPI_Allreduce(send_buf, gblcblk, static_cast(size), MPI_DOUBLE, MPI_SUM, - comm_cart); -} - -/** Checks if a charged particle is in the forbidden gap region - */ -inline void check_gap_elc(const Particle &p) { - if (p.p.q != 0) { - auto const z = p.r.p[2]; - if (z < 0. or z > elc_params.h) { - runtimeErrorMsg() << "Particle " << p.p.identity << " entered ELC gap " - << "region by " << ((z < 0.) ? z : z - elc_params.h); - } - } -} - -/*****************************************************************/ -/* dipole terms */ -/*****************************************************************/ - -/** Calculate the dipole force. - * See @cite yeh99a. - */ -static void add_dipole_force(const ParticleRange &particles) { - double const pref = coulomb.prefactor * 4 * Utils::pi() * - box_geo.length_inv()[0] * box_geo.length_inv()[1] * - box_geo.length_inv()[2]; - constexpr std::size_t size = 3; - - auto local_particles = particles; - - /* for nonneutral systems, this shift gives the background contribution - (rsp. for this shift, the DM of the background is zero) */ - double const shift = box_geo.length_half()[2]; - - // collect moments - - gblcblk[0] = 0; // sum q_i (z_i - L/2) - gblcblk[1] = 0; // sum q_i z_i - gblcblk[2] = 0; // sum q_i - - for (auto const &p : local_particles) { - check_gap_elc(p); - - gblcblk[0] += p.p.q * (p.r.p[2] - shift); - gblcblk[1] += p.p.q * p.r.p[2]; - gblcblk[2] += p.p.q; - - if (elc_params.dielectric_contrast_on) { - if (p.r.p[2] < elc_params.space_layer) { - gblcblk[0] += elc_params.delta_mid_bot * p.p.q * (-p.r.p[2] - shift); - gblcblk[2] += elc_params.delta_mid_bot * p.p.q; - } - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - gblcblk[0] += elc_params.delta_mid_top * p.p.q * - (2 * elc_params.h - p.r.p[2] - shift); - gblcblk[2] += elc_params.delta_mid_top * p.p.q; - } - } - } - - gblcblk[0] *= pref; - gblcblk[1] *= pref / elc_params.h * box_geo.length()[2]; - gblcblk[2] *= pref; - - distribute(size); - - // Yeh + Berkowitz dipole term @cite yeh99a - double field_tot = gblcblk[0]; - - // Const. potential contribution - if (elc_params.const_pot) { - coulomb.field_induced = gblcblk[1]; - coulomb.field_applied = elc_params.pot_diff / elc_params.h; - field_tot -= coulomb.field_applied + coulomb.field_induced; - } - - for (auto &p : local_particles) { - p.f.f[2] -= field_tot * p.p.q; - - if (!elc_params.neutralize) { - // SUBTRACT the forces of the P3M homogeneous neutralizing background - p.f.f[2] += gblcblk[2] * p.p.q * (p.r.p[2] - shift); - } - } -} - -/** Calculate the dipole energy. - * See @cite yeh99a. - */ -static double dipole_energy(const ParticleRange &particles) { - double const pref = coulomb.prefactor * 2 * Utils::pi() * - box_geo.length_inv()[0] * box_geo.length_inv()[1] * - box_geo.length_inv()[2]; - constexpr std::size_t size = 7; - /* for nonneutral systems, this shift gives the background contribution - (rsp. for this shift, the DM of the background is zero) */ - double const shift = box_geo.length_half()[2]; - - // collect moments - - gblcblk[0] = 0; // sum q_i primary box - gblcblk[1] = 0; // sum q_i boundary layers - gblcblk[2] = 0; // sum q_i (z_i - L/2) primary box - gblcblk[3] = 0; // sum q_i (z_i - L/2) boundary layers - gblcblk[4] = 0; // sum q_i (z_i - L/2)^2 primary box - gblcblk[5] = 0; // sum q_i (z_i - L/2)^2 boundary layers - gblcblk[6] = 0; // sum q_i z_i primary box - - for (auto &p : particles) { - check_gap_elc(p); - - gblcblk[0] += p.p.q; - gblcblk[2] += p.p.q * (p.r.p[2] - shift); - gblcblk[4] += p.p.q * (Utils::sqr(p.r.p[2] - shift)); - gblcblk[6] += p.p.q * p.r.p[2]; - - if (elc_params.dielectric_contrast_on) { - if (p.r.p[2] < elc_params.space_layer) { - gblcblk[1] += elc_params.delta_mid_bot * p.p.q; - gblcblk[3] += elc_params.delta_mid_bot * p.p.q * (-p.r.p[2] - shift); - gblcblk[5] += - elc_params.delta_mid_bot * p.p.q * (Utils::sqr(-p.r.p[2] - shift)); - } - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - gblcblk[1] += elc_params.delta_mid_top * p.p.q; - gblcblk[3] += elc_params.delta_mid_top * p.p.q * - (2 * elc_params.h - p.r.p[2] - shift); - gblcblk[5] += elc_params.delta_mid_top * p.p.q * - (Utils::sqr(2 * elc_params.h - p.r.p[2] - shift)); - } - } - } - - distribute(size); - - // Yeh + Berkowitz term @cite yeh99a - double energy = 2 * pref * (Utils::sqr(gblcblk[2]) + gblcblk[2] * gblcblk[3]); - - if (!elc_params.neutralize) { - // SUBTRACT the energy of the P3M homogeneous neutralizing background - energy += 2 * pref * - (-gblcblk[0] * gblcblk[4] - - (.25 - .5 / 3.) * Utils::sqr(gblcblk[0] * box_geo.length()[2])); - } - - if (elc_params.dielectric_contrast_on) { - if (elc_params.const_pot) { - // zero potential difference contribution - energy += - pref / elc_params.h * box_geo.length()[2] * Utils::sqr(gblcblk[6]); - // external potential shift contribution - energy -= 2 * elc_params.pot_diff / elc_params.h * gblcblk[6]; - } - - /* counter the P3M homogeneous background contribution to the - boundaries. We never need that, since a homogeneous background - spanning the artificial boundary layers is aphysical. */ - energy += pref * (-(gblcblk[1] * gblcblk[4] + gblcblk[0] * gblcblk[5]) - - (1. - 2. / 3.) * gblcblk[0] * gblcblk[1] * - Utils::sqr(box_geo.length()[2])); - } - - return this_node == 0 ? energy : 0; -} - -/*****************************************************************/ - -inline double image_sum_b(double q, double z) { - double const shift = box_geo.length_half()[2]; - double const fac = elc_params.delta_mid_top * elc_params.delta_mid_bot; - double const image_sum = - (q / (1.0 - fac) * (z - 2.0 * fac * box_geo.length()[2] / (1.0 - fac))) - - q * shift / (1 - fac); - return image_sum; -} - -inline double image_sum_t(double q, double z) { - double const shift = box_geo.length_half()[2]; - double const fac = elc_params.delta_mid_top * elc_params.delta_mid_bot; - double const image_sum = - (q / (1.0 - fac) * (z + 2.0 * fac * box_geo.length()[2] / (1.0 - fac))) - - q * shift / (1 - fac); - return image_sum; -} - -/*****************************************************************/ -static double z_energy(const ParticleRange &particles) { - double const pref = coulomb.prefactor * 2 * Utils::pi() * - box_geo.length_inv()[0] * box_geo.length_inv()[1]; - constexpr std::size_t size = 4; - - /* for nonneutral systems, this shift gives the background contribution - (rsp. for this shift, the DM of the background is zero) */ - double const shift = box_geo.length_half()[2]; - - if (elc_params.dielectric_contrast_on) { - if (elc_params.const_pot) { - clear_vec(gblcblk, size); - for (auto &p : particles) { - gblcblk[0] += p.p.q; - gblcblk[1] += p.p.q * (p.r.p[2] - shift); - if (p.r.p[2] < elc_params.space_layer) { - gblcblk[2] -= elc_params.delta_mid_bot * p.p.q; - gblcblk[3] -= elc_params.delta_mid_bot * p.p.q * (-p.r.p[2] - shift); - } - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - gblcblk[2] += elc_params.delta_mid_top * p.p.q; - gblcblk[3] += elc_params.delta_mid_top * p.p.q * - (2 * elc_params.h - p.r.p[2] - shift); - } - } - } else { - double const delta = elc_params.delta_mid_top * elc_params.delta_mid_bot; - double const fac_delta_mid_bot = elc_params.delta_mid_bot / (1 - delta); - double const fac_delta_mid_top = elc_params.delta_mid_top / (1 - delta); - double const fac_delta = delta / (1 - delta); - - clear_vec(gblcblk, size); - for (auto &p : particles) { - gblcblk[0] += p.p.q; - gblcblk[1] += p.p.q * (p.r.p[2] - shift); - if (elc_params.dielectric_contrast_on) { - if (p.r.p[2] < elc_params.space_layer) { - gblcblk[2] += fac_delta * (elc_params.delta_mid_bot + 1) * p.p.q; - gblcblk[3] += - p.p.q * (image_sum_b(elc_params.delta_mid_bot * delta, - -(2 * elc_params.h + p.r.p[2])) + - image_sum_b(delta, -(2 * elc_params.h - p.r.p[2]))); - } else { - gblcblk[2] += - fac_delta_mid_bot * (1 + elc_params.delta_mid_top) * p.p.q; - gblcblk[3] += - p.p.q * (image_sum_b(elc_params.delta_mid_bot, -p.r.p[2]) + - image_sum_b(delta, -(2 * elc_params.h - p.r.p[2]))); - } - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - // note the minus sign here which is required due to |z_i-z_j| - gblcblk[2] -= fac_delta * (elc_params.delta_mid_top + 1) * p.p.q; - gblcblk[3] -= - p.p.q * (image_sum_t(elc_params.delta_mid_top * delta, - 4 * elc_params.h - p.r.p[2]) + - image_sum_t(delta, 2 * elc_params.h + p.r.p[2])); - } else { - // note the minus sign here which is required due to |z_i-z_j| - gblcblk[2] -= - fac_delta_mid_top * (1 + elc_params.delta_mid_bot) * p.p.q; - gblcblk[3] -= - p.p.q * (image_sum_t(elc_params.delta_mid_top, - 2 * elc_params.h - p.r.p[2]) + - image_sum_t(delta, 2 * elc_params.h + p.r.p[2])); - } - } - } - } - } - distribute(size); - - double energy = 0; - if (this_node == 0) - energy -= gblcblk[1] * gblcblk[2] - gblcblk[0] * gblcblk[3]; - - return pref * energy; -} - -/*****************************************************************/ -static void add_z_force(const ParticleRange &particles) { - double const pref = coulomb.prefactor * 2 * Utils::pi() * - box_geo.length_inv()[0] * box_geo.length_inv()[1]; - constexpr std::size_t size = 1; - - if (elc_params.dielectric_contrast_on) { - auto local_particles = particles; - if (elc_params.const_pot) { - clear_vec(gblcblk, size); - /* just counter the 2 pi |z| contribution stemming from P3M */ - for (auto &p : local_particles) { - if (p.r.p[2] < elc_params.space_layer) - gblcblk[0] -= elc_params.delta_mid_bot * p.p.q; - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) - gblcblk[0] += elc_params.delta_mid_top * p.p.q; - } - } else { - double const delta = elc_params.delta_mid_top * elc_params.delta_mid_bot; - double const fac_delta_mid_bot = elc_params.delta_mid_bot / (1 - delta); - double const fac_delta_mid_top = elc_params.delta_mid_top / (1 - delta); - double const fac_delta = delta / (1 - delta); - - clear_vec(gblcblk, size); - for (auto &p : local_particles) { - if (p.r.p[2] < elc_params.space_layer) { - gblcblk[0] += fac_delta * (elc_params.delta_mid_bot + 1) * p.p.q; - } else { - gblcblk[0] += - fac_delta_mid_bot * (1 + elc_params.delta_mid_top) * p.p.q; - } - - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - // note the minus sign here which is required due to |z_i-z_j| - gblcblk[0] -= fac_delta * (elc_params.delta_mid_top + 1) * p.p.q; - } else { - // note the minus sign here which is required due to |z_i-z_j| - gblcblk[0] -= - fac_delta_mid_top * (1 + elc_params.delta_mid_bot) * p.p.q; - } - } - } - - gblcblk[0] *= pref; - - distribute(size); - - for (auto &p : local_particles) { - p.f.f[2] += gblcblk[0] * p.p.q; - } - } -} - -/*****************************************************************/ -/* PoQ exp sum */ -/*****************************************************************/ - -/** \name q=0 or p=0 per frequency code */ -/**@{*/ -template -void setup_PoQ(std::size_t index, double omega, - const ParticleRange &particles) { - assert(index >= 1); - double const pref_di = coulomb.prefactor * 4 * Utils::pi() * - box_geo.length_inv()[0] * box_geo.length_inv()[1]; - double const pref = -pref_di / expm1(omega * box_geo.length()[2]); - constexpr std::size_t size = 4; - double lclimgebot[4], lclimgetop[4], lclimge[4]; - double fac_delta_mid_bot = 1, fac_delta_mid_top = 1, fac_delta = 1; - - if (elc_params.dielectric_contrast_on) { - double const fac_elc = - 1.0 / (1 - elc_params.delta_mid_top * elc_params.delta_mid_bot * - exp(-omega * 2 * elc_params.h)); - fac_delta_mid_bot = elc_params.delta_mid_bot * fac_elc; - fac_delta_mid_top = elc_params.delta_mid_top * fac_elc; - fac_delta = fac_delta_mid_bot * elc_params.delta_mid_top; - } - - clear_vec(lclimge, size); - clear_vec(gblcblk, size); - auto const &sc_cache = (axis == PoQ::P) ? scxcache : scycache; - - std::size_t ic = 0; - auto const o = (index - 1) * particles.size(); - for (auto &p : particles) { - double e = exp(omega * p.r.p[2]); - - partblk[size * ic + POQESM] = p.p.q * sc_cache[o + ic].s / e; - partblk[size * ic + POQESP] = p.p.q * sc_cache[o + ic].s * e; - partblk[size * ic + POQECM] = p.p.q * sc_cache[o + ic].c / e; - partblk[size * ic + POQECP] = p.p.q * sc_cache[o + ic].c * e; - - add_vec(gblcblk, gblcblk, block(partblk.data(), ic, size), size); - - if (elc_params.dielectric_contrast_on) { - if (p.r.p[2] < elc_params.space_layer) { // handle the lower case first - // negative sign is okay here as the image is located at -p.r.p[2] - - e = exp(-omega * p.r.p[2]); - - double const scale = p.p.q * elc_params.delta_mid_bot; - - lclimgebot[POQESM] = sc_cache[o + ic].s / e; - lclimgebot[POQESP] = sc_cache[o + ic].s * e; - lclimgebot[POQECM] = sc_cache[o + ic].c / e; - lclimgebot[POQECP] = sc_cache[o + ic].c * e; - - addscale_vec(gblcblk, scale, lclimgebot, gblcblk, size); - - e = (exp(omega * (-p.r.p[2] - 2 * elc_params.h)) * - elc_params.delta_mid_bot + - exp(omega * (p.r.p[2] - 2 * elc_params.h))) * - fac_delta; - - } else { - - e = (exp(omega * (-p.r.p[2])) + - exp(omega * (p.r.p[2] - 2 * elc_params.h)) * - elc_params.delta_mid_top) * - fac_delta_mid_bot; - } - - lclimge[POQESP] += p.p.q * sc_cache[o + ic].s * e; - lclimge[POQECP] += p.p.q * sc_cache[o + ic].c * e; - - if (p.r.p[2] > (elc_params.h - - elc_params.space_layer)) { // handle the upper case now - - e = exp(omega * (2 * elc_params.h - p.r.p[2])); - - double const scale = p.p.q * elc_params.delta_mid_top; - - lclimgetop[POQESM] = sc_cache[o + ic].s / e; - lclimgetop[POQESP] = sc_cache[o + ic].s * e; - lclimgetop[POQECM] = sc_cache[o + ic].c / e; - lclimgetop[POQECP] = sc_cache[o + ic].c * e; - - addscale_vec(gblcblk, scale, lclimgetop, gblcblk, size); - - e = (exp(omega * (+p.r.p[2] - 4 * elc_params.h)) * - elc_params.delta_mid_top + - exp(omega * (-p.r.p[2] - 2 * elc_params.h))) * - fac_delta; - - } else { - - e = (exp(omega * (+p.r.p[2] - 2 * elc_params.h)) + - exp(omega * (-p.r.p[2] - 2 * elc_params.h)) * - elc_params.delta_mid_bot) * - fac_delta_mid_top; - } - - lclimge[POQESM] += p.p.q * sc_cache[o + ic].s * e; - lclimge[POQECM] += p.p.q * sc_cache[o + ic].c * e; - } - - ic++; - } - - scale_vec(pref, gblcblk, size); - - if (elc_params.dielectric_contrast_on) { - scale_vec(pref_di, lclimge, size); - add_vec(gblcblk, gblcblk, lclimge, size); - } -} - -template void add_PoQ_force(const ParticleRange &particles) { - constexpr auto i = static_cast(axis); - constexpr std::size_t size = 4; - - std::size_t ic = 0; - for (auto &p : particles) { - p.f.f[i] += partblk[size * ic + POQESM] * gblcblk[POQECP] - - partblk[size * ic + POQECM] * gblcblk[POQESP] + - partblk[size * ic + POQESP] * gblcblk[POQECM] - - partblk[size * ic + POQECP] * gblcblk[POQESM]; - p.f.f[2] += partblk[size * ic + POQECM] * gblcblk[POQECP] + - partblk[size * ic + POQESM] * gblcblk[POQESP] - - partblk[size * ic + POQECP] * gblcblk[POQECM] - - partblk[size * ic + POQESP] * gblcblk[POQESM]; - ic++; - } -} - -static double PoQ_energy(double omega, std::size_t n_part) { - constexpr std::size_t size = 4; - - double energy = 0; - for (std::size_t ic = 0; ic < n_part; ic++) { - energy += partblk[size * ic + POQECM] * gblcblk[POQECP] + - partblk[size * ic + POQESM] * gblcblk[POQESP] + - partblk[size * ic + POQECP] * gblcblk[POQECM] + - partblk[size * ic + POQESP] * gblcblk[POQESM]; - } - - return energy / omega; -} -/**@}*/ - -/*****************************************************************/ -/* PQ particle blocks */ -/*****************************************************************/ - -/** \name p,q <> 0 per frequency code */ -/**@{*/ -static void setup_PQ(std::size_t index_p, std::size_t index_q, double omega, - const ParticleRange &particles) { - assert(index_p >= 1); - assert(index_q >= 1); - double const pref_di = coulomb.prefactor * 8 * Utils::pi() * - box_geo.length_inv()[0] * box_geo.length_inv()[1]; - double const pref = -pref_di / expm1(omega * box_geo.length()[2]); - constexpr std::size_t size = 8; - double lclimgebot[8], lclimgetop[8], lclimge[8]; - double fac_delta_mid_bot = 1, fac_delta_mid_top = 1, fac_delta = 1; - if (elc_params.dielectric_contrast_on) { - double fac_elc = - 1.0 / (1 - elc_params.delta_mid_top * elc_params.delta_mid_bot * - exp(-omega * 2 * elc_params.h)); - fac_delta_mid_bot = elc_params.delta_mid_bot * fac_elc; - fac_delta_mid_top = elc_params.delta_mid_top * fac_elc; - fac_delta = fac_delta_mid_bot * elc_params.delta_mid_top; - } - - clear_vec(lclimge, size); - clear_vec(gblcblk, size); - - std::size_t ic = 0; - auto const ox = (index_p - 1) * particles.size(); - auto const oy = (index_q - 1) * particles.size(); - for (auto const &p : particles) { - double e = exp(omega * p.r.p[2]); - - partblk[size * ic + PQESSM] = - scxcache[ox + ic].s * scycache[oy + ic].s * p.p.q / e; - partblk[size * ic + PQESCM] = - scxcache[ox + ic].s * scycache[oy + ic].c * p.p.q / e; - partblk[size * ic + PQECSM] = - scxcache[ox + ic].c * scycache[oy + ic].s * p.p.q / e; - partblk[size * ic + PQECCM] = - scxcache[ox + ic].c * scycache[oy + ic].c * p.p.q / e; - - partblk[size * ic + PQESSP] = - scxcache[ox + ic].s * scycache[oy + ic].s * p.p.q * e; - partblk[size * ic + PQESCP] = - scxcache[ox + ic].s * scycache[oy + ic].c * p.p.q * e; - partblk[size * ic + PQECSP] = - scxcache[ox + ic].c * scycache[oy + ic].s * p.p.q * e; - partblk[size * ic + PQECCP] = - scxcache[ox + ic].c * scycache[oy + ic].c * p.p.q * e; - - add_vec(gblcblk, gblcblk, block(partblk.data(), ic, size), size); - - if (elc_params.dielectric_contrast_on) { - if (p.r.p[2] < elc_params.space_layer) { // handle the lower case first - // change e to take into account the z position of the images - - e = exp(-omega * p.r.p[2]); - auto const scale = p.p.q * elc_params.delta_mid_bot; - - lclimgebot[PQESSM] = scxcache[ox + ic].s * scycache[oy + ic].s / e; - lclimgebot[PQESCM] = scxcache[ox + ic].s * scycache[oy + ic].c / e; - lclimgebot[PQECSM] = scxcache[ox + ic].c * scycache[oy + ic].s / e; - lclimgebot[PQECCM] = scxcache[ox + ic].c * scycache[oy + ic].c / e; - - lclimgebot[PQESSP] = scxcache[ox + ic].s * scycache[oy + ic].s * e; - lclimgebot[PQESCP] = scxcache[ox + ic].s * scycache[oy + ic].c * e; - lclimgebot[PQECSP] = scxcache[ox + ic].c * scycache[oy + ic].s * e; - lclimgebot[PQECCP] = scxcache[ox + ic].c * scycache[oy + ic].c * e; - - addscale_vec(gblcblk, scale, lclimgebot, gblcblk, size); - - e = (exp(omega * (-p.r.p[2] - 2 * elc_params.h)) * - elc_params.delta_mid_bot + - exp(omega * (p.r.p[2] - 2 * elc_params.h))) * - fac_delta * p.p.q; - - } else { - - e = (exp(omega * (-p.r.p[2])) + - exp(omega * (p.r.p[2] - 2 * elc_params.h)) * - elc_params.delta_mid_top) * - fac_delta_mid_bot * p.p.q; - } - - lclimge[PQESSP] += scxcache[ox + ic].s * scycache[oy + ic].s * e; - lclimge[PQESCP] += scxcache[ox + ic].s * scycache[oy + ic].c * e; - lclimge[PQECSP] += scxcache[ox + ic].c * scycache[oy + ic].s * e; - lclimge[PQECCP] += scxcache[ox + ic].c * scycache[oy + ic].c * e; - - if (p.r.p[2] > (elc_params.h - - elc_params.space_layer)) { // handle the upper case now - - e = exp(omega * (2 * elc_params.h - p.r.p[2])); - auto const scale = p.p.q * elc_params.delta_mid_top; - - lclimgetop[PQESSM] = scxcache[ox + ic].s * scycache[oy + ic].s / e; - lclimgetop[PQESCM] = scxcache[ox + ic].s * scycache[oy + ic].c / e; - lclimgetop[PQECSM] = scxcache[ox + ic].c * scycache[oy + ic].s / e; - lclimgetop[PQECCM] = scxcache[ox + ic].c * scycache[oy + ic].c / e; - - lclimgetop[PQESSP] = scxcache[ox + ic].s * scycache[oy + ic].s * e; - lclimgetop[PQESCP] = scxcache[ox + ic].s * scycache[oy + ic].c * e; - lclimgetop[PQECSP] = scxcache[ox + ic].c * scycache[oy + ic].s * e; - lclimgetop[PQECCP] = scxcache[ox + ic].c * scycache[oy + ic].c * e; - - addscale_vec(gblcblk, scale, lclimgetop, gblcblk, size); - - e = (exp(omega * (p.r.p[2] - 4 * elc_params.h)) * - elc_params.delta_mid_top + - exp(omega * (-p.r.p[2] - 2 * elc_params.h))) * - fac_delta * p.p.q; - - } else { - - e = (exp(omega * (p.r.p[2] - 2 * elc_params.h)) + - exp(omega * (-p.r.p[2] - 2 * elc_params.h)) * - elc_params.delta_mid_bot) * - fac_delta_mid_top * p.p.q; - } - - lclimge[PQESSM] += scxcache[ox + ic].s * scycache[oy + ic].s * e; - lclimge[PQESCM] += scxcache[ox + ic].s * scycache[oy + ic].c * e; - lclimge[PQECSM] += scxcache[ox + ic].c * scycache[oy + ic].s * e; - lclimge[PQECCM] += scxcache[ox + ic].c * scycache[oy + ic].c * e; - } - - ic++; - } - - scale_vec(pref, gblcblk, size); - if (elc_params.dielectric_contrast_on) { - scale_vec(pref_di, lclimge, size); - add_vec(gblcblk, gblcblk, lclimge, size); - } -} - -static void add_PQ_force(std::size_t index_p, std::size_t index_q, double omega, - const ParticleRange &particles) { - constexpr double c_2pi = 2 * Utils::pi(); - double const pref_x = - c_2pi * box_geo.length_inv()[0] * static_cast(index_p) / omega; - double const pref_y = - c_2pi * box_geo.length_inv()[1] * static_cast(index_q) / omega; - constexpr std::size_t size = 8; - - std::size_t ic = 0; - for (auto &p : particles) { - p.f.f[0] += pref_x * (partblk[size * ic + PQESCM] * gblcblk[PQECCP] + - partblk[size * ic + PQESSM] * gblcblk[PQECSP] - - partblk[size * ic + PQECCM] * gblcblk[PQESCP] - - partblk[size * ic + PQECSM] * gblcblk[PQESSP] + - partblk[size * ic + PQESCP] * gblcblk[PQECCM] + - partblk[size * ic + PQESSP] * gblcblk[PQECSM] - - partblk[size * ic + PQECCP] * gblcblk[PQESCM] - - partblk[size * ic + PQECSP] * gblcblk[PQESSM]); - p.f.f[1] += pref_y * (partblk[size * ic + PQECSM] * gblcblk[PQECCP] + - partblk[size * ic + PQESSM] * gblcblk[PQESCP] - - partblk[size * ic + PQECCM] * gblcblk[PQECSP] - - partblk[size * ic + PQESCM] * gblcblk[PQESSP] + - partblk[size * ic + PQECSP] * gblcblk[PQECCM] + - partblk[size * ic + PQESSP] * gblcblk[PQESCM] - - partblk[size * ic + PQECCP] * gblcblk[PQECSM] - - partblk[size * ic + PQESCP] * gblcblk[PQESSM]); - p.f.f[2] += (partblk[size * ic + PQECCM] * gblcblk[PQECCP] + - partblk[size * ic + PQECSM] * gblcblk[PQECSP] + - partblk[size * ic + PQESCM] * gblcblk[PQESCP] + - partblk[size * ic + PQESSM] * gblcblk[PQESSP] - - partblk[size * ic + PQECCP] * gblcblk[PQECCM] - - partblk[size * ic + PQECSP] * gblcblk[PQECSM] - - partblk[size * ic + PQESCP] * gblcblk[PQESCM] - - partblk[size * ic + PQESSP] * gblcblk[PQESSM]); - ic++; - } -} - -static double PQ_energy(double omega, std::size_t n_part) { - constexpr std::size_t size = 8; - - double energy = 0; - for (std::size_t ic = 0; ic < n_part; ic++) { - energy += partblk[size * ic + PQECCM] * gblcblk[PQECCP] + - partblk[size * ic + PQECSM] * gblcblk[PQECSP] + - partblk[size * ic + PQESCM] * gblcblk[PQESCP] + - partblk[size * ic + PQESSM] * gblcblk[PQESSP] + - partblk[size * ic + PQECCP] * gblcblk[PQECCM] + - partblk[size * ic + PQECSP] * gblcblk[PQECSM] + - partblk[size * ic + PQESCP] * gblcblk[PQESCM] + - partblk[size * ic + PQESSP] * gblcblk[PQESSM]; - } - return energy / omega; -} -/**@}*/ - -/*****************************************************************/ -/* main loops */ -/*****************************************************************/ - -void ELC_add_force(const ParticleRange &particles) { - constexpr double c_2pi = 2 * Utils::pi(); - auto const n_scxcache = - std::size_t(ceil(elc_params.far_cut * box_geo.length()[0]) + 1); - auto const n_scycache = - std::size_t(ceil(elc_params.far_cut * box_geo.length()[1]) + 1); - - prepare_sc_cache(particles, n_scxcache, box_geo.length_inv()[0], n_scycache, - box_geo.length_inv()[1]); - partblk.resize(particles.size() * 8); - - add_dipole_force(particles); - add_z_force(particles); - - /* the second condition is just for the case of numerical accident */ - for (std::size_t p = 1; box_geo.length_inv()[0] * static_cast(p - 1) < - elc_params.far_cut && - p <= n_scxcache; - p++) { - auto const omega = c_2pi * box_geo.length_inv()[0] * static_cast(p); - setup_PoQ(p, omega, particles); - distribute(4); - add_PoQ_force(particles); - } - - for (std::size_t q = 1; box_geo.length_inv()[1] * static_cast(q - 1) < - elc_params.far_cut && - q <= n_scycache; - q++) { - auto const omega = c_2pi * box_geo.length_inv()[1] * static_cast(q); - setup_PoQ(q, omega, particles); - distribute(4); - add_PoQ_force(particles); - } - - for (std::size_t p = 1; box_geo.length_inv()[0] * static_cast(p - 1) < - elc_params.far_cut && - p <= n_scxcache; - p++) { - for (std::size_t q = 1; - Utils::sqr(box_geo.length_inv()[0] * static_cast(p - 1)) + - Utils::sqr(box_geo.length_inv()[1] * - static_cast(q - 1)) < - elc_params.far_cut2 && - q <= n_scycache; - q++) { - auto const omega = - c_2pi * - sqrt(Utils::sqr(box_geo.length_inv()[0] * static_cast(p)) + - Utils::sqr(box_geo.length_inv()[1] * static_cast(q))); - setup_PQ(p, q, omega, particles); - distribute(8); - add_PQ_force(p, q, omega, particles); - } - } -} - -double ELC_energy(const ParticleRange &particles) { - constexpr double c_2pi = 2 * Utils::pi(); - auto energy = dipole_energy(particles); - energy += z_energy(particles); - - auto const n_scxcache = - std::size_t(ceil(elc_params.far_cut * box_geo.length()[0]) + 1); - auto const n_scycache = - std::size_t(ceil(elc_params.far_cut * box_geo.length()[1]) + 1); - prepare_sc_cache(particles, n_scxcache, box_geo.length_inv()[0], n_scycache, - box_geo.length_inv()[1]); - - auto const n_localpart = particles.size(); - partblk.resize(n_localpart * 8); - - /* the second condition is just for the case of numerical accident */ - for (std::size_t p = 1; box_geo.length_inv()[0] * static_cast(p - 1) < - elc_params.far_cut && - p <= n_scxcache; - p++) { - auto const omega = c_2pi * box_geo.length_inv()[0] * static_cast(p); - setup_PoQ(p, omega, particles); - distribute(4); - energy += PoQ_energy(omega, n_localpart); - } - - for (std::size_t q = 1; box_geo.length_inv()[1] * static_cast(q - 1) < - elc_params.far_cut && - q <= n_scycache; - q++) { - auto const omega = c_2pi * box_geo.length_inv()[1] * static_cast(q); - setup_PoQ(q, omega, particles); - distribute(4); - energy += PoQ_energy(omega, n_localpart); - } - - for (std::size_t p = 1; box_geo.length_inv()[0] * static_cast(p - 1) < - elc_params.far_cut && - p <= n_scxcache; - p++) { - for (std::size_t q = 1; - Utils::sqr(box_geo.length_inv()[0] * static_cast(p - 1)) + - Utils::sqr(box_geo.length_inv()[1] * - static_cast(q - 1)) < - elc_params.far_cut2 && - q <= n_scycache; - q++) { - auto const omega = - c_2pi * - sqrt(Utils::sqr(box_geo.length_inv()[0] * static_cast(p)) + - Utils::sqr(box_geo.length_inv()[1] * static_cast(q))); - setup_PQ(p, q, omega, particles); - distribute(8); - energy += PQ_energy(omega, n_localpart); - } - } - /* we count both i<->j and j<->i, so return just half of it */ - return 0.5 * energy; -} - -double ELC_tune_far_cut(ELCParameters const ¶ms) { - // Largest reasonable cutoff for far formula - constexpr auto maximal_far_cut = 50.; - double const h = params.h; - double lz = box_geo.length()[2]; - double const min_inv_boxl = - std::min(box_geo.length_inv()[0], box_geo.length_inv()[1]); - - if (params.dielectric_contrast_on) { - // adjust lz according to dielectric layer method - lz = params.h + params.space_layer; - } - - if (h < 0.) { - throw std::runtime_error("gap size too large"); - } - - auto far_cut = min_inv_boxl; - double err; - do { - const auto prefactor = 2 * Utils::pi() * far_cut; - - const auto sum = - prefactor + 2 * (box_geo.length_inv()[0] + box_geo.length_inv()[1]); - const auto den = -expm1(-prefactor * lz); - const auto num1 = exp(prefactor * (h - lz)); - const auto num2 = exp(-prefactor * (h + lz)); - - err = 0.5 / den * - (num1 * (sum + 1 / (lz - h)) / (lz - h) + - num2 * (sum + 1 / (lz + h)) / (lz + h)); - - far_cut += min_inv_boxl; - } while (err > params.maxPWerror && far_cut < maximal_far_cut); - if (far_cut >= maximal_far_cut) { - throw std::runtime_error("ELC tuning failed: maxPWerror too small"); - } - return far_cut - min_inv_boxl; -} - -/**************************************** - * COMMON PARTS - ****************************************/ - -void ELC_sanity_checks(ELCParameters const ¶ms) { - if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { - throw std::runtime_error("ELC requires periodicity 1 1 1"); - } - /* The product of the two dielectric contrasts should be < 1 for ELC to - work. This is not the case for two parallel boundaries, which can only - be treated by the constant potential code */ - if (params.dielectric_contrast_on && - (fabs(1.0 - params.delta_mid_top * params.delta_mid_bot) < - ROUND_ERROR_PREC) && - !params.const_pot) { - throw std::runtime_error("ELC with two parallel metallic boundaries " - "requires the const_pot option"); - } - - // ELC with non-neutral systems and no fully metallic boundaries does not work - if (params.dielectric_contrast_on && !params.const_pot && - p3m.square_sum_q > ROUND_ERROR_PREC) { - throw std::runtime_error("ELC does not work for non-neutral systems and " - "non-metallic dielectric contrast."); - } - - // Disable this line to make ELC work again with non-neutral systems and - // metallic boundaries - if (params.dielectric_contrast_on && params.const_pot && - p3m.square_sum_q > ROUND_ERROR_PREC) { - throw std::runtime_error("ELC does not currently support non-neutral " - "systems with a dielectric contrast."); - } -} - -void ELC_init() { - elc_params.h = box_geo.length()[2] - elc_params.gap_size; - - if (elc_params.dielectric_contrast_on) { - // recalculate the space layer size - // set the space_layer to be 1/3 of the gap size, so that box = layer - elc_params.space_layer = (1. / 3.) * elc_params.gap_size; - // but make sure we leave enough space to not have to bother with - // overlapping realspace P3M - double maxsl = elc_params.gap_size - p3m.params.r_cut; - // and make sure the space layer is not bigger than half the actual - // simulation box, to avoid overlaps - if (maxsl > .5 * elc_params.h) - maxsl = .5 * elc_params.h; - if (elc_params.space_layer > maxsl) { - if (maxsl <= 0) { - runtimeErrorMsg() << "P3M real space cutoff too large for ELC w/ " - "dielectric contrast"; - } else - elc_params.space_layer = maxsl; - } - - // set the space_box - elc_params.space_box = elc_params.gap_size - 2 * elc_params.space_layer; - // reset minimal_dist for tuning - elc_params.minimal_dist = - std::min(elc_params.space_box, elc_params.space_layer); - } - - if (elc_params.far_calculated && elc_params.dielectric_contrast_on) { - try { - elc_params.far_cut = ELC_tune_far_cut(elc_params); - elc_params.far_cut2 = Utils::sqr(elc_params.far_cut); - } catch (std::runtime_error const &err) { - runtimeErrorMsg() << err.what() << " (during auto-retuning)"; - } - } -} - -void ELC_set_params(double maxPWerror, double gap_size, double far_cut, - bool neutralize, double delta_top, double delta_bot, - bool const_pot, double pot_diff) { - assert(coulomb.method == COULOMB_ELC_P3M or coulomb.method == COULOMB_P3M); - auto const h = box_geo.length()[2] - gap_size; - if (maxPWerror <= 0.) { - throw std::domain_error("maxPWerror must be > 0"); - } - if (gap_size <= 0.) { - throw std::domain_error("gap_size must be > 0"); - } - if (h < 0.) { - throw std::domain_error("gap size too large"); - } - - ELCParameters new_elc_params; - if (delta_top != 0.0 || delta_bot != 0.0) { - // setup with dielectric contrast (neutralize is automatic) - - // initial setup of parameters, may change later when P3M is finally tuned - // set the space_layer to be 1/3 of the gap size, so that box = layer - auto const space_layer = gap_size / 3.; - auto const space_box = gap_size - 2. * space_layer; - - new_elc_params = ELCParameters{maxPWerror, - far_cut, - 0., - gap_size, - far_cut == -1., - false, - true, - delta_top, - delta_bot, - const_pot, - (const_pot) ? pot_diff : 0., - std::min(space_box, space_layer), - space_layer, - space_box, - h}; - } else { - // setup without dielectric contrast - new_elc_params = - ELCParameters{maxPWerror, far_cut, 0., gap_size, far_cut == -1., - neutralize, false, 0., 0., false, - 0., gap_size, 0., gap_size, h}; - } - - ELC_sanity_checks(new_elc_params); - - if (new_elc_params.far_calculated) { - new_elc_params.far_cut = ELC_tune_far_cut(new_elc_params); - } - new_elc_params.far_cut2 = Utils::sqr(new_elc_params.far_cut); - - // set new parameters - elc_params = new_elc_params; - p3m.params.epsilon = P3M_EPSILON_METALLIC; - coulomb.method = COULOMB_ELC_P3M; - - mpi_bcast_coulomb_params(); -} - -//////////////////////////////////////////////////////////////////////////////////// - -void ELC_P3M_self_forces(const ParticleRange &particles) { - for (auto &p : particles) { - p.f.f += coulomb.prefactor * ELC_P3M_dielectric_layers_force_contribution( - p.r.p, p.r.p, p.p.q * p.p.q); - } -} - -//////////////////////////////////////////////////////////////////////////////////// - -namespace { -void assign_image_charge(const Particle &p) { - if (p.r.p[2] < elc_params.space_layer) { - auto const q_eff = elc_params.delta_mid_bot * p.p.q; - auto const pos = Utils::Vector3d{p.r.p[0], p.r.p[1], -p.r.p[2]}; - - p3m_assign_charge(q_eff, pos); - } - - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - auto const q_eff = elc_params.delta_mid_top * p.p.q; - auto const pos = - Utils::Vector3d{p.r.p[0], p.r.p[1], 2 * elc_params.h - p.r.p[2]}; - - p3m_assign_charge(q_eff, pos); - } -} -} // namespace - -void ELC_p3m_charge_assign_both(const ParticleRange &particles) { - p3m.inter_weights.reset(p3m.params.cao); - - /* prepare local FFT mesh */ - for (int i = 0; i < p3m.local_mesh.size; i++) - p3m.rs_mesh[i] = 0.0; - - for (auto const &p : particles) { - if (p.p.q != 0.0) { - p3m_assign_charge(p.p.q, p.r.p, p3m.inter_weights); - assign_image_charge(p); - } - } -} - -void ELC_p3m_charge_assign_image(const ParticleRange &particles) { - /* prepare local FFT mesh */ - for (int i = 0; i < p3m.local_mesh.size; i++) - p3m.rs_mesh[i] = 0.0; - - for (auto const &p : particles) { - if (p.p.q != 0.0) { - assign_image_charge(p); - } - } -} - -//////////////////////////////////////////////////////////////////////////////////// - -Utils::Vector3d ELC_P3M_dielectric_layers_force_contribution( - const Utils::Vector3d &pos1, const Utils::Vector3d &pos2, double q1q2) { - Utils::Vector3d force{}; - - if (pos1[2] < elc_params.space_layer) { - auto const q = elc_params.delta_mid_bot * q1q2; - auto const d = box_geo.get_mi_vector(pos2, {pos1[0], pos1[1], -pos1[2]}); - - p3m_add_pair_force(q, d, d.norm(), force); - } - - if (pos1[2] > (elc_params.h - elc_params.space_layer)) { - auto const q = elc_params.delta_mid_top * q1q2; - auto const d = box_geo.get_mi_vector( - pos2, {pos1[0], pos1[1], 2 * elc_params.h - pos1[2]}); - - p3m_add_pair_force(q, d, d.norm(), force); - } - - return force; -} - -///////////////////////////////////////////////////////////////////////////////////// - -double ELC_P3M_dielectric_layers_energy_contribution( - Utils::Vector3d const &pos1, Utils::Vector3d const &pos2, double q1q2) { - double eng = 0.0; - - if (pos1[2] < elc_params.space_layer) { - auto const q = elc_params.delta_mid_bot * q1q2; - - eng += p3m_pair_energy( - q, box_geo.get_mi_vector(pos2, {pos1[0], pos1[1], -pos1[2]}).norm()); - } - - if (pos1[2] > (elc_params.h - elc_params.space_layer)) { - auto const q = elc_params.delta_mid_top * q1q2; - eng += p3m_pair_energy( - q, - box_geo - .get_mi_vector(pos2, {pos1[0], pos1[1], 2 * elc_params.h - pos1[2]}) - .norm()); - } - - return eng; -} - -double ELC_P3M_dielectric_layers_energy_contribution(Particle const &p1, - Particle const &p2) { - double eng = 0.0; - - auto const pos1 = p1.r.p; - auto const pos2 = p2.r.p; - auto const q1q2 = p1.p.q * p2.p.q; - - eng += ELC_P3M_dielectric_layers_energy_contribution(pos1, pos2, q1q2); - eng += ELC_P3M_dielectric_layers_energy_contribution(pos2, pos1, q1q2); - - return eng; -} - -////////////////////////////////////////////////////////////////////////////////// - -double ELC_P3M_dielectric_layers_energy_self(ParticleRange const &particles) { - double eng = 0.0; - - // Loop cell neighbors - for (auto const &p : particles) { - eng += ELC_P3M_dielectric_layers_energy_contribution(p.r.p, p.r.p, - p.p.q * p.p.q); - } - - return eng; -} - -///////////////////////////////////////////////////////////////////////////////// - -void ELC_P3M_modify_p3m_sums_both(ParticleRange const &particles) { - double node_sums[3], tot_sums[3]; - - for (int i = 0; i < 3; i++) { - node_sums[i] = 0.0; - tot_sums[i] = 0.0; - } - - for (auto const &p : particles) { - if (p.p.q != 0.0) { - - node_sums[0] += 1.0; - node_sums[1] += Utils::sqr(p.p.q); - node_sums[2] += p.p.q; - - if (p.r.p[2] < elc_params.space_layer) { - - node_sums[0] += 1.0; - node_sums[1] += Utils::sqr(elc_params.delta_mid_bot * p.p.q); - node_sums[2] += elc_params.delta_mid_bot * p.p.q; - } - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - - node_sums[0] += 1.0; - node_sums[1] += Utils::sqr(elc_params.delta_mid_top * p.p.q); - node_sums[2] += elc_params.delta_mid_top * p.p.q; - } - } - } - - MPI_Allreduce(node_sums, tot_sums, 3, MPI_DOUBLE, MPI_SUM, comm_cart); - p3m.sum_qpart = (int)(tot_sums[0] + 0.1); - p3m.sum_q2 = tot_sums[1]; - p3m.square_sum_q = Utils::sqr(tot_sums[2]); -} - -void ELC_P3M_modify_p3m_sums_image(ParticleRange const &particles) { - double node_sums[3], tot_sums[3]; - - for (int i = 0; i < 3; i++) { - node_sums[i] = 0.0; - tot_sums[i] = 0.0; - } - - for (auto const &p : particles) { - if (p.p.q != 0.0) { - - if (p.r.p[2] < elc_params.space_layer) { - - node_sums[0] += 1.0; - node_sums[1] += Utils::sqr(elc_params.delta_mid_bot * p.p.q); - node_sums[2] += elc_params.delta_mid_bot * p.p.q; - } - if (p.r.p[2] > (elc_params.h - elc_params.space_layer)) { - - node_sums[0] += 1.0; - node_sums[1] += Utils::sqr(elc_params.delta_mid_top * p.p.q); - node_sums[2] += elc_params.delta_mid_top * p.p.q; - } - } - } - - MPI_Allreduce(node_sums, tot_sums, 3, MPI_DOUBLE, MPI_SUM, comm_cart); - - p3m.sum_qpart = (int)(tot_sums[0] + 0.1); - p3m.sum_q2 = tot_sums[1]; - p3m.square_sum_q = Utils::sqr(tot_sums[2]); -} - -// this function is required in force.cpp for energy evaluation -void ELC_P3M_restore_p3m_sums(ParticleRange const &particles) { - double node_sums[3], tot_sums[3]; - - for (int i = 0; i < 3; i++) { - node_sums[i] = 0.0; - tot_sums[i] = 0.0; - } - - for (auto const &p : particles) { - if (p.p.q != 0.0) { - - node_sums[0] += 1.0; - node_sums[1] += Utils::sqr(p.p.q); - node_sums[2] += p.p.q; - } - } - - MPI_Allreduce(node_sums, tot_sums, 3, MPI_DOUBLE, MPI_SUM, comm_cart); - - p3m.sum_qpart = (int)(tot_sums[0] + 0.1); - p3m.sum_q2 = tot_sums[1]; - p3m.square_sum_q = Utils::sqr(tot_sums[2]); -} - -#endif diff --git a/src/core/electrostatics_magnetostatics/elc.hpp b/src/core/electrostatics_magnetostatics/elc.hpp deleted file mode 100644 index 8079a6accf3..00000000000 --- a/src/core/electrostatics_magnetostatics/elc.hpp +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - * \brief ELC algorithm for long-range Coulomb interactions. - * - * Implementation of the ELC method for the calculation of the electrostatic - * interaction in two dimensional periodic systems. For details on the method - * see MMM in general. The ELC method works together with any three-dimensional - * method, for example \ref p3m.hpp "P3M", with metallic boundary conditions. - */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_ELC_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_ELC_HPP - -#include "config.hpp" - -#ifdef P3M - -#include "Particle.hpp" -#include "ParticleRange.hpp" - -#include - -/** @brief Parameters for the ELC method */ -struct ELCParameters { - /** Maximal allowed pairwise error for the potential and force. - * Used at least by the near formula, since this does the error control at - * runtime. - */ - double maxPWerror; - /** Cutoff of the exponential sum. Since in all other MMM methods this is - * the far formula, we call it here the same, although in the ELC context - * it does not make much sense. - */ - double far_cut; - /** Squared value of #far_cut. */ - double far_cut2; - /** Size of the empty gap. Note that ELC relies on the user to make sure - * that this condition is fulfilled. - */ - double gap_size; - /** Flag whether #far_cut was set by the user, or calculated by ESPResSo. - * In the latter case, the cutoff will be adapted if important parameters, - * such as the box dimensions, change. - */ - bool far_calculated; - /** Flag whether the box is neutralized by a homogeneous background. - * If true, use a homogeneous neutralizing background for nonneutral - * systems. Unlike the 3D case, this background adds an additional - * force pointing towards the system center, so be careful with this. - */ - bool neutralize; - - /// flag whether there is any dielectric contrast in the system. - bool dielectric_contrast_on; - - /// dielectric contrast in the upper part of the simulation cell. - double delta_mid_top; - /// dielectric contrast in the lower part of the simulation cell. - double delta_mid_bot; - /// @brief Flag whether a const. potential is applied. - bool const_pot; - /// @brief Const. potential. - double pot_diff; - - /** Minimal distance of two charges for which the far formula is used. - * For plain ELC, this equals #gap_size, but for dielectric ELC it is - * only 1/3 of that. - */ - double minimal_dist; - /** Layer around the dielectric contrast in which we trick around. */ - double space_layer; - /** The space that is finally left. */ - double space_box; - /** Up to where particles can be found. */ - double h; -}; -extern ELCParameters elc_params; - -/** Set parameters for ELC. - * @param maxPWerror @copybrief ELCParameters::maxPWerror - * Note that this counts for the plain 1/r contribution - * alone, without the prefactor and the charge prefactor. - * @param min_dist @copybrief ELCParameters::minimal_dist - * @param far_cut @copybrief ELCParameters::far_cut - * If -1, the cutoff is automatically calculated using - * the error formulas. - * @param neutralize whether to add a neutralizing background. - * WARNING: This background exerts forces, which are - * dependent on the simulation box; especially the gap - * size enters into the value of the forces. - * @param delta_mid_top @copybrief ELCParameters::delta_mid_top - * @param delta_mid_bot @copybrief ELCParameters::delta_mid_bot - * @param const_pot @copybrief ELCParameters::const_pot - * @param pot_diff @copybrief ELCParameters::pot_diff - */ -void ELC_set_params(double maxPWerror, double min_dist, double far_cut, - bool neutralize, double delta_mid_top, double delta_mid_bot, - bool const_pot, double pot_diff); - -/// the force calculation -void ELC_add_force(const ParticleRange &particles); - -/// the energy calculation -double ELC_energy(const ParticleRange &particles); - -/// check the ELC parameters -void ELC_sanity_checks(ELCParameters const ¶ms); - -/// initialize the ELC constants -void ELC_init(); - -/// pairwise contributions from the lowest and top layers to the energy -double ELC_P3M_dielectric_layers_energy_contribution(Particle const &p1, - Particle const &p2); -/// pairwise contributions from the lowest and top layers to the force -Utils::Vector3d ELC_P3M_dielectric_layers_force_contribution( - const Utils::Vector3d &pos1, const Utils::Vector3d &pos2, double q1q2); -/// self energies of top and bottom layers with their virtual images -double ELC_P3M_dielectric_layers_energy_self(const ParticleRange &particles); -/// forces of particles in border layers with themselves -void ELC_P3M_self_forces(const ParticleRange &particles); - -/// assign the additional, virtual charges, used only in energy.cpp -void ELC_p3m_charge_assign_both(const ParticleRange &particles); -/// assign the additional, virtual charges, used only in energy.cpp -void ELC_p3m_charge_assign_image(const ParticleRange &particles); - -/// take into account the virtual charges in the charge sums, used in energy.cpp -void ELC_P3M_modify_p3m_sums_both(const ParticleRange &particles); -/// take into account the virtual charges in the charge sums, used in energy.cpp -void ELC_P3M_modify_p3m_sums_image(const ParticleRange &particles); - -/// assign the additional, virtual charges, used only in energy.cpp -void ELC_P3M_restore_p3m_sums(const ParticleRange &particles); - -#endif // P3M - -#endif diff --git a/src/core/electrostatics_magnetostatics/icc.cpp b/src/core/electrostatics_magnetostatics/icc.cpp deleted file mode 100644 index b93f56002b7..00000000000 --- a/src/core/electrostatics_magnetostatics/icc.cpp +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/** \file - * Functions to compute the electric field acting on the induced charges, - * excluding forces other than the electrostatic ones. Detailed information - * about the ICC* method is included in the corresponding header file - * \ref icc.hpp. - */ - -#include "config.hpp" - -#ifdef ELECTROSTATICS - -#include "icc.hpp" - -#include "Particle.hpp" -#include "ParticleRange.hpp" -#include "cell_system/CellStructure.hpp" -#include "cells.hpp" -#include "communication.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/coulomb_inline.hpp" -#include "errorhandling.hpp" -#include "event.hpp" - -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -icc_struct icc_cfg; - -/** Calculate the electrostatic forces between source charges (= real charges) - * and wall charges. For each electrostatic method, the proper functions - * for short- and long-range parts are called. Long-range parts are calculated - * directly, short-range parts need helper functions according to the particle - * data organisation. This is a modified version of \ref force_calc. - */ -void force_calc_icc(CellStructure &cell_structure, - const ParticleRange &particles, - const ParticleRange &ghost_particles); - -/** Variant of @ref add_non_bonded_pair_force where only %Coulomb - * contributions are calculated - */ -inline void add_non_bonded_pair_force_icc(Particle &p1, Particle &p2, - Utils::Vector3d const &d, - double dist) { - auto forces = Coulomb::pair_force(p1, p2, d, dist); - - p1.f.f += std::get<0>(forces); - p2.f.f -= std::get<0>(forces); -#ifdef P3M - p1.f.f += std::get<1>(forces); - p2.f.f += std::get<2>(forces); -#endif -} - -void icc_iteration(CellStructure &cell_structure, - const ParticleRange &particles, - const ParticleRange &ghost_particles) { - if (icc_cfg.n_icc == 0) - return; - - Coulomb::icc_sanity_check(); - - auto const pref = 1.0 / (coulomb.prefactor * 2 * Utils::pi()); - icc_cfg.citeration = 0; - - double globalmax = 0.; - - for (int j = 0; j < icc_cfg.num_iteration; j++) { - double charge_density_max = 0.; - - // calculate electrostatic forces (SR+LR) excluding self-interactions - force_calc_icc(cell_structure, particles, ghost_particles); - cell_structure.ghosts_reduce_forces(); - - double diff = 0; - - for (auto &p : particles) { - if (p.p.identity < icc_cfg.n_icc + icc_cfg.first_id && - p.p.identity >= icc_cfg.first_id) { - auto const id = p.p.identity - icc_cfg.first_id; - /* the dielectric-related prefactor: */ - auto const del_eps = - (icc_cfg.ein[id] - icc_cfg.eout) / (icc_cfg.ein[id] + icc_cfg.eout); - /* calculate the electric field at the certain position */ - auto const local_e_field = p.f.f / p.p.q + icc_cfg.ext_field; - - if (local_e_field.norm2() == 0) { - runtimeErrorMsg() - << "ICC found zero electric field on a charge. This must " - "never happen"; - } - - auto const charge_density_old = p.p.q / icc_cfg.areas[id]; - - charge_density_max = - std::max(charge_density_max, std::abs(charge_density_old)); - - auto const charge_density_update = - del_eps * pref * (local_e_field * icc_cfg.normals[id]) + - 2 * icc_cfg.eout / (icc_cfg.eout + icc_cfg.ein[id]) * - icc_cfg.sigma[id]; - /* relative variation: never use an estimator which can be negative - * here */ - auto const charge_density_new = - (1. - icc_cfg.relax) * charge_density_old + - (icc_cfg.relax) * charge_density_update; - - /* Take the largest error to check for convergence */ - auto const relative_difference = - std::abs((charge_density_new - charge_density_old) / - (charge_density_max + - std::abs(charge_density_new + charge_density_old))); - - diff = std::max(diff, relative_difference); - - p.p.q = charge_density_new * icc_cfg.areas[id]; - - /* check if the charge now is more than 1e6, to determine if ICC still - * leads to reasonable results. This is kind of an arbitrary measure - * but does a good job spotting divergence! */ - if (std::abs(p.p.q) > 1e6) { - runtimeErrorMsg() - << "too big charge assignment in icc! q >1e6 , assigned " - "charge= " - << p.p.q; - - diff = 1e90; /* A very high value is used as error code */ - break; - } - } - } /* cell particles */ - /* Update charges on ghosts. */ - cell_structure.ghosts_update(Cells::DATA_PART_PROPERTIES); - - icc_cfg.citeration++; - - boost::mpi::all_reduce(comm_cart, diff, globalmax, - boost::mpi::maximum()); - - if (globalmax < icc_cfg.convergence) - break; - } /* iteration */ - - if (globalmax > icc_cfg.convergence) { - runtimeErrorMsg() - << "ICC failed to converge in the given number of maximal steps."; - } - - on_particle_charge_change(); -} - -void force_calc_icc(CellStructure &cell_structure, - const ParticleRange &particles, - const ParticleRange &ghost_particles) { - // reset forces - for (auto &p : particles) { - p.f.f = {}; - } - for (auto &p : ghost_particles) { - p.f.f = {}; - } - - // calc ICC forces - cell_structure.non_bonded_loop( - [](Particle &p1, Particle &p2, Distance const &d) { - add_non_bonded_pair_force_icc(p1, p2, d.vec21, sqrt(d.dist2)); - }); - - Coulomb::calc_long_range_force(particles); -} - -void mpi_icc_init_local(const icc_struct &icc_cfg_) { - icc_cfg = icc_cfg_; - - on_particle_charge_change(); - check_runtime_errors(comm_cart); -} - -REGISTER_CALLBACK(mpi_icc_init_local) - -int mpi_icc_init() { - mpi_call(mpi_icc_init_local, icc_cfg); - - on_particle_charge_change(); - return check_runtime_errors(comm_cart); -} - -void icc_set_params(int n_icc, double convergence, double relaxation, - Utils::Vector3d const &ext_field, int max_iterations, - int first_id, double eps_out, std::vector &areas, - std::vector &e_in, std::vector &sigma, - std::vector &normals) { - if (convergence <= 0) - throw std::runtime_error("ICC: invalid convergence value. " + - std::to_string(convergence)); - if (relaxation < 0 or relaxation > 2) - throw std::runtime_error("ICC: invalid relaxation value. " + - std::to_string(relaxation)); - if (max_iterations <= 0) - throw std::runtime_error("ICC: invalid max_iterations. " + - std::to_string(max_iterations)); - if (first_id < 0) - throw std::runtime_error("ICC: invalid first_id. " + - std::to_string(first_id)); - if (eps_out <= 0) - throw std::runtime_error("ICC: invalid eps_out. " + - std::to_string(eps_out)); - - assert(n_icc >= 0); - assert(areas.size() == n_icc); - assert(e_in.size() == n_icc); - assert(sigma.size() == n_icc); - assert(normals.size() == n_icc); - - icc_cfg.n_icc = n_icc; - icc_cfg.convergence = convergence; - icc_cfg.relax = relaxation; - icc_cfg.ext_field = ext_field; - icc_cfg.num_iteration = max_iterations; - icc_cfg.first_id = first_id; - icc_cfg.eout = eps_out; - - icc_cfg.areas = std::move(areas); - icc_cfg.ein = std::move(e_in); - icc_cfg.sigma = std::move(sigma); - icc_cfg.normals = std::move(normals); - - mpi_icc_init(); -} - -void icc_deactivate() { - icc_cfg.n_icc = 0; - icc_cfg.areas.resize(0); - icc_cfg.ein.resize(0); - icc_cfg.normals.resize(0); - icc_cfg.sigma.resize(0); - - mpi_icc_init(); -} -#endif diff --git a/src/core/electrostatics_magnetostatics/icc.hpp b/src/core/electrostatics_magnetostatics/icc.hpp deleted file mode 100644 index 92cbec97fff..00000000000 --- a/src/core/electrostatics_magnetostatics/icc.hpp +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - * - * ICC is a method that allows to take into account the influence - * of arbitrarily shaped dielectric interfaces. The dielectric - * properties of a dielectric medium in the bulk of the simulation - * box are taken into account by reproducing the jump in the electric - * field at the interface with charge surface segments. The charge - * density of the surface segments have to be determined - * self-consistently using an iterative scheme. It can at present - * be used with P3M, ELCP3M and MMM1D. For details see: @cite tyagi10a - * - * To set up ICC, first the dielectric boundary has to be modeled - * by ESPResSo particles n_0...n_0+n where n_0 and n have to be passed - * as a parameter to ICC. - * - * For the determination of the induced charges only the forces - * acting on the induced charges has to be determined. As P3M and the - * other Coulomb solvers calculate all mutual forces, the force - * calculation was modified to avoid the calculation of the short - * range part of the source-source force calculation. For different - * particle data organisation schemes this is performed differently. - */ - -#ifndef CORE_ICC_HPP -#define CORE_ICC_HPP - -#include "config.hpp" - -#if defined(ELECTROSTATICS) - -#include "ParticleRange.hpp" -#include "cell_system/CellStructure.hpp" - -#include - -#include - -/** ICC data structure */ -struct icc_struct { - /** First id of ICC particle */ - int n_icc; - /** maximum number of iterations */ - int num_iteration = 30; - /** bulk dielectric constant */ - double eout; - /** areas of the particles */ - std::vector areas; - /** dielectric constants of the particles */ - std::vector ein; - /** surface charge density of the particles */ - std::vector sigma; - /** convergence criteria */ - double convergence = 1e-2; - /** surface normal vectors */ - std::vector normals; - /** external electric field */ - Utils::Vector3d ext_field = {0, 0, 0}; - /** relaxation parameter */ - double relax; - /** last number of iterations */ - int citeration = 0; - /** first ICC particle id */ - int first_id = 0; - - template - void serialize(Archive &ar, long int /* version */) { - ar &n_icc; - ar &num_iteration; - ar &first_id; - ar &convergence; - ar &eout; - ar &relax; - ar &areas; - ar &ein; - ar &normals; - ar σ - ar &ext_field; - ar &citeration; - } -}; - -/** ICC parameters */ -extern icc_struct icc_cfg; - -/** The main iterative scheme, where the surface element charges are calculated - * self-consistently. - */ -void icc_iteration(CellStructure &cell_structure, - const ParticleRange &particles, - const ParticleRange &ghost_particles); - -/** Perform ICC initialization. - * @return non-zero value on error - */ -int mpi_icc_init(); - -/** Set ICC parameters - */ -void icc_set_params(int n_ic, double convergence, double relaxation, - Utils::Vector3d const &ext_field, int max_iterations, - int first_id, double eps_out, std::vector &areas, - std::vector &e_in, std::vector &sigma, - std::vector &normals); - -/** clear ICC vector allocations - */ -void icc_deactivate(); - -#endif /* ELECTROSTATICS */ -#endif /* CORE_ICC_HPP */ diff --git a/src/core/electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp b/src/core/electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp deleted file mode 100644 index 7565f32cbe9..00000000000 --- a/src/core/electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef MAG_NON_P3M_H -#define MAG_NON_P3M_H -/** \file - * All 3d non-P3M methods to deal with the magnetic dipoles - * - * DAWAANR => Dipolar All With All And No Replica - * Handling of a system of dipoles where no replicas exist. - * Assumes minimum image convention for those axis in which the - * system is periodic. - * - * MDDS => Magnetic Dipoles Direct Sum - * Calculate dipole-dipole interaction of a periodic system - * by explicitly summing the dipole-dipole interaction over several copies of - * the system. - * Uses spherical summation order. - * - */ -#include "config.hpp" - -#ifdef DIPOLES - -#include "Particle.hpp" -#include "ParticleRange.hpp" - -/* ============================================================================= - DAWAANR => DIPOLAR ALL WITH ALL AND NO REPLICA - ============================================================================= -*/ - -/** Core of the DAWAANR method: here you compute all the magnetic forces, - * torques and the magnetic energy for the whole system - */ -double dawaanr_calculations(bool force_flag, bool energy_flag, - ParticleRange const &particles); - -/** Switch on DAWAANR magnetostatics. */ -void dawaanr_set_params(); - -/* ============================================================================= - DIRECT SUM FOR MAGNETIC SYSTEMS - ============================================================================= -*/ - -/** Sanity checks for the magnetic dipolar direct sum. */ -void mdds_sanity_checks(); - -/** Core of the method: here you compute all the magnetic forces, torques and - * the energy for the whole system using direct sum - */ -double magnetic_dipolar_direct_sum_calculations(bool force_flag, - bool energy_flag, - ParticleRange const &particles); - -/** Switch on direct sum magnetostatics. - * @param n_replica Number of replicas to be taken for the explicit summation - */ -void mdds_set_params(int n_replica); - -int mdds_get_n_replica(); - -#endif /*of ifdef DIPOLES */ -#endif /* of ifndef MAG_NON_P3M_H */ diff --git a/src/core/electrostatics_magnetostatics/mdlc_correction.cpp b/src/core/electrostatics_magnetostatics/mdlc_correction.cpp deleted file mode 100644 index 5885741a92f..00000000000 --- a/src/core/electrostatics_magnetostatics/mdlc_correction.cpp +++ /dev/null @@ -1,518 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - */ - -#include "electrostatics_magnetostatics/mdlc_correction.hpp" - -#ifdef DIPOLES -#include "Particle.hpp" -#include "cells.hpp" -#include "communication.hpp" -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" -#include "electrostatics_magnetostatics/p3m-dipolar.hpp" -#include "errorhandling.hpp" -#include "grid.hpp" -#include "particle_data.hpp" -#include "particle_node.hpp" - -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -DLC_struct dlc_params = {1e100, 0., 0., false, 0.}; - -/** Checks if a magnetic particle is in the forbidden gap region - */ -inline void check_gap_mdlc(const Particle &p) { - if (p.p.dipm != 0.0) { - auto const z = p.r.p[2]; - if (z < 0.0 or z > dlc_params.h) { - runtimeErrorMsg() << "Particle " << p.p.identity << " entered MDLC gap " - << "region by " << z - ((z < 0.) ? 0. : dlc_params.h); - } - } -} - -/** Calculate the maximal dipole moment in the system */ -double calc_mu_max() { - auto const local_particles = cell_structure.local_particles(); - auto const mu_max_local = std::accumulate( - local_particles.begin(), local_particles.end(), 0.0, - [](double mu, Particle const &p) { return std::max(mu, p.p.dipm); }); - - double mu_max; - boost::mpi::reduce(comm_cart, mu_max_local, mu_max, - boost::mpi::maximum(), 0); - return mu_max; -} - -REGISTER_CALLBACK_MAIN_RANK(calc_mu_max) - -inline double g1_DLC_dip(double g, double x) { - auto const c = g / x; - auto const cc2 = c * c; - auto const x3 = x * x * x; - auto const a = g * g * g / x + 1.5 * cc2 + 1.5 * g / x3 + 0.75 / (x3 * x); - return a; -} - -inline double g2_DLC_dip(double g, double x) { - auto const x2 = x * x; - auto const a = g * g / x + 2.0 * g / x2 + 2.0 / (x2 * x); - return a; -} - -/** Compute the box magnetic dipole. */ -inline Utils::Vector3d calc_slab_dipole(const ParticleRange &particles) { - - Utils::Vector3d local_dip{}; - for (auto const &p : particles) { - if (p.p.dipm != 0.0) { - local_dip += p.calc_dip(); - } - } - - return boost::mpi::all_reduce(comm_cart, local_dip, std::plus<>()); -} - -/** Compute the dipolar DLC corrections for forces and torques. - * %Algorithm implemented accordingly to @cite brodka04a. - */ -double get_DLC_dipolar(int kcut, std::vector &fs, - std::vector &ts, - const ParticleRange &particles) { - auto const n_local_particles = particles.size(); - - std::vector ReSjp(n_local_particles), ReSjm(n_local_particles); - std::vector ImSjp(n_local_particles), ImSjm(n_local_particles); - std::vector ReGrad_Mup(n_local_particles), - ImGrad_Mup(n_local_particles); - std::vector ReGrad_Mum(n_local_particles), - ImGrad_Mum(n_local_particles); - double s1, s2, s3, s4; - double s1z, s2z, s3z, s4z; - double ss; - - auto const facux = 2.0 * Utils::pi() * box_geo.length_inv()[0]; - auto const facuy = 2.0 * Utils::pi() * box_geo.length_inv()[1]; - double energy = 0.0; - - for (int ix = -kcut; ix <= +kcut; ix++) { - for (int iy = -kcut; iy <= +kcut; iy++) { - if (!(ix == 0 && iy == 0)) { - auto const gx = static_cast(ix) * facux; - auto const gy = static_cast(iy) * facuy; - - auto const gr = sqrt(gx * gx + gy * gy); - - // We assume short slab direction is z direction - auto const fa1 = 1. / (gr * (exp(gr * box_geo.length()[2]) - 1.0)); - - // ... Compute S+,(S+)*,S-,(S-)*, and Spj,Smj for the current g - - double S[4] = {0.0, 0.0, 0.0, 0.0}; // S of Brodka method, or is S[4] = - // {Re(S+), Im(S+), Re(S-), Im(S-)} - int ip = 0; - - for (auto const &p : particles) { - if (p.p.dipm > 0) { - Utils::Vector3d const dip = p.calc_dip(); - - auto const a = gx * dip[0] + gy * dip[1]; - auto const b = gr * dip[2]; - auto const er = gx * p.r.p[0] + gy * p.r.p[1]; - auto const ez = gr * p.r.p[2]; - auto const c = cos(er); - auto const d = sin(er); - auto const f = exp(ez); - - ReSjp[ip] = (b * c - a * d) * f; - ImSjp[ip] = (c * a + b * d) * f; - ReSjm[ip] = (-b * c - a * d) / f; - ImSjm[ip] = (c * a - b * d) / f; - ReGrad_Mup[ip] = c * f; - ReGrad_Mum[ip] = c / f; - ImGrad_Mup[ip] = d * f; - ImGrad_Mum[ip] = d / f; - - S[0] += ReSjp[ip]; - S[1] += ImSjp[ip]; - S[2] += ReSjm[ip]; - S[3] += ImSjm[ip]; - } - ip++; - } - - MPI_Allreduce(MPI_IN_PLACE, S, 4, MPI_DOUBLE, MPI_SUM, comm_cart); - - // We compute the contribution to the energy ............ - - // s2=(ReSm*ReSp+ImSm*ImSp); s2=s1!!! - - energy += fa1 * ((S[0] * S[2] + S[1] * S[3]) * 2.0); - - // ... Now we can compute the contributions to E,Fj,Ej for the current - // g-value - ip = 0; - for (auto &p : particles) { - if (p.p.dipm > 0) { - // We compute the contributions to the forces ............ - - s1 = -(-ReSjp[ip] * S[3] + ImSjp[ip] * S[2]); - s2 = +(ReSjm[ip] * S[1] - ImSjm[ip] * S[0]); - s3 = -(-ReSjm[ip] * S[1] + ImSjm[ip] * S[0]); - s4 = +(ReSjp[ip] * S[3] - ImSjp[ip] * S[2]); - - s1z = +(ReSjp[ip] * S[2] + ImSjp[ip] * S[3]); - s2z = -(ReSjm[ip] * S[0] + ImSjm[ip] * S[1]); - s3z = -(ReSjm[ip] * S[0] + ImSjm[ip] * S[1]); - s4z = +(ReSjp[ip] * S[2] + ImSjp[ip] * S[3]); - - ss = s1 + s2 + s3 + s4; - fs[ip][0] += fa1 * gx * ss; - fs[ip][1] += fa1 * gy * ss; - fs[ip][2] += fa1 * gr * (s1z + s2z + s3z + s4z); - - // We compute the contributions to the electrical field - // ............ - - s1 = -(-ReGrad_Mup[ip] * S[3] + ImGrad_Mup[ip] * S[2]); - s2 = +(ReGrad_Mum[ip] * S[1] - ImGrad_Mum[ip] * S[0]); - s3 = -(-ReGrad_Mum[ip] * S[1] + ImGrad_Mum[ip] * S[0]); - s4 = +(ReGrad_Mup[ip] * S[3] - ImGrad_Mup[ip] * S[2]); - - s1z = +(ReGrad_Mup[ip] * S[2] + ImGrad_Mup[ip] * S[3]); - s2z = -(ReGrad_Mum[ip] * S[0] + ImGrad_Mum[ip] * S[1]); - s3z = -(ReGrad_Mum[ip] * S[0] + ImGrad_Mum[ip] * S[1]); - s4z = +(ReGrad_Mup[ip] * S[2] + ImGrad_Mup[ip] * S[3]); - - ss = s1 + s2 + s3 + s4; - ts[ip][0] += fa1 * gx * ss; - ts[ip][1] += fa1 * gy * ss; - ts[ip][2] += fa1 * gr * (s1z + s2z + s3z + s4z); - } // if dipm>0 .... - ip++; - } // loop j - } // end of if(ii> ... - } - } // end of loops for gx,gy - - // Convert from the corrections to the Electrical field to the corrections - // for the torques .... - - int ip = 0; - for (auto const &p : particles) { - if (p.p.dipm > 0) { - ts[ip] = vector_product(p.calc_dip(), ts[ip]); - } - ip++; - } - - // Multiply by the factors we have left during the loops - - auto const piarea = - Utils::pi() * box_geo.length_inv()[0] * box_geo.length_inv()[1]; - - for (int j = 0; j < n_local_particles; j++) { - fs[j] *= piarea; - ts[j] *= piarea; - } - - energy *= (-piarea); - - return energy; -} - -/** Compute the dipolar DLC corrections - * %Algorithm implemented accordingly to @cite brodka04a. - */ -double get_DLC_energy_dipolar(int kcut, const ParticleRange &particles) { - auto const facux = 2.0 * Utils::pi() * box_geo.length_inv()[0]; - auto const facuy = 2.0 * Utils::pi() * box_geo.length_inv()[1]; - - double energy = 0.0; - for (int ix = -kcut; ix <= +kcut; ix++) { - for (int iy = -kcut; iy <= +kcut; iy++) { - - if (!(ix == 0 && iy == 0)) { - auto const gx = static_cast(ix) * facux; - auto const gy = static_cast(iy) * facuy; - auto const gr = sqrt(gx * gx + gy * gy); - // We assume short slab direction is z direction - auto const fa1 = 1. / (gr * (exp(gr * box_geo.length()[2]) - 1.0)); - - // ... Compute S+,(S+)*,S-,(S-)*, and Spj,Smj for the current g - - double S[4] = {0.0, 0.0, 0.0, 0.0}; - int ip = 0; - for (auto const &p : particles) { - if (p.p.dipm > 0) { - const Utils::Vector3d dip = p.calc_dip(); - - auto const a = gx * dip[0] + gy * dip[1]; - { - auto const b = gr * dip[2]; - auto const er = gx * p.r.p[0] + gy * p.r.p[1]; - auto const ez = gr * p.r.p[2]; - auto const c = cos(er); - auto const d = sin(er); - auto const f = exp(ez); - - S[0] += (b * c - a * d) * f; - S[1] += (c * a + b * d) * f; - S[2] += (-b * c - a * d) / f; - S[3] += (c * a - b * d) / f; - } - } - ip++; - } - - double global_S[4]; - MPI_Reduce(S, global_S, 4, MPI_DOUBLE, MPI_SUM, 0, comm_cart); - - // We compute the contribution to the energy ............ - auto const s1 = global_S[0] * global_S[2] + global_S[1] * global_S[3]; - // s2=(ReSm*ReSp+ImSm*ImSp); s2=s1!!! - - energy += fa1 * (s1 * 2.0); - - } // end of if(... - } - } // end of loops for gx,gy - - // Multiply by the factors we have left during the loops - - auto const piarea = - Utils::pi() * box_geo.length_inv()[0] * box_geo.length_inv()[1]; - energy *= (-piarea); - return (this_node == 0) ? energy : 0.0; -} - -/** Compute and add the terms needed to correct the 3D dipolar - * methods when we have a slab geometry - */ -void add_mdlc_force_corrections(const ParticleRange &particles) { - auto const volume = box_geo.volume(); - auto const correc = 4. * Utils::pi() / volume; - - // --- Create arrays that should contain the corrections to - // the forces and torques, and set them to zero. - std::vector dip_DLC_f(particles.size()); - std::vector dip_DLC_t(particles.size()); - - //---- Compute the corrections ---------------------------------- - - // First the DLC correction - get_DLC_dipolar(static_cast(std::round(dlc_params.far_cut)), dip_DLC_f, - dip_DLC_t, particles); - - // Now we compute the correction like Yeh and Klapp to take into account - // the fact that you are using a 3D PBC method which uses spherical - // summation instead of slab-wise summation. - // Slab-wise summation is the one required to apply DLC correction. - // This correction is often called SDC = Shape Dependent Correction. - // See @cite brodka04a. - - auto const box_dip = calc_slab_dipole(particles); - - // --- Transfer the computed corrections to the Forces, Energy and torques - // of the particles - - int ip = 0; - for (auto &p : particles) { - check_gap_mdlc(p); - - if (p.p.dipm != 0.0) { - // SDC correction term is zero for the forces - p.f.f += dipole.prefactor * dip_DLC_f[ip]; - - auto const dip = p.calc_dip(); - // SDC correction for the torques - Utils::Vector3d d = {0.0, 0.0, -correc * box_dip[2]}; -#ifdef DP3M - if (dipole.method == DIPOLAR_MDLC_P3M and - dp3m.params.epsilon != P3M_EPSILON_METALLIC) { - auto const correps = correc / (2.0 * dp3m.params.epsilon + 1.0); - d += correps * box_dip; - } -#endif - p.f.torque += dipole.prefactor * (dip_DLC_t[ip] + vector_product(dip, d)); - } - ip++; - } -} - -/** Compute and add the terms needed to correct the energy of - * 3D dipolar methods when we have a slab geometry - */ -double add_mdlc_energy_corrections(const ParticleRange &particles) { - - auto const volume = box_geo.volume(); - auto const prefactor = dipole.prefactor * 2. * Utils::pi() / volume; - - // Check if particles aren't in the forbidden gap region - // This loop is needed, because there is no other guaranteed - // single pass over all particles in this function. - for (auto const &p : particles) { - check_gap_mdlc(p); - } - - //---- Compute the corrections ---------------------------------- - - // First the DLC correction - auto const k_cut = static_cast(std::round(dlc_params.far_cut)); - double dip_DLC_energy = - dipole.prefactor * get_DLC_energy_dipolar(k_cut, particles); - - // Now we compute the correction like Yeh and Klapp to take into account - // the fact that you are using a 3D PBC method which uses spherical - // summation instead of slab-wise summation. - // Slab-wise summation is the one required to apply DLC correction. - // This correction is often called SDC = Shape Dependent Correction. - // See @cite brodka04a. - - auto const box_dip = calc_slab_dipole(particles); - - if (this_node == 0) { - dip_DLC_energy += prefactor * Utils::sqr(box_dip[2]); -#ifdef DP3M - if (dipole.method == DIPOLAR_MDLC_P3M and - dp3m.params.epsilon != P3M_EPSILON_METALLIC) { - auto const correps = 1.0 / (2.0 * dp3m.params.epsilon + 1.0); - dip_DLC_energy -= prefactor * box_dip.norm2() * correps; - } -#endif - return dip_DLC_energy; - } - return 0.0; -} - -/** Compute the cut-off in the DLC dipolar part to get a certain accuracy. - * We assume particles to have all the same value of the dipolar momentum - * modulus, which is taken as the largest value of mu inside the system. - * If we assume the gap has a width @c gap_size (within - * which there is no particles): Lz = h + gap_size - * - * BE CAREFUL: - * 1. We assume the short distance for the slab to be in the *z*-direction - * 2. You must also tune the other 3D method to the same accuracy, otherwise - * it makes no sense to have an accurate result for DLC-dipolar. - */ -double mdlc_tune_far_cut(DLC_struct const ¶ms) { - /* we take the maximum dipole in the system, to be sure that the errors - * in the other case will be equal or less than for this one */ - auto const mu_max = mpi_call(Communication::Result::main_rank, calc_mu_max); - auto const mu_max_sq = mu_max * mu_max; - - const double n = get_n_part(); - auto const lx = box_geo.length()[0]; - auto const ly = box_geo.length()[1]; - auto const lz = box_geo.length()[2]; - auto const h = params.h; - - if (std::abs(lx - ly) > 0.001) { - throw std::runtime_error("MDLC tuning: box size in x direction is " - "different from y direction. The tuning " - "formula requires both to be equal."); - } - - constexpr int limitkc = 200; - for (int kc = 1; kc < limitkc; kc++) { - auto const gc = kc * 2.0 * Utils::pi() / lx; - auto const fa0 = sqrt(9.0 * exp(+2.0 * gc * h) * g1_DLC_dip(gc, lz - h) + - 9.0 * exp(-2.0 * gc * h) * g1_DLC_dip(gc, lz + h) + - 22.0 * g1_DLC_dip(gc, lz)); - auto const fa1 = 0.5 * sqrt(Utils::pi() / (2.0 * lx * ly)) * fa0; - auto const fa2 = g2_DLC_dip(gc, lz); - auto const de = n * mu_max_sq / (4.0 * (exp(gc * lz) - 1.0)) * (fa1 + fa2); - if (de < params.maxPWerror) { - return static_cast(kc); - } - } - - throw std::runtime_error("MDLC tuning failed: unable to find a proper " - "cut-off for the given accuracy."); -} - -void mdlc_sanity_checks() { - if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { - throw std::runtime_error("MDLC requires periodicity 1 1 1"); - } -} - -void mdlc_set_params(double maxPWerror, double gap_size, double far_cut) { - auto const h = box_geo.length()[2] - gap_size; - if (maxPWerror <= 0.) { - throw std::domain_error("maxPWerror must be > 0"); - } - if (gap_size <= 0.) { - throw std::domain_error("gap_size must be > 0"); - } - if (h < 0.) { - throw std::domain_error("gap size too large"); - } - mdlc_sanity_checks(); - - DipolarInteraction new_method; - switch (dipole.method) { -#ifdef DP3M - case DIPOLAR_MDLC_P3M: - case DIPOLAR_P3M: - new_method = DIPOLAR_MDLC_P3M; - break; -#endif - case DIPOLAR_MDLC_DS: - case DIPOLAR_DS: - new_method = DIPOLAR_MDLC_DS; - fprintf(stderr, "You are not using the P3M method, therefore dp3m.params." - "epsilon unknown, I will assume metallic borders.\n"); - break; - default: - throw std::runtime_error( - "MDLC cannot extend the currently active magnetostatics solver."); - } - - DLC_struct new_dlc_params{maxPWerror, far_cut, gap_size, far_cut == -1., h}; - - if (new_dlc_params.far_calculated) { - new_dlc_params.far_cut = mdlc_tune_far_cut(new_dlc_params); - } - - dlc_params = new_dlc_params; - - Dipole::set_method_local(new_method); - mpi_bcast_coulomb_params(); -} - -#endif // DIPOLES diff --git a/src/core/electrostatics_magnetostatics/mdlc_correction.hpp b/src/core/electrostatics_magnetostatics/mdlc_correction.hpp deleted file mode 100644 index 166baa7731e..00000000000 --- a/src/core/electrostatics_magnetostatics/mdlc_correction.hpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_DLC_DIPOLAR_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_DLC_DIPOLAR_HPP -/** \file - * main header-file for MDLC (magnetic dipolar layer correction). - * - * Developer: Joan J. Cerda. - * - * Purpose: get the corrections for dipolar 3D algorithms - * when applied to a slab geometry and dipolar - * particles. DLC & co - * - * Article: @cite brodka04a - * - * We also include a tuning function that returns the - * cut-off necessary to attend a certain accuracy. - * - * Restrictions: the slab must be such that the z is the short - * direction. Otherwise we get trash. - * - * Limitations: at this moment it is restricted to work with 1 cpu - */ - -#include "config.hpp" - -#ifdef DIPOLES - -#include - -/** parameters for the MDLC method */ -struct DLC_struct { - /** maximal pairwise error of the potential and force */ - double maxPWerror; - - /** Cutoff of the exponential sum. Since in all other MMM methods this is - * the far formula, we call it here the same, although in the ELC context - * it does not make much sense. - */ - double far_cut; - - /** Size of the empty gap. Note that MDLC relies on the user to make sure - * that this condition is fulfilled. - */ - double gap_size; - - /** Flag whether #far_cut was set by the user, or calculated by ESPResSo. - * In the latter case, the cutoff will be adapted if important parameters, - * such as the box dimensions, change. - */ - bool far_calculated; - - /** Up to where particles can be found */ - double h; - - template void serialize(Archive &ar, long int) { - ar &maxPWerror &far_cut &gap_size &far_calculated &h; - } -}; -extern DLC_struct dlc_params; - -void mdlc_set_params(double maxPWerror, double gap_size, double far_cut); -void mdlc_sanity_checks(); -void add_mdlc_force_corrections(const ParticleRange &particles); -double add_mdlc_energy_corrections(const ParticleRange &particles); -#endif // DIPOLES - -#endif diff --git a/src/core/electrostatics_magnetostatics/mmm1d.hpp b/src/core/electrostatics_magnetostatics/mmm1d.hpp deleted file mode 100644 index 38c7a441998..00000000000 --- a/src/core/electrostatics_magnetostatics/mmm1d.hpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - * MMM1D algorithm for long range %Coulomb interactions. - * Implementation of the MMM1D method for the calculation of the electrostatic - * interaction in one-dimensionally periodic systems. For details on the - * method see MMM in general. The MMM1D method works only with the nsquared, - * since neither the near nor far formula can be decomposed. However, this - * implementation is reasonably fast, so that one can use up to 200 charges - * easily in a simulation. - */ -#ifndef MMM1D_H -#define MMM1D_H - -#include "config.hpp" - -#include - -#ifdef ELECTROSTATICS - -/** @brief Parameters for the MMM1D electrostatic interaction */ -struct MMM1DParameters { - /** square of the switching radius */ - double far_switch_radius_2; - /** Maximal allowed pairwise error for the potential and force. - * This error ignores prefactors, i.e. this is for a pure lattice 1/r-sum. - */ - double maxPWerror; - /** cutoff of the Bessel sum. Only used by the GPU implementation */ - int bessel_cutoff; -}; -extern MMM1DParameters mmm1d_params; - -/** Set parameters for MMM1D. - * Most of the parameters can also be tuned automatically. Unlike P3M, this - * tuning is redone automatically whenever parameters change, but not - * immediately if you set these parameters. - * @param switch_rad at which xy-distance the calculation switches from the far - * to the near formula. If -1, this parameter will be tuned automatically. - * @param maxPWerror @copydoc MMM1DParameters::maxPWerror - */ -void MMM1D_set_params(double switch_rad, double maxPWerror); - -/// check that MMM1D can run with the current parameters -bool MMM1D_sanity_checks(); - -/// initialize the MMM1D constants -int MMM1D_init(); - -void add_mmm1d_coulomb_pair_force(double chpref, Utils::Vector3d const &d, - double r, Utils::Vector3d &force); - -double mmm1d_coulomb_pair_energy(double q1q2, Utils::Vector3d const &d, - double r); - -/** Tuning of the parameters which are not set by the user. Tune either the - * @ref MMM1DParameters::far_switch_radius_2 "switching radius" or the - * @ref MMM1DParameters::bessel_cutoff "Bessel cutoff". Call this only - * on the head node. - * - * @param verbose output information about the tuning (tried values and errors) - * @param timings Number of test force calculations - * @retval ES_OK - * @retval ES_ERROR - */ -int mmm1d_tune(int timings, bool verbose); - -#endif -#endif diff --git a/src/core/electrostatics_magnetostatics/p3m-common.cpp b/src/core/electrostatics_magnetostatics/p3m-common.cpp deleted file mode 100644 index 48bbcdbf376..00000000000 --- a/src/core/electrostatics_magnetostatics/p3m-common.cpp +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - * P3M main file. - */ -#include "p3m-common.hpp" - -#if defined(P3M) || defined(DP3M) -#include "communication.hpp" -#include "errorhandling.hpp" - -#include -#include -#include - -#include -#include - -void p3m_add_block(double const *in, double *out, int const start[3], - int const size[3], int const dim[3]) { - /* fast,mid and slow changing indices */ - int f, m, s; - /* linear index of in grid, linear index of out grid */ - int li_in = 0, li_out = 0; - /* offsets for indices in output grid */ - int m_out_offset, s_out_offset; - - li_out = start[2] + (dim[2] * (start[1] + (dim[1] * start[0]))); - m_out_offset = dim[2] - size[2]; - s_out_offset = (dim[2] * (dim[1] - size[1])); - - for (s = 0; s < size[0]; s++) { - for (m = 0; m < size[1]; m++) { - for (f = 0; f < size[2]; f++) { - out[li_out++] += in[li_in++]; - } - li_out += m_out_offset; - } - li_out += s_out_offset; - } -} - -double p3m_analytic_cotangent_sum(int n, double mesh_i, int cao) { - double c, res = 0.0; - c = Utils::sqr(cos(Utils::pi() * mesh_i * (double)n)); - - switch (cao) { - case 1: { - res = 1; - break; - } - case 2: { - res = (1.0 + c * 2.0) / 3.0; - break; - } - case 3: { - res = (2.0 + c * (11.0 + c * 2.0)) / 15.0; - break; - } - case 4: { - res = (17.0 + c * (180.0 + c * (114.0 + c * 4.0))) / 315.0; - break; - } - case 5: { - res = (62.0 + c * (1072.0 + c * (1452.0 + c * (247.0 + c * 2.0)))) / 2835.0; - break; - } - case 6: { - res = (1382.0 + - c * (35396.0 + - c * (83021.0 + c * (34096.0 + c * (2026.0 + c * 4.0))))) / - 155925.0; - break; - } - case 7: { - res = - (21844.0 + - c * (776661.0 + c * (2801040.0 + - c * (2123860.0 + - c * (349500.0 + c * (8166.0 + c * 4.0)))))) / - 6081075.0; - break; - } - default: { - fprintf(stderr, - "%d: INTERNAL_ERROR: The value %d for the interpolation order " - "should not occur!\n", - this_node, cao); - errexit(); - } - } - - return res; -} - -void p3m_calc_local_ca_mesh(P3MLocalMesh &local_mesh, - const P3MParameters ¶ms, - const LocalBox &local_geo, double skin, - double space_layer) { - int i; - int ind[3]; - /* total skin size */ - double full_skin[3]; - - for (i = 0; i < 3; i++) - full_skin[i] = params.cao_cut[i] + skin; - - full_skin[2] += space_layer; - - /* inner left down grid point (global index) */ - for (i = 0; i < 3; i++) - local_mesh.in_ld[i] = - (int)ceil(local_geo.my_left()[i] * params.ai[i] - params.mesh_off[i]); - /* inner up right grid point (global index) */ - for (i = 0; i < 3; i++) - local_mesh.in_ur[i] = - (int)floor(local_geo.my_right()[i] * params.ai[i] - params.mesh_off[i]); - - /* correct roundoff errors at boundary */ - for (i = 0; i < 3; i++) { - if ((local_geo.my_right()[i] * params.ai[i] - params.mesh_off[i]) - - local_mesh.in_ur[i] < - ROUND_ERROR_PREC) - local_mesh.in_ur[i]--; - if (1.0 + (local_geo.my_left()[i] * params.ai[i] - params.mesh_off[i]) - - local_mesh.in_ld[i] < - ROUND_ERROR_PREC) - local_mesh.in_ld[i]--; - } - /* inner grid dimensions */ - for (i = 0; i < 3; i++) - local_mesh.inner[i] = local_mesh.in_ur[i] - local_mesh.in_ld[i] + 1; - /* index of left down grid point in global mesh */ - for (i = 0; i < 3; i++) - local_mesh.ld_ind[i] = - (int)ceil((local_geo.my_left()[i] - full_skin[i]) * params.ai[i] - - params.mesh_off[i]); - /* left down margin */ - for (i = 0; i < 3; i++) - local_mesh.margin[i * 2] = local_mesh.in_ld[i] - local_mesh.ld_ind[i]; - /* up right grid point */ - for (i = 0; i < 3; i++) - ind[i] = - (int)floor((local_geo.my_right()[i] + full_skin[i]) * params.ai[i] - - params.mesh_off[i]); - /* correct roundoff errors at up right boundary */ - for (i = 0; i < 3; i++) - if (((local_geo.my_right()[i] + full_skin[i]) * params.ai[i] - - params.mesh_off[i]) - - ind[i] == - 0) - ind[i]--; - /* up right margin */ - for (i = 0; i < 3; i++) - local_mesh.margin[(i * 2) + 1] = ind[i] - local_mesh.in_ur[i]; - - /* grid dimension */ - local_mesh.size = 1; - for (i = 0; i < 3; i++) { - local_mesh.dim[i] = ind[i] - local_mesh.ld_ind[i] + 1; - local_mesh.size *= local_mesh.dim[i]; - } - /* reduce inner grid indices from global to local */ - for (i = 0; i < 3; i++) - local_mesh.in_ld[i] = local_mesh.margin[i * 2]; - for (i = 0; i < 3; i++) - local_mesh.in_ur[i] = local_mesh.margin[i * 2] + local_mesh.inner[i]; - - local_mesh.q_2_off = local_mesh.dim[2] - params.cao; - local_mesh.q_21_off = local_mesh.dim[2] * (local_mesh.dim[1] - params.cao); -} - -void p3m_calc_lm_ld_pos(P3MLocalMesh &local_mesh, const P3MParameters ¶ms) { - /* spatial position of left down mesh point */ - for (int i = 0; i < 3; i++) { - local_mesh.ld_pos[i] = - (local_mesh.ld_ind[i] + params.mesh_off[i]) * params.a[i]; - } -} - -#endif /* defined(P3M) || defined(DP3M) */ diff --git a/src/core/electrostatics_magnetostatics/p3m-dipolar.cpp b/src/core/electrostatics_magnetostatics/p3m-dipolar.cpp deleted file mode 100644 index 17b63d8352c..00000000000 --- a/src/core/electrostatics_magnetostatics/p3m-dipolar.cpp +++ /dev/null @@ -1,1501 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - * P3M algorithm for long range magnetic dipole-dipole interaction. - * - * @note - * In general the magnetic dipole-dipole functions bear the same name than - * the charge-charge, but adding a "D" in front of the name and replacing - * "charge" by "dipole". In this way one can recognize the similarity of - * the functions while avoiding nasty confusions in their use. - * - * By default the magnetic epsilon is metallic = 0. - * - * The corresponding header file is p3m-dipolar.hpp. - */ - -#include "config.hpp" - -#ifdef DP3M - -#include "electrostatics_magnetostatics/p3m-dipolar.hpp" - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/dp3m_influence_function.hpp" -#include "electrostatics_magnetostatics/fft.hpp" -#include "electrostatics_magnetostatics/p3m-common.hpp" -#include "electrostatics_magnetostatics/p3m_interpolation.hpp" -#include "electrostatics_magnetostatics/p3m_send_mesh.hpp" - -#include "Particle.hpp" -#include "ParticleRange.hpp" -#include "cell_system/CellStructureType.hpp" -#include "cells.hpp" -#include "communication.hpp" -#include "errorhandling.hpp" -#include "grid.hpp" -#include "integrate.hpp" -#include "npt.hpp" -#include "tuning.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -/************************************************ - * DEFINES - ************************************************/ - -#define DP3M_RTBISECTION_ERROR 9999999 - -/************************************************ - * variables - ************************************************/ - -dp3m_data_struct dp3m; - -/** \name Private Functions */ -/**@{*/ - -/** Initialize for magnetic dipoles the (inverse) mesh constant @ref - * P3MParameters::a "a" (@ref P3MParameters::ai "ai") and the - * cutoff for charge assignment @ref P3MParameters::cao_cut "cao_cut". - * - * Function called by @ref dp3m_init() once and by @ref - * dp3m_scaleby_box_l() whenever the box_length changes. - */ -static void dp3m_init_a_ai_cao_cut(); - -/** Checks for correctness for magnetic dipoles in P3M of the cao_cut, - * necessary when the box length changes - */ -static bool dp3m_sanity_checks_boxl(); - -/** Calculate the influence function optimized for the dipolar forces. */ -static void dp3m_calc_influence_function_force(); - -/** Calculate the influence function optimized for the dipolar energy and - * torques. - */ -static void dp3m_calc_influence_function_energy(); - -/** Calculate the constants necessary to correct the dipolar energy to minimize - * the error. - */ -static void dp3m_compute_constants_energy_dipolar(); - -static double dp3m_k_space_error(double box_size, int mesh, int cao, - int n_c_part, double sum_q2, double alpha_L); -/**@}*/ - -/** Compute the dipolar surface terms */ -static double calc_surface_term(bool force_flag, bool energy_flag, - ParticleRange const &particles); - -/** \name P3M Tuning Functions */ -/************************************************************/ -/**@{*/ - -double dp3m_real_space_error(double box_size, double r_cut_iL, int n_c_part, - double sum_q2, double alpha_L); -static void dp3m_tune_aliasing_sums(int nx, int ny, int nz, int mesh, - double mesh_i, int cao, double alpha_L_i, - double *alias1, double *alias2); - -/** Compute the value of alpha through a bisection method. - * Based on eq. (33) @cite wang01a. - */ -double dp3m_rtbisection(double box_size, double r_cut_iL, int n_c_part, - double sum_q2, double x1, double x2, double xacc, - double tuned_accuracy); - -/**@}*/ - -/** Correction of the dipolar p3m-energy. */ -double dp3m_average_dipolar_self_energy() { - auto const start = Utils::Vector3i{dp3m.fft.plan[3].start}; - auto const size = Utils::Vector3i{dp3m.fft.plan[3].new_mesh}; - - auto const node_phi = grid_influence_function_self_energy( - dp3m.params, start, start + size, dp3m.g_energy); - - double phi = 0.0; - boost::mpi::reduce(comm_cart, node_phi, phi, std::plus<>(), 0); - phi /= 3. * box_geo.length()[0] * Utils::int_pow<3>(dp3m.params.mesh[0]); - return phi * Utils::pi(); -} - -/************************************************************/ - -dp3m_data_struct::dp3m_data_struct() { - /* local_mesh is uninitialized */ - /* sm is uninitialized */ - sum_dip_part = 0; - sum_mu2 = 0.0; - - pos_shift = 0.0; - ks_pnum = 0; - - energy_correction = 0.0; -} - -void dp3m_deactivate() { - dp3m.params.alpha = 0.0; - dp3m.params.alpha_L = 0.0; - dp3m.params.r_cut = 0.0; - dp3m.params.r_cut_iL = 0.0; - dp3m.params.mesh[0] = 0; - dp3m.params.mesh[1] = 0; - dp3m.params.mesh[2] = 0; - dp3m.params.cao = 0; -} - -void dp3m_init() { - if (dipole.prefactor <= 0.0) { - // dipolar prefactor is zero: magnetostatics switched off - dp3m.params.r_cut = 0.0; - dp3m.params.r_cut_iL = 0.0; - return; - } - - if (dp3m_sanity_checks(node_grid)) { - return; - } - - dp3m.params.cao3 = Utils::int_pow<3>(dp3m.params.cao); - - /* initializes the (inverse) mesh constant dp3m.params.a (dp3m.params.ai) - * and the cutoff for charge assignment dp3m.params.cao_cut */ - dp3m_init_a_ai_cao_cut(); - - p3m_calc_local_ca_mesh(dp3m.local_mesh, dp3m.params, local_geo, skin, 0.0); - - dp3m.sm.resize(comm_cart, dp3m.local_mesh); - - int ca_mesh_size = fft_init(dp3m.local_mesh.dim, dp3m.local_mesh.margin, - dp3m.params.mesh, dp3m.params.mesh_off, - dp3m.ks_pnum, dp3m.fft, node_grid, comm_cart); - dp3m.rs_mesh.resize(ca_mesh_size); - dp3m.ks_mesh.resize(ca_mesh_size); - - for (auto &val : dp3m.rs_mesh_dip) { - val.resize(ca_mesh_size); - } - - dp3m.calc_differential_operator(); - - /* fix box length dependent constants */ - dp3m_scaleby_box_l(); - - dp3m_count_magnetic_particles(); -} - -/****************** - * functions related to the parsing & tuning of the dipolar parameters - ******************/ - -void dp3m_set_tune_params(double r_cut, int mesh, int cao, double accuracy) { - if (r_cut >= 0) { - dp3m.params.r_cut = r_cut; - dp3m.params.r_cut_iL = r_cut * box_geo.length_inv()[0]; - } - - if (mesh >= 0) - dp3m.params.mesh[2] = dp3m.params.mesh[1] = dp3m.params.mesh[0] = mesh; - - if (cao >= 0) - dp3m.params.cao = cao; - - if (accuracy >= 0) - dp3m.params.accuracy = accuracy; -} - -/*****************************************************************************/ - -void dp3m_set_params(double r_cut, int mesh, int cao, double alpha, - double accuracy) { - if (r_cut < 0) - throw std::runtime_error("DipolarP3M: invalid r_cut"); - - if (mesh < 0) - throw std::runtime_error("DipolarP3M: invalid mesh size"); - - if (cao < 1 || cao > 7) - throw std::runtime_error("DipolarP3M: invalid cao"); - - if (cao > mesh) - throw std::runtime_error("DipolarP3M: cao larger than mesh size"); - - if (alpha <= 0.0 && alpha != -1.0) - throw std::runtime_error("DipolarP3M: invalid alpha"); - - if (accuracy <= 0.0 && accuracy != -1.0) - throw std::runtime_error("DipolarP3M: invalid accuracy"); - - if (dipole.method != DIPOLAR_P3M && dipole.method != DIPOLAR_MDLC_P3M) - Dipole::set_method_local(DIPOLAR_P3M); - - dp3m.params.r_cut = r_cut; - dp3m.params.r_cut_iL = r_cut * box_geo.length_inv()[0]; - dp3m.params.mesh[2] = dp3m.params.mesh[1] = dp3m.params.mesh[0] = mesh; - dp3m.params.cao = cao; - dp3m.params.alpha = alpha; - dp3m.params.alpha_L = alpha * box_geo.length()[0]; - dp3m.params.accuracy = accuracy; - - mpi_bcast_coulomb_params(); -} - -void dp3m_set_mesh_offset(double x, double y, double z) { - if (x == -1.0 && y == -1.0 && z == -1.0) - return; - - if (x < 0.0 || x > 1.0 || y < 0.0 || y > 1.0 || z < 0.0 || z > 1.0) - throw std::runtime_error("DipolarP3M: invalid mesh offset"); - - dp3m.params.mesh_off[0] = x; - dp3m.params.mesh_off[1] = y; - dp3m.params.mesh_off[2] = z; - - mpi_bcast_coulomb_params(); -} - -/** We left the handling of the epsilon, due to portability reasons in - * the future for the electrical dipoles, or if people want to do - * electrical dipoles alone using the magnetic code. Currently unused. - */ -void dp3m_set_eps(double eps) { - dp3m.params.epsilon = eps; - - mpi_bcast_coulomb_params(); -} - -namespace { -template struct AssignDipole { - void operator()(Utils::Vector3d const &real_pos, - Utils::Vector3d const &dip) const { - auto const weights = p3m_calculate_interpolation_weights( - real_pos, dp3m.params.ai, dp3m.local_mesh); - p3m_interpolate(dp3m.local_mesh, weights, [&dip](int ind, double w) { - dp3m.rs_mesh_dip[0][ind] += w * dip[0]; - dp3m.rs_mesh_dip[1][ind] += w * dip[1]; - dp3m.rs_mesh_dip[2][ind] += w * dip[2]; - }); - - dp3m.inter_weights.store(weights); - } -}; -} // namespace - -void dp3m_dipole_assign(const ParticleRange &particles) { - dp3m.inter_weights.reset(dp3m.params.cao); - - /* prepare local FFT mesh */ - for (auto &i : dp3m.rs_mesh_dip) - for (int j = 0; j < dp3m.local_mesh.size; j++) - i[j] = 0.0; - - for (auto const &p : particles) { - if (p.p.dipm != 0.0) { - Utils::integral_parameter(dp3m.params.cao, p.r.p, - p.calc_dip()); - } - } -} - -namespace { -template struct AssignTorques { - void operator()(double prefac, int d_rs, - const ParticleRange &particles) const { - /* particle counter */ - int cp_cnt = 0; - for (auto &p : particles) { - auto const w = dp3m.inter_weights.load(cp_cnt++); - - Utils::Vector3d E{}; - p3m_interpolate(dp3m.local_mesh, w, [&E, d_rs](int ind, double w) { - E[d_rs] += w * dp3m.rs_mesh[ind]; - }); - - p.f.torque -= vector_product(p.calc_dip(), prefac * E); - } - } -}; - -template struct AssignForces { - void operator()(double prefac, int d_rs, - const ParticleRange &particles) const { - /* particle counter */ - int cp_cnt = 0; - for (auto &p : particles) { - auto const w = dp3m.inter_weights.load(cp_cnt++); - - Utils::Vector3d E{}; - - p3m_interpolate(dp3m.local_mesh, w, [&E](int ind, double w) { - E[0] += w * dp3m.rs_mesh_dip[0][ind]; - E[1] += w * dp3m.rs_mesh_dip[1][ind]; - E[2] += w * dp3m.rs_mesh_dip[2][ind]; - }); - - p.f.f[d_rs] += p.calc_dip() * prefac * E; - } - } -}; -} // namespace - -/*****************************************************************************/ - -double dp3m_calc_kspace_forces(bool force_flag, bool energy_flag, - const ParticleRange &particles) { - int i, ind, j[3]; - /* k-space energy */ - double k_space_energy_dip = 0.0; - double tmp0, tmp1; - - auto const dipole_prefac = - dipole.prefactor / Utils::int_pow<3>(dp3m.params.mesh[0]); - - if (dp3m.sum_mu2 > 0) { - /* Gather information for FFT grid inside the nodes domain (inner local - * mesh) and perform forward 3D FFT (Charge Assignment Mesh). */ - std::array meshes = {{dp3m.rs_mesh_dip[0].data(), - dp3m.rs_mesh_dip[1].data(), - dp3m.rs_mesh_dip[2].data()}}; - - dp3m.sm.gather_grid(Utils::make_span(meshes), comm_cart, - dp3m.local_mesh.dim); - - fft_perform_forw(dp3m.rs_mesh_dip[0].data(), dp3m.fft, comm_cart); - fft_perform_forw(dp3m.rs_mesh_dip[1].data(), dp3m.fft, comm_cart); - fft_perform_forw(dp3m.rs_mesh_dip[2].data(), dp3m.fft, comm_cart); - // Note: after these calls, the grids are in the order yzx and not xyz - // anymore!!! - } - - /* === k-space calculations === */ - - /* === k-space energy calculation === */ - if (energy_flag) { - /********************* - Dipolar energy - **********************/ - if (dp3m.sum_mu2 > 0) { - /* i*k differentiation for dipolar gradients: - * |(\Fourier{\vect{mu}}(k)\cdot \vect{k})|^2 */ - ind = 0; - i = 0; - double node_k_space_energy_dip = 0.0; - for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { - for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; j[1]++) { - for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; j[2]++) { - node_k_space_energy_dip += - dp3m.g_energy[i] * - (Utils::sqr( - dp3m.rs_mesh_dip[0][ind] * - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + - dp3m.rs_mesh_dip[1][ind] * - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + - dp3m.rs_mesh_dip[2][ind] * - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]) + - Utils::sqr( - dp3m.rs_mesh_dip[0][ind + 1] * - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + - dp3m.rs_mesh_dip[1][ind + 1] * - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + - dp3m.rs_mesh_dip[2][ind + 1] * - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]])); - ind += 2; - i++; - } - } - } - node_k_space_energy_dip *= - dipole_prefac * Utils::pi() * box_geo.length_inv()[0]; - boost::mpi::reduce(comm_cart, node_k_space_energy_dip, k_space_energy_dip, - std::plus<>(), 0); - - dp3m_compute_constants_energy_dipolar(); - - if (this_node == 0) { - /* self energy correction */ - k_space_energy_dip -= - dipole.prefactor * - (dp3m.sum_mu2 * 2 * - pow(dp3m.params.alpha_L * box_geo.length_inv()[0], 3) * - Utils::sqrt_pi_i() / 3.0); - - auto const volume = box_geo.volume(); - k_space_energy_dip += dipole.prefactor * dp3m.energy_correction / - volume; /* add the dipolar energy correction due - to systematic Madelung-Self effects */ - } - } - } // if (energy_flag) - - /* === k-space force calculation === */ - if (force_flag) { - /**************************** - * DIPOLAR TORQUES (k-space) - ****************************/ - if (dp3m.sum_mu2 > 0) { - /* fill in ks_mesh array for torque calculation */ - ind = 0; - i = 0; - - for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { // j[0]=n_y - for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; - j[1]++) { // j[1]=n_z - for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; - j[2]++) { // j[2]=n_x - // tmp0 = Re(mu)*k, tmp1 = Im(mu)*k - - tmp0 = dp3m.rs_mesh_dip[0][ind] * - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + - dp3m.rs_mesh_dip[1][ind] * - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + - dp3m.rs_mesh_dip[2][ind] * - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; - - tmp1 = dp3m.rs_mesh_dip[0][ind + 1] * - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + - dp3m.rs_mesh_dip[1][ind + 1] * - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + - dp3m.rs_mesh_dip[2][ind + 1] * - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; - - /* the optimal influence function is the same for torques - and energy */ - - dp3m.ks_mesh[ind] = tmp0 * dp3m.g_energy[i]; - dp3m.ks_mesh[ind + 1] = tmp1 * dp3m.g_energy[i]; - ind += 2; - i++; - } - } - } - - /* Force component loop */ - for (int d = 0; d < 3; d++) { - auto const d_rs = (d + dp3m.ks_pnum) % 3; - ind = 0; - for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { - for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; j[1]++) { - for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; j[2]++) { - dp3m.rs_mesh[ind] = - dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * - dp3m.ks_mesh[ind]; - ind++; - dp3m.rs_mesh[ind] = - dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * - dp3m.ks_mesh[ind]; - ind++; - } - } - } - - /* Back FFT force component mesh */ - fft_perform_back(dp3m.rs_mesh.data(), false, dp3m.fft, comm_cart); - /* redistribute force component mesh */ - dp3m.sm.spread_grid(dp3m.rs_mesh.data(), comm_cart, - dp3m.local_mesh.dim); - /* Assign force component from mesh to particle */ - Utils::integral_parameter( - dp3m.params.cao, - dipole_prefac * (2 * Utils::pi() * box_geo.length_inv()[0]), d_rs, - particles); - } - - /*************************** - DIPOLAR FORCES (k-space) - ****************************/ - - // Compute forces after torques because the algorithm below overwrites the - // grids dp3m.rs_mesh_dip ! - // Note: I'll do here 9 inverse FFTs. By symmetry, we can reduce this - // number to 6 ! - /* fill in ks_mesh array for force calculation */ - ind = 0; - i = 0; - for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { // j[0]=n_y - for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; - j[1]++) { // j[1]=n_z - for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; - j[2]++) { // j[2]=n_x - // tmp0 = Im(mu)*k, tmp1 = -Re(mu)*k - tmp0 = dp3m.rs_mesh_dip[0][ind + 1] * - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + - dp3m.rs_mesh_dip[1][ind + 1] * - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + - dp3m.rs_mesh_dip[2][ind + 1] * - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; - tmp1 = dp3m.rs_mesh_dip[0][ind] * - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + - dp3m.rs_mesh_dip[1][ind] * - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + - dp3m.rs_mesh_dip[2][ind] * - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; - dp3m.ks_mesh[ind] = tmp0 * dp3m.g_force[i]; - dp3m.ks_mesh[ind + 1] = -tmp1 * dp3m.g_force[i]; - ind += 2; - i++; - } - } - } - - /* Force component loop */ - for (int d = 0; d < 3; d++) { /* direction in k-space: */ - auto const d_rs = (d + dp3m.ks_pnum) % 3; - ind = 0; - for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; - j[0]++) { // j[0]=n_y - for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; - j[1]++) { // j[1]=n_z - for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; - j[2]++) { // j[2]=n_x - tmp0 = dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * - dp3m.ks_mesh[ind]; - dp3m.rs_mesh_dip[0][ind] = - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] * tmp0; - dp3m.rs_mesh_dip[1][ind] = - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] * tmp0; - dp3m.rs_mesh_dip[2][ind] = - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]] * tmp0; - ind++; - tmp0 = dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * - dp3m.ks_mesh[ind]; - dp3m.rs_mesh_dip[0][ind] = - dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] * tmp0; - dp3m.rs_mesh_dip[1][ind] = - dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] * tmp0; - dp3m.rs_mesh_dip[2][ind] = - dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]] * tmp0; - ind++; - } - } - } - /* Back FFT force component mesh */ - fft_perform_back(dp3m.rs_mesh_dip[0].data(), false, dp3m.fft, - comm_cart); - fft_perform_back(dp3m.rs_mesh_dip[1].data(), false, dp3m.fft, - comm_cart); - fft_perform_back(dp3m.rs_mesh_dip[2].data(), false, dp3m.fft, - comm_cart); - /* redistribute force component mesh */ - std::array meshes = {{dp3m.rs_mesh_dip[0].data(), - dp3m.rs_mesh_dip[1].data(), - dp3m.rs_mesh_dip[2].data()}}; - - dp3m.sm.spread_grid(Utils::make_span(meshes), comm_cart, - dp3m.local_mesh.dim); - /* Assign force component from mesh to particle */ - Utils::integral_parameter( - dp3m.params.cao, - dipole_prefac * pow(2 * Utils::pi() * box_geo.length_inv()[0], 2), - d_rs, particles); - } - } /* if (dp3m.sum_mu2 > 0) */ - } /* if (force_flag) */ - - if (dp3m.params.epsilon != P3M_EPSILON_METALLIC) { - auto const surface_term = - calc_surface_term(force_flag, energy_flag, particles); - if (this_node == 0) - k_space_energy_dip += surface_term; - } - - return k_space_energy_dip; -} - -/************************************************************/ - -double calc_surface_term(bool force_flag, bool energy_flag, - const ParticleRange &particles) { - auto const pref = dipole.prefactor * 4 * Utils::pi() / box_geo.volume() / - (2 * dp3m.params.epsilon + 1); - auto const n_local_part = particles.size(); - - // We put all the dipolar momenta in a the arrays mx,my,mz according to the - // id-number of the particles - std::vector mx(n_local_part); - std::vector my(n_local_part); - std::vector mz(n_local_part); - - int ip = 0; - for (auto const &p : particles) { - auto const dip = p.calc_dip(); - mx[ip] = dip[0]; - my[ip] = dip[1]; - mz[ip] = dip[2]; - ip++; - } - - // we will need the sum of all dipolar momenta vectors - auto local_dip = Utils::Vector3d{}; - for (int i = 0; i < n_local_part; i++) { - local_dip[0] += mx[i]; - local_dip[1] += my[i]; - local_dip[2] += mz[i]; - } - auto const box_dip = - boost::mpi::all_reduce(comm_cart, local_dip, std::plus<>()); - - double energy = 0.0; - if (energy_flag) { - double sum_e = 0.0; - for (int i = 0; i < n_local_part; i++) { - sum_e += mx[i] * box_dip[0] + my[i] * box_dip[1] + mz[i] * box_dip[2]; - } - energy = - 0.5 * pref * boost::mpi::all_reduce(comm_cart, sum_e, std::plus<>()); - } - - if (force_flag) { - - std::vector sumix(n_local_part); - std::vector sumiy(n_local_part); - std::vector sumiz(n_local_part); - - for (int i = 0; i < n_local_part; i++) { - sumix[i] = my[i] * box_dip[2] - mz[i] * box_dip[1]; - sumiy[i] = mz[i] * box_dip[0] - mx[i] * box_dip[2]; - sumiz[i] = mx[i] * box_dip[1] - my[i] * box_dip[0]; - } - - ip = 0; - for (auto &p : particles) { - p.f.torque[0] -= pref * sumix[ip]; - p.f.torque[1] -= pref * sumiy[ip]; - p.f.torque[2] -= pref * sumiz[ip]; - ip++; - } - } - - return energy; -} - -/*****************************************************************************/ - -void dp3m_calc_influence_function_force() { - auto const start = Utils::Vector3i{dp3m.fft.plan[3].start}; - auto const size = Utils::Vector3i{dp3m.fft.plan[3].new_mesh}; - - dp3m.g_force = grid_influence_function<3>(dp3m.params, start, start + size, - box_geo.length()); -} - -void dp3m_calc_influence_function_energy() { - auto const start = Utils::Vector3i{dp3m.fft.plan[3].start}; - auto const size = Utils::Vector3i{dp3m.fft.plan[3].new_mesh}; - - dp3m.g_energy = grid_influence_function<2>(dp3m.params, start, start + size, - box_geo.length()); -} - -/*****************************************************************************/ - -/** @copybrief p3m_get_accuracy - * - * The real space error is tuned such that it contributes half of the - * total error, and then the Fourier space error is calculated. - * If an optimal alpha is not found, the value 0.1 is used as fallback. - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[out] _alpha_L @copybrief P3MParameters::alpha_L - * @param[out] _rs_err real space error - * @param[out] _ks_err Fourier space error - * @returns Error magnitude - */ -double dp3m_get_accuracy(int mesh, int cao, double r_cut_iL, double *_alpha_L, - double *_rs_err, double *_ks_err) { - double rs_err, ks_err; - double alpha_L; - - /* calc maximal real space error for setting */ - - // Alpha cannot be zero in the dipolar case because real_space formula breaks - // down - rs_err = dp3m_real_space_error(box_geo.length()[0], r_cut_iL, - dp3m.sum_dip_part, dp3m.sum_mu2, 0.001); - - if (Utils::sqrt_2() * rs_err > dp3m.params.accuracy) { - /* assume rs_err = ks_err -> rs_err = accuracy/sqrt(2.0) -> alpha_L */ - alpha_L = dp3m_rtbisection(box_geo.length()[0], r_cut_iL, dp3m.sum_dip_part, - dp3m.sum_mu2, 0.0001 * box_geo.length()[0], - 5.0 * box_geo.length()[0], 0.0001, - dp3m.params.accuracy); - if (alpha_L == -DP3M_RTBISECTION_ERROR) { - *_rs_err = -1; - *_ks_err = -1; - return -DP3M_RTBISECTION_ERROR; - } - } - - else - /* even alpha=0 is ok, however, we cannot choose it since it kills the - k-space error formula. - Anyways, this very likely NOT the optimal solution */ - alpha_L = 0.1; - - *_alpha_L = alpha_L; - /* calculate real space and k-space error for this alpha_L */ - - rs_err = dp3m_real_space_error(box_geo.length()[0], r_cut_iL, - dp3m.sum_dip_part, dp3m.sum_mu2, alpha_L); - ks_err = dp3m_k_space_error(box_geo.length()[0], mesh, cao, dp3m.sum_dip_part, - dp3m.sum_mu2, alpha_L); - - *_rs_err = rs_err; - *_ks_err = ks_err; - return sqrt(Utils::sqr(rs_err) + Utils::sqr(ks_err)); -} - -/** @copybrief p3m_mcr_time - * - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[in] alpha_L @copybrief P3MParameters::alpha_L - * @param[in] timings Number of test force calculations - * - * @returns The integration time in case of success, otherwise - * -@ref P3M_TUNE_FAIL - */ -static double dp3m_mcr_time(int mesh, int cao, double r_cut_iL, double alpha_L, - int timings) { - - /* broadcast p3m parameters for test run */ - if (dipole.method != DIPOLAR_P3M && dipole.method != DIPOLAR_MDLC_P3M) - Dipole::set_method_local(DIPOLAR_P3M); - dp3m.params.r_cut_iL = r_cut_iL; - dp3m.params.mesh[0] = dp3m.params.mesh[1] = dp3m.params.mesh[2] = mesh; - dp3m.params.cao = cao; - dp3m.params.alpha_L = alpha_L; - /* initialize p3m structures */ - mpi_bcast_coulomb_params(); - /* perform force calculation test */ - double int_time = time_force_calc(timings); - if (int_time == -1) { - return -P3M_TUNE_FAIL; - } - return int_time; -} - -/** @copybrief p3m_mc_time - * - * The @p _r_cut_iL is determined via a simple bisection. - * - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] r_cut_iL_min lower bound for @p _r_cut_iL - * @param[in] r_cut_iL_max upper bound for @p _r_cut_iL - * @param[out] _r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[out] _alpha_L @copybrief P3MParameters::alpha_L - * @param[out] _accuracy @copybrief P3MParameters::accuracy - * @param[in] timings Number of test force calculations - * @param[in] verbose printf output - * - * @returns The integration time in case of success, otherwise - * -@ref P3M_TUNE_FAIL, -@ref P3M_TUNE_ACCURACY_TOO_LARGE, - * -@ref P3M_TUNE_CAO_TOO_LARGE, -@ref P3M_TUNE_ELCTEST, or - * -@ref P3M_TUNE_CUTOFF_TOO_LARGE - */ -static double dp3m_mc_time(int mesh, int cao, double r_cut_iL_min, - double r_cut_iL_max, double *_r_cut_iL, - double *_alpha_L, double *_accuracy, int timings, - bool verbose) { - double r_cut_iL; - double rs_err, ks_err; - - /* initial checks. */ - auto const mesh_size = box_geo.length()[0] / static_cast(mesh); - auto const k_cut = mesh_size * cao / 2.0; - - auto const min_box_l = *boost::min_element(box_geo.length()); - auto const min_local_box_l = *boost::min_element(local_geo.length()); - - if (cao >= mesh || k_cut >= std::min(min_box_l, min_local_box_l) - skin) { - /* print result */ - if (verbose) { - std::printf("%-4d %-3d cao too large for this mesh\n", mesh, cao); - } - return -P3M_TUNE_CAO_TOO_LARGE; - } - - /* Either low and high boundary are equal (for fixed cut), or the low border - is initially 0 and therefore - has infinite error estimate, as required. Therefore if the high boundary - fails, there is no possible r_cut */ - *_accuracy = - dp3m_get_accuracy(mesh, cao, r_cut_iL_max, _alpha_L, &rs_err, &ks_err); - if (*_accuracy == -DP3M_RTBISECTION_ERROR) - return *_accuracy; - if (*_accuracy > dp3m.params.accuracy) { - /* print result */ - if (verbose) { - std::printf("%-4d %-3d %.5e %.5e %.5e %.3e %.3e accuracy not achieved\n", - mesh, cao, r_cut_iL_max, *_alpha_L, *_accuracy, rs_err, - ks_err); - } - return -P3M_TUNE_ACCURACY_TOO_LARGE; - } - - for (;;) { - r_cut_iL = 0.5 * (r_cut_iL_min + r_cut_iL_max); - - if (r_cut_iL_max - r_cut_iL_min < P3M_RCUT_PREC) - break; - - /* bisection */ - auto const tmp_accuracy = - dp3m_get_accuracy(mesh, cao, r_cut_iL, _alpha_L, &rs_err, &ks_err); - if (tmp_accuracy == -DP3M_RTBISECTION_ERROR) - return tmp_accuracy; - if (tmp_accuracy > dp3m.params.accuracy) - r_cut_iL_min = r_cut_iL; - else - r_cut_iL_max = r_cut_iL; - } - /* final result is always the upper interval boundary, since only there - we know that the desired minimal accuracy is obtained */ - *_r_cut_iL = r_cut_iL = r_cut_iL_max; - - /* check whether we are running P3M+DLC, and whether we leave a reasonable gap - * space */ - if (dipole.method == DIPOLAR_MDLC_P3M) { - runtimeErrorMsg() << "dipolar P3M: tuning when dlc needs to be fixed"; - } - - double const int_time = - dp3m_mcr_time(mesh, cao, r_cut_iL, *_alpha_L, timings); - if (int_time == -P3M_TUNE_FAIL) { - if (verbose) { - std::printf("tuning failed, test integration not possible\n"); - } - return int_time; - } - - *_accuracy = - dp3m_get_accuracy(mesh, cao, r_cut_iL, _alpha_L, &rs_err, &ks_err); - if (*_accuracy == -DP3M_RTBISECTION_ERROR) { - return *_accuracy; - } - - /* print result */ - if (verbose) { - std::printf("%-4d %-3d %.5e %.5e %.5e %.3e %.3e %-8.0f\n", mesh, cao, - r_cut_iL, *_alpha_L, *_accuracy, rs_err, ks_err, int_time); - } - return int_time; -} - -/** @copybrief p3m_m_time - * - * @p _cao should contain an initial guess, which is then adapted by stepping - * up and down. - * - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao_min lower bound for @p _cao - * @param[in] cao_max upper bound for @p _cao - * @param[in,out] _cao initial guess for the - * @copybrief P3MParameters::cao - * @param[in] r_cut_iL_min lower bound for @p _r_cut_iL - * @param[in] r_cut_iL_max upper bound for @p _r_cut_iL - * @param[out] _r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[out] _alpha_L @copybrief P3MParameters::alpha_L - * @param[out] _accuracy @copybrief P3MParameters::accuracy - * @param[in] timings Number of test force calculations - * @param[in] verbose printf output - * - * @returns The integration time in case of success, otherwise - * -@ref P3M_TUNE_FAIL or -@ref P3M_TUNE_CAO_TOO_LARGE */ -static double dp3m_m_time(int mesh, int cao_min, int cao_max, int *_cao, - double r_cut_iL_min, double r_cut_iL_max, - double *_r_cut_iL, double *_alpha_L, - double *_accuracy, int timings, bool verbose) { - double best_time = -1, tmp_r_cut_iL = -1., tmp_alpha_L = 0.0, - tmp_accuracy = 0.0; - /* in which direction improvement is possible. Initially, we don't know it - * yet. - */ - int final_dir = 0; - int cao = *_cao; - - /* the initial step sets a timing mark. If there is no valid r_cut, we can - only try - to increase cao to increase the obtainable precision of the far formula. */ - double tmp_time; - do { - tmp_time = - dp3m_mc_time(mesh, cao, r_cut_iL_min, r_cut_iL_max, &tmp_r_cut_iL, - &tmp_alpha_L, &tmp_accuracy, timings, verbose); - /* bail out if the force evaluation is not working */ - if (tmp_time == -P3M_TUNE_FAIL || tmp_time == -DP3M_RTBISECTION_ERROR) - return tmp_time; - /* cao is too large for this grid, but still the accuracy cannot be - * achieved, give up */ - if (tmp_time == -P3M_TUNE_CAO_TOO_LARGE) { - return tmp_time; - } - /* we have a valid time, start optimising from there */ - if (tmp_time >= 0) { - best_time = tmp_time; - *_r_cut_iL = tmp_r_cut_iL; - *_alpha_L = tmp_alpha_L; - *_accuracy = tmp_accuracy; - *_cao = cao; - break; - } - /* the required accuracy could not be obtained, try higher caos */ - cao++; - final_dir = 1; - } while (cao <= cao_max); - /* with this mesh, the required accuracy cannot be obtained. */ - if (cao > cao_max) - return -P3M_TUNE_CAO_TOO_LARGE; - - /* at the boundaries, only the opposite direction can be used for optimisation - */ - if (cao == cao_min) - final_dir = 1; - else if (cao == cao_max) - final_dir = -1; - - if (final_dir == 0) { - /* check in which direction we can optimise. Both directions are possible */ - double dir_times[3]; - for (final_dir = -1; final_dir <= 1; final_dir += 2) { - dir_times[final_dir + 1] = tmp_time = dp3m_mc_time( - mesh, cao + final_dir, r_cut_iL_min, r_cut_iL_max, &tmp_r_cut_iL, - &tmp_alpha_L, &tmp_accuracy, timings, verbose); - /* bail out on errors, as usual */ - if (tmp_time == -P3M_TUNE_FAIL || tmp_time == -DP3M_RTBISECTION_ERROR) - return tmp_time; - /* in this direction, we cannot optimise, since we get into precision - * trouble */ - if (tmp_time < 0) - continue; - - if (tmp_time < best_time) { - best_time = tmp_time; - *_r_cut_iL = tmp_r_cut_iL; - *_alpha_L = tmp_alpha_L; - *_accuracy = tmp_accuracy; - *_cao = cao + final_dir; - } - } - /* choose the direction which was optimal, if any of the two */ - if (dir_times[0] == best_time) { - final_dir = -1; - } else if (dir_times[2] == best_time) { - final_dir = 1; - } else { - /* no improvement in either direction, however if one is only marginally - * worse, we can still try*/ - /* down is possible and not much worse, while up is either illegal or even - * worse */ - if ((dir_times[0] >= 0 && dir_times[0] < best_time + P3M_TIME_GRAN) && - (dir_times[2] < 0 || dir_times[2] > dir_times[0])) - final_dir = -1; - /* same for up */ - else if ((dir_times[2] >= 0 && - dir_times[2] < best_time + P3M_TIME_GRAN) && - (dir_times[0] < 0 || dir_times[0] > dir_times[2])) - final_dir = 1; - else { - /* really no chance for optimisation */ - return best_time; - } - } - /* we already checked the initial cao and its neighbor */ - cao += 2 * final_dir; - } else { - /* here some constraint is active, and we only checked the initial cao - * itself */ - cao += final_dir; - } - - /* move cao into the optimisation direction until we do not gain anymore. */ - for (; cao >= cao_min && cao <= cao_max; cao += final_dir) { - tmp_time = - dp3m_mc_time(mesh, cao, r_cut_iL_min, r_cut_iL_max, &tmp_r_cut_iL, - &tmp_alpha_L, &tmp_accuracy, timings, verbose); - /* bail out on errors, as usual */ - if (tmp_time == -P3M_TUNE_FAIL || tmp_time == -DP3M_RTBISECTION_ERROR) - return tmp_time; - /* if we cannot meet the precision anymore, give up */ - if (tmp_time < 0) - break; - - if (tmp_time < best_time) { - best_time = tmp_time; - *_r_cut_iL = tmp_r_cut_iL; - *_alpha_L = tmp_alpha_L; - *_accuracy = tmp_accuracy; - *_cao = cao; - } - /* no hope of further optimisation */ - else if (tmp_time > best_time + P3M_TIME_GRAN) - break; - } - return best_time; -} - -int dp3m_adaptive_tune(int timings, bool verbose) { - /** Tuning of dipolar P3M. The algorithm basically determines the mesh, cao - * and then the real space cutoff, in this nested order. - * - * For each mesh, the cao optimal for the mesh tested previously is used as - * an initial guess, and the algorithm tries whether increasing or decreasing - * it leads to a better solution. This is efficient, since the optimal cao - * only changes little with the meshes in general. - * - * The real space cutoff for a given mesh and cao is determined via a - * bisection on the error estimate, which determines where the error - * estimate equals the required accuracy. Therefore the smallest possible, - * i.e. fastest real space cutoff is determined. - * - * Both the search over mesh and cao stop to search in a specific direction - * once the computation time is significantly higher than the currently - * known optimum. - */ - int mesh_max, mesh = -1, tmp_mesh; - double r_cut_iL_min, r_cut_iL_max, r_cut_iL = -1, tmp_r_cut_iL = 0.0; - int cao_min, cao_max, cao = -1, tmp_cao; - - double alpha_L = -1, tmp_alpha_L = 0.0; - double accuracy = -1, tmp_accuracy = 0.0; - double time_best = 1e20, tmp_time; - - /* preparation */ - mpi_call_all(dp3m_count_magnetic_particles); - - if (dp3m.sum_dip_part == 0) { - runtimeErrorMsg() << "no dipolar particles in the system"; - return ES_ERROR; - } - - /* Print Status */ - if (verbose) { - std::printf("Dipolar P3M tune parameters: Accuracy goal = %.5e prefactor " - "= %.5e\n" - "System: box_l = %.5e # charged part = %d Sum[q_i^2] = %.5e\n", - dp3m.params.accuracy, dipole.prefactor, box_geo.length()[0], - dp3m.sum_dip_part, dp3m.sum_mu2); - } - - /* parameter ranges */ - if (dp3m.params.mesh[0] == 0) { - double expo; - expo = log(pow((double)dp3m.sum_dip_part, (1.0 / 3.0))) / log(2.0); - - tmp_mesh = (int)(pow(2.0, (double)((int)expo)) + 0.1); - /* this limits the tried meshes if the accuracy cannot - be obtained with smaller meshes, but normally not all these - meshes have to be tested */ - mesh_max = tmp_mesh * 256; - /* avoid using more than 1 GB of FFT arrays (per default, see config.hpp) */ - if (mesh_max > P3M_MAX_MESH) - mesh_max = P3M_MAX_MESH; - } else { - tmp_mesh = mesh_max = dp3m.params.mesh[0]; - - if (verbose) { - std::printf("fixed mesh %d\n", dp3m.params.mesh[0]); - } - } - - if (dp3m.params.r_cut_iL == 0.0) { - auto const min_box_l = *boost::min_element(box_geo.length()); - auto const min_local_box_l = *boost::min_element(local_geo.length()); - - r_cut_iL_min = 0; - r_cut_iL_max = std::min(min_local_box_l, min_box_l / 2) - skin; - r_cut_iL_min *= box_geo.length_inv()[0]; - r_cut_iL_max *= box_geo.length_inv()[0]; - } else { - r_cut_iL_min = r_cut_iL_max = dp3m.params.r_cut_iL; - - if (verbose) { - std::printf("fixed r_cut_iL %f\n", dp3m.params.r_cut_iL); - } - } - - if (dp3m.params.cao == 0) { - cao_min = 1; - cao_max = 7; - cao = 3; - } else { - cao_min = cao_max = cao = dp3m.params.cao; - - if (verbose) { - std::printf("fixed cao %d\n", dp3m.params.cao); - } - } - - if (verbose) { - std::printf("Dmesh cao Dr_cut_iL Dalpha_L Derr " - " Drs_err Dks_err time [ms]\n"); - } - - /* mesh loop */ - for (; tmp_mesh <= mesh_max; tmp_mesh += 2) { - tmp_cao = cao; - tmp_time = dp3m_m_time(tmp_mesh, cao_min, cao_max, &tmp_cao, r_cut_iL_min, - r_cut_iL_max, &tmp_r_cut_iL, &tmp_alpha_L, - &tmp_accuracy, timings, verbose); - /* some error occurred during the tuning force evaluation */ - if (tmp_time == -P3M_TUNE_FAIL || tmp_time == -DP3M_RTBISECTION_ERROR) - return ES_ERROR; - /* this mesh does not work at all */ - if (tmp_time < 0) - continue; - - /* the optimum r_cut for this mesh is the upper limit for higher meshes, - everything else is slower */ - r_cut_iL_max = tmp_r_cut_iL; - - /* new optimum */ - if (tmp_time < time_best) { - time_best = tmp_time; - mesh = tmp_mesh; - cao = tmp_cao; - r_cut_iL = tmp_r_cut_iL; - alpha_L = tmp_alpha_L; - accuracy = tmp_accuracy; - } - /* no hope of further optimisation */ - else if (tmp_time > time_best + P3M_TIME_GRAN) - break; - } - - if (time_best == 1e20) { - runtimeErrorMsg() << "failed to reach requested accuracy"; - return ES_ERROR; - } - - /* set tuned p3m parameters */ - dp3m.params.r_cut_iL = r_cut_iL; - dp3m.params.mesh[0] = dp3m.params.mesh[1] = dp3m.params.mesh[2] = mesh; - dp3m.params.cao = cao; - dp3m.params.alpha_L = alpha_L; - dp3m.params.accuracy = accuracy; - /* broadcast tuned p3m parameters */ - mpi_bcast_coulomb_params(); - /* Tell the user about the outcome */ - if (verbose) { - std::printf( - "\nresulting parameters: mesh: %d, cao: %d, r_cut_iL: %.4e," - "\n alpha_L: %.4e, accuracy: %.4e, time: %.0f\n", - mesh, cao, r_cut_iL, alpha_L, accuracy, time_best); - } - return ES_OK; -} - -void dp3m_count_magnetic_particles() { - using boost::mpi::all_reduce; - int local_n = 0; - double local_mu2 = 0.0; - - for (auto const &p : cell_structure.local_particles()) { - if (p.p.dipm != 0.0) { - local_mu2 += p.calc_dip().norm2(); - local_n++; - } - } - - dp3m.sum_mu2 = all_reduce(comm_cart, local_mu2, std::plus<>()); - dp3m.sum_dip_part = all_reduce(comm_cart, local_n, std::plus<>()); -} - -REGISTER_CALLBACK(dp3m_count_magnetic_particles) - -/** Calculate the k-space error of dipolar-P3M */ -static double dp3m_k_space_error(double box_size, int mesh, int cao, - int n_c_part, double sum_q2, double alpha_L) { - double he_q = 0.0; - auto const mesh_i = 1. / mesh; - auto const alpha_L_i = 1. / alpha_L; - - for (int nx = -mesh / 2; nx < mesh / 2; nx++) - for (int ny = -mesh / 2; ny < mesh / 2; ny++) - for (int nz = -mesh / 2; nz < mesh / 2; nz++) - if ((nx != 0) || (ny != 0) || (nz != 0)) { - auto const n2 = Utils::sqr(nx) + Utils::sqr(ny) + Utils::sqr(nz); - auto const cs = p3m_analytic_cotangent_sum(nx, mesh_i, cao) * - p3m_analytic_cotangent_sum(ny, mesh_i, cao) * - p3m_analytic_cotangent_sum(nz, mesh_i, cao); - double alias1, alias2; - dp3m_tune_aliasing_sums(nx, ny, nz, mesh, mesh_i, cao, alpha_L_i, - &alias1, &alias2); - double d = alias1 - Utils::sqr(alias2 / cs) / - Utils::int_pow<3>(static_cast(n2)); - /* at high precision, d can become negative due to extinction; - also, don't take values that have no significant digits left*/ - if (d > 0 && (fabs(d / alias1) > ROUND_ERROR_PREC)) - he_q += d; - } - - return 8. * Utils::sqr(Utils::pi()) / 3. * sum_q2 * sqrt(he_q / n_c_part) / - Utils::int_pow<4>(box_size); -} - -/** Tuning dipolar-P3M */ -void dp3m_tune_aliasing_sums(int nx, int ny, int nz, int mesh, double mesh_i, - int cao, double alpha_L_i, double *alias1, - double *alias2) { - using Utils::sinc; - - auto const factor1 = Utils::sqr(Utils::pi() * alpha_L_i); - - *alias1 = *alias2 = 0.0; - for (int mx = -P3M_BRILLOUIN; mx <= P3M_BRILLOUIN; mx++) { - auto const nmx = nx + mx * mesh; - auto const fnmx = mesh_i * nmx; - for (int my = -P3M_BRILLOUIN; my <= P3M_BRILLOUIN; my++) { - auto const nmy = ny + my * mesh; - auto const fnmy = mesh_i * nmy; - for (int mz = -P3M_BRILLOUIN; mz <= P3M_BRILLOUIN; mz++) { - auto const nmz = nz + mz * mesh; - auto const fnmz = mesh_i * nmz; - - auto const nm2 = Utils::sqr(nmx) + Utils::sqr(nmy) + Utils::sqr(nmz); - auto const ex = std::exp(-factor1 * nm2); - - auto const U2 = pow(sinc(fnmx) * sinc(fnmy) * sinc(fnmz), 2.0 * cao); - - *alias1 += Utils::sqr(ex) * nm2; - *alias2 += U2 * ex * pow((nx * nmx + ny * nmy + nz * nmz), 3.) / nm2; - } - } - } -} - -/** Calculate the value of the errors for the REAL part of the force in terms - * of the splitting parameter alpha of Ewald. Based on eq. (33) @cite wang01a. - * - * Please note that in this more refined approach we don't use - * eq. (37), but eq. (33) which maintains all the powers in alpha. - */ -double dp3m_real_space_error(double box_size, double r_cut_iL, int n_c_part, - double sum_q2, double alpha_L) { - double d_error_f, d_cc, d_dc, d_rcut2, d_con; - double d_a2, d_c, d_RCUT; - - d_RCUT = r_cut_iL * box_size; - d_rcut2 = d_RCUT * d_RCUT; - - d_a2 = Utils::sqr(alpha_L) / Utils::sqr(box_size); - - d_c = sum_q2 * exp(-d_a2 * Utils::sqr(d_RCUT)); - - d_cc = - 4.0 * Utils::sqr(d_a2) * Utils::sqr(d_rcut2) + 6.0 * d_a2 * d_rcut2 + 3.0; - - d_dc = 8.0 * Utils::int_pow<3>(d_a2) * Utils::int_pow<3>(d_rcut2) + - 20.0 * Utils::sqr(d_a2) * Utils::sqr(d_rcut2) + 30.0 * d_a2 * d_rcut2 + - 15.0; - - d_con = 1.0 / sqrt(Utils::int_pow<3>(box_size) * Utils::sqr(d_a2) * - Utils::int_pow<4>(d_rcut2) * d_RCUT * (double)n_c_part); - - d_error_f = d_c * d_con * - sqrt((13. / 6.) * Utils::sqr(d_cc) + - (2. / 15.) * Utils::sqr(d_dc) - (13. / 15.) * d_cc * d_dc); - - return d_error_f; -} - -/** Using bisection, find the root of a function "func-tuned_accuracy/sqrt(2.)" - * known to lie between x1 and x2. The root, returned as rtbis, will be - * refined until its accuracy is \f$\pm\f$ @p xacc. - */ -double dp3m_rtbisection(double box_size, double r_cut_iL, int n_c_part, - double sum_q2, double x1, double x2, double xacc, - double tuned_accuracy) { - constexpr int JJ_RTBIS_MAX = 40; - - auto const constant = tuned_accuracy / Utils::sqrt_2(); - - auto const f1 = - dp3m_real_space_error(box_size, r_cut_iL, n_c_part, sum_q2, x1) - - constant; - auto const f2 = - dp3m_real_space_error(box_size, r_cut_iL, n_c_part, sum_q2, x2) - - constant; - if (f1 * f2 >= 0.0) { - runtimeErrorMsg() - << "Root must be bracketed for bisection in dp3m_rtbisection"; - return -DP3M_RTBISECTION_ERROR; - } - // Orient the search dx, and set rtb to x1 or x2 ... - double dx; - double rtb = f1 < 0.0 ? (dx = x2 - x1, x1) : (dx = x1 - x2, x2); - for (int j = 1; j <= JJ_RTBIS_MAX; j++) { - auto const xmid = rtb + (dx *= 0.5); - auto const fmid = - dp3m_real_space_error(box_size, r_cut_iL, n_c_part, sum_q2, xmid) - - constant; - if (fmid <= 0.0) - rtb = xmid; - if (fabs(dx) < xacc || fmid == 0.0) - return rtb; - } - runtimeErrorMsg() << "Too many bisections in dp3m_rtbisection"; - return -DP3M_RTBISECTION_ERROR; -} - -/************************************************************/ - -void dp3m_init_a_ai_cao_cut() { - for (int i = 0; i < 3; i++) { - dp3m.params.ai[i] = dp3m.params.mesh[i] * box_geo.length_inv()[i]; - dp3m.params.a[i] = 1.0 / dp3m.params.ai[i]; - dp3m.params.cao_cut[i] = 0.5 * dp3m.params.a[i] * dp3m.params.cao; - } -} - -/*****************************************************************************/ - -bool dp3m_sanity_checks_boxl() { - bool ret = false; - for (int i = 0; i < 3; i++) { - /* check k-space cutoff */ - if (dp3m.params.cao_cut[i] >= box_geo.length_half()[i]) { - runtimeErrorMsg() << "dipolar P3M_init: k-space cutoff " - << dp3m.params.cao_cut[i] - << " is larger than half of box dimension " - << box_geo.length()[i]; - ret = true; - } - if (dp3m.params.cao_cut[i] >= local_geo.length()[i]) { - runtimeErrorMsg() << "dipolar P3M_init: k-space cutoff " - << dp3m.params.cao_cut[i] - << " is larger than local box dimension " - << local_geo.length()[i]; - ret = true; - } - } - return ret; -} - -/*****************************************************************************/ - -bool dp3m_sanity_checks(const Utils::Vector3i &grid) { - bool ret = false; - - if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { - runtimeErrorMsg() << "dipolar P3M requires periodicity (1, 1, 1)"; - ret = true; - } - - if (local_geo.cell_structure_type() != - CellStructureType::CELL_STRUCTURE_REGULAR) { - runtimeErrorMsg() << "dipolar P3M requires the regular decomposition " - "cell system"; - ret = true; - } - - if ((box_geo.length()[0] != box_geo.length()[1]) || - (box_geo.length()[1] != box_geo.length()[2])) { - runtimeErrorMsg() << "dipolar P3M requires a cubic box"; - ret = true; - } - - if ((dp3m.params.mesh[0] != dp3m.params.mesh[1]) || - (dp3m.params.mesh[1] != dp3m.params.mesh[2])) { - runtimeErrorMsg() << "dipolar P3M requires a cubic mesh"; - ret = true; - } - - ret |= dp3m_sanity_checks_boxl(); - - if (dp3m.params.mesh[0] == 0) { - runtimeErrorMsg() << "dipolar P3M_init: mesh size is not yet set"; - ret = true; - } - if (dp3m.params.cao == 0) { - runtimeErrorMsg() << "dipolar P3M_init: cao is not yet set"; - ret = true; - } - if (grid[0] < grid[1] || grid[1] < grid[2]) { - runtimeErrorMsg() - << "dipolar P3M_init: node grid must be sorted, largest first"; - ret = true; - } - - return ret; -} - -/************************************************/ - -void dp3m_scaleby_box_l() { - if (dipole.prefactor < 0.0) { - runtimeErrorMsg() << "Dipolar prefactor has to be >=0"; - return; - } - - dp3m.params.r_cut = dp3m.params.r_cut_iL * box_geo.length()[0]; - dp3m.params.alpha = dp3m.params.alpha_L * box_geo.length_inv()[0]; - dp3m_init_a_ai_cao_cut(); - p3m_calc_lm_ld_pos(dp3m.local_mesh, dp3m.params); - dp3m_sanity_checks_boxl(); - - dp3m_calc_influence_function_force(); - dp3m_calc_influence_function_energy(); -} - -/** Calculate the dipolar-P3M energy correction */ -void dp3m_compute_constants_energy_dipolar() { - - if (dp3m.energy_correction != 0.0) - return; - - auto const Ukp3m = dp3m_average_dipolar_self_energy() * box_geo.volume(); - - auto const Eself = -2 * pow(dp3m.params.alpha_L, 3) * Utils::sqrt_pi_i() / 3; - - dp3m.energy_correction = - -dp3m.sum_mu2 * (Ukp3m + Eself + 2. * Utils::pi() / 3.); -} - -#ifdef NPT -void npt_add_virial_magnetic_contribution(double energy) { - npt_add_virial_contribution(energy); -} -#endif - -#endif /* DP3M */ diff --git a/src/core/electrostatics_magnetostatics/p3m-dipolar.hpp b/src/core/electrostatics_magnetostatics/p3m-dipolar.hpp deleted file mode 100644 index d74b3ddd2a0..00000000000 --- a/src/core/electrostatics_magnetostatics/p3m-dipolar.hpp +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_DIPOLAR_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_DIPOLAR_HPP -/** \file - * P3M algorithm for long range magnetic dipole-dipole interaction. - * - * We use here a P3M (Particle-Particle Particle-Mesh) method based - * on the dipolar Ewald summation. Details of the used method can be found in - * @cite hockney88a and @cite deserno98a @cite deserno98b. The file p3m - * contains only the Particle-Mesh part. - * - * Further reading: @cite cerda08d - * - * Implementation in p3m-dipolar.cpp. - */ - -#include "config.hpp" - -#ifdef DP3M - -#include "electrostatics_magnetostatics/dipole.hpp" -#include "electrostatics_magnetostatics/fft.hpp" -#include "electrostatics_magnetostatics/p3m-common.hpp" -#include "electrostatics_magnetostatics/p3m-data_struct.hpp" -#include "electrostatics_magnetostatics/p3m_interpolation.hpp" -#include "electrostatics_magnetostatics/p3m_send_mesh.hpp" - -#include "Particle.hpp" -#include "ParticleRange.hpp" - -#include -#include -#include - -#include -#include -#include - -struct dp3m_data_struct : public p3m_data_struct_base { - dp3m_data_struct(); - - /** local mesh. */ - P3MLocalMesh local_mesh; - /** real space mesh (local) for CA/FFT. */ - fft_vector rs_mesh; - /** real space mesh (local) for CA/FFT of the dipolar field. */ - std::array, 3> rs_mesh_dip; - /** k-space mesh (local) for k-space calculation and FFT. */ - std::vector ks_mesh; - - /** number of dipolar particles (only on head node). */ - int sum_dip_part; - /** Sum of square of magnetic dipoles (only on head node). */ - double sum_mu2; - - /** position shift for calculation of first assignment mesh point. */ - double pos_shift; - - p3m_interpolation_cache inter_weights; - - /** send/recv mesh sizes */ - p3m_send_mesh sm; - - /** value of the energy correction due to MS effects */ - double energy_correction; - - fft_data_struct fft; -}; - -/** dipolar P3M parameters. */ -extern dp3m_data_struct dp3m; - -/** @copydoc p3m_set_tune_params */ -void dp3m_set_tune_params(double r_cut, int mesh, int cao, double accuracy); - -/** @copydoc p3m_set_params */ -void dp3m_set_params(double r_cut, int mesh, int cao, double alpha, - double accuracy); - -/** @copydoc p3m_set_mesh_offset */ -void dp3m_set_mesh_offset(double x, double y, double z); - -/** @copydoc p3m_set_eps */ -void dp3m_set_eps(double eps); - -/** Initialize all structures, parameters and arrays needed for the - * P3M algorithm for dipole-dipole interactions. - */ -void dp3m_init(); - -/** @copydoc p3m_scaleby_box_l */ -void dp3m_scaleby_box_l(); - -/** Sanity checks */ -bool dp3m_sanity_checks(const Utils::Vector3i &grid); - -/** Assign the physical dipoles using the tabulated assignment function. - */ -void dp3m_dipole_assign(const ParticleRange &particles); - -/** Reset @ref dp3m core parameters */ -void dp3m_deactivate(); - -/** Tune dipolar P3M parameters to desired accuracy. - * - * The parameters - * @ref P3MParameters::mesh "mesh", - * @ref P3MParameters::cao "cao", - * @ref P3MParameters::r_cut_iL "r_cut_iL" and - * @ref P3MParameters::alpha_L "alpha_L" - * are tuned to obtain the target accuracy (initially stored in - * @ref P3MParameters::accuracy "accuracy") in optimal time. - * These parameters are stored in the @ref dp3m object. - * - * The function utilizes the analytic expression of the error estimate - * for the dipolar P3M method in the paper of @cite cerda08d in - * order to obtain the rms error in the force for a system of N randomly - * distributed particles in a cubic box. For the real space error, the - * estimate in @cite kolafa92a is used. - * - * Parameter ranges if not given explicit values via dp3m_set_tune_params(): - * - @p mesh is set up such that the number of mesh points is equal to the - * number of magnetic dipolar particles - * - @p cao explores all possible values - * - @p alpha_L is tuned for each tuple (@p r_cut_iL, @p mesh, @p cao) and - * calculated assuming that the error contributions of real and reciprocal - * space should be equal - * - * After checking if the total error lies below the target accuracy, the - * time needed for one force calculation (including Verlet list update) - * is measured via time_force_calc(). - * - * The function generates a log of the performed tuning. - * - * The function is based on routines of the program HE_Q.cpp for charges - * written by M. Deserno. - * - * @param verbose printf output - * @param timings Number of test force calculations - * @retval ES_OK - * @retval ES_ERROR - */ -int dp3m_adaptive_tune(int timings, bool verbose); - -/** Compute the k-space part of forces and energies for the magnetic - * dipole-dipole interaction - */ -double dp3m_calc_kspace_forces(bool force_flag, bool energy_flag, - ParticleRange const &particles); - -/** Calculate number of magnetic particles, the sum of the squared - * charges and the squared sum of the charges. - */ -void dp3m_count_magnetic_particles(); - -#ifdef NPT -/** Update the NpT virial */ -void npt_add_virial_magnetic_contribution(double energy); -#endif - -/** Calculate real space contribution of p3m dipolar pair forces and torques. - * If NPT is compiled in, update the NpT virial. - */ -inline ParticleForce dp3m_pair_force(Particle const &p1, Particle const &p2, - Utils::Vector3d const &d, double dist2, - double dist) { - if ((p1.p.dipm == 0.) || (p2.p.dipm == 0.) || dist >= dp3m.params.r_cut || - dist <= 0.) - return {}; - - auto const dip1 = p1.calc_dip(); - auto const dip2 = p2.calc_dip(); - auto const alpsq = dp3m.params.alpha * dp3m.params.alpha; - auto const adist = dp3m.params.alpha * dist; -#if USE_ERFC_APPROXIMATION - auto const erfc_part_ri = Utils::AS_erfc_part(adist) / dist; -#else - auto const erfc_part_ri = erfc(adist) / dist; -#endif - - // Calculate scalar multiplications for vectors mi, mj, rij - auto const mimj = dip1 * dip2; - - auto const mir = dip1 * d; - auto const mjr = dip2 * d; - - auto const coeff = 2.0 * dp3m.params.alpha * Utils::sqrt_pi_i(); - auto const dist2i = 1 / dist2; - auto const exp_adist2 = exp(-adist * adist); - - double B_r, C_r, D_r; - if (dp3m.params.accuracy > 5e-06) - B_r = (erfc_part_ri + coeff) * exp_adist2 * dist2i; - else - B_r = (erfc(adist) / dist + coeff * exp_adist2) * dist2i; - - C_r = (3 * B_r + 2 * alpsq * coeff * exp_adist2) * dist2i; - D_r = (5 * C_r + 4 * coeff * alpsq * alpsq * exp_adist2) * dist2i; - - // Calculate real-space forces - auto const force = - dipole.prefactor * - ((mimj * d + dip1 * mjr + dip2 * mir) * C_r - mir * mjr * D_r * d); - - // Calculate vector multiplications for vectors mi, mj, rij - auto const mixmj = vector_product(dip1, dip2); - auto const mixr = vector_product(dip1, d); - - // Calculate real-space torques - auto const torque = dipole.prefactor * (-mixmj * B_r + mixr * (mjr * C_r)); -#ifdef NPT -#if USE_ERFC_APPROXIMATION - auto const fac = dipole.prefactor * p1.p.dipm * p2.p.dipm * exp_adist2; -#else - auto const fac = dipole.prefactor * p1.p.dipm * p2.p.dipm; -#endif - auto const energy = fac * (mimj * B_r - mir * mjr * C_r); - npt_add_virial_magnetic_contribution(energy); -#endif // NPT - return ParticleForce{force, torque}; -} - -/** Calculate real space contribution of dipolar pair energy. */ -inline double dp3m_pair_energy(Particle const &p1, Particle const &p2, - Utils::Vector3d const &d, double dist2, - double dist) { - if ((p1.p.dipm == 0.) || (p2.p.dipm == 0.) || dist >= dp3m.params.r_cut || - dist <= 0.) - return {}; - - auto const dip1 = p1.calc_dip(); - auto const dip2 = p2.calc_dip(); - - auto const alpsq = dp3m.params.alpha * dp3m.params.alpha; - auto const adist = dp3m.params.alpha * dist; - /*fac1 = dipole.prefactor;*/ - -#if USE_ERFC_APPROXIMATION - auto const erfc_part_ri = Utils::AS_erfc_part(adist) / dist; - /* fac1 = dipole.prefactor * p1.p.dipm*p2.p.dipm; IT WAS WRONG */ - /* *exp(-adist*adist); */ -#else - auto const erfc_part_ri = erfc(adist) / dist; - /* fac1 = dipole.prefactor * p1.p.dipm*p2.p.dipm; IT WAS WRONG*/ -#endif - - // Calculate scalar multiplications for vectors mi, mj, rij - auto const mimj = dip1 * dip2; - auto const mir = dip1 * d; - auto const mjr = dip2 * d; - - auto const coeff = 2.0 * dp3m.params.alpha * Utils::sqrt_pi_i(); - auto const dist2i = 1 / dist2; - auto const exp_adist2 = exp(-adist * adist); - - double B_r; - if (dp3m.params.accuracy > 5e-06) - B_r = (erfc_part_ri + coeff) * exp_adist2 * dist2i; - else - B_r = (erfc(adist) / dist + coeff * exp_adist2) * dist2i; - - auto const C_r = (3 * B_r + 2 * alpsq * coeff * exp_adist2) * dist2i; - - return dipole.prefactor * (mimj * B_r - mir * mjr * C_r); -} - -#endif /* DP3M */ -#endif /* CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_DIPOLAR_HPP */ diff --git a/src/core/electrostatics_magnetostatics/p3m.cpp b/src/core/electrostatics_magnetostatics/p3m.cpp deleted file mode 100644 index f07fc3898d2..00000000000 --- a/src/core/electrostatics_magnetostatics/p3m.cpp +++ /dev/null @@ -1,1360 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** @file - * - * The corresponding header file is @ref p3m.hpp. - */ -#include "electrostatics_magnetostatics/p3m.hpp" - -#ifdef P3M - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/elc.hpp" -#include "electrostatics_magnetostatics/p3m_influence_function.hpp" - -#include "Particle.hpp" -#include "ParticleRange.hpp" -#include "cell_system/CellStructureType.hpp" -#include "cells.hpp" -#include "communication.hpp" -#include "errorhandling.hpp" -#include "fft.hpp" -#include "grid.hpp" -#include "integrate.hpp" -#include "tuning.hpp" - -#ifdef CUDA -#include "p3m_gpu_error.hpp" -#endif - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -p3m_data_struct p3m; - -/** \name Private Functions */ -/**@{*/ - -/** Initialize the (inverse) mesh constant @ref P3MParameters::a "a" - * (@ref P3MParameters::ai "ai") and the cutoff for charge assignment - * @ref P3MParameters::cao_cut "cao_cut". - * - * Function called by @ref p3m_init() once and by @ref p3m_scaleby_box_l() - * whenever the box length changes. - */ -static void p3m_init_a_ai_cao_cut(); - -static bool p3m_sanity_checks_system(const Utils::Vector3i &grid); - -/** Checks for correctness for charges in P3M of the cao_cut, - * necessary when the box length changes - */ -static bool p3m_sanity_checks_boxl(); - -/** Checks that necessary parameters are set - */ -static bool p3m_sanity_checks_parameters(); - -/** Calculate the optimal influence function of @cite hockney88a. - * (optimised for force calculations) - * - * Each node calculates only the values for its domain in k-space - * (see fft.plan[3].mesh and fft.plan[3].start). - * - * See also: @cite hockney88a eq. 8-22 (p. 275). Note the somewhat - * different convention for the prefactors, which is described in - * @cite deserno98a @cite deserno98b. - */ -static void p3m_calc_influence_function_force(); - -/** Calculate the influence function optimized for the energy and the - * self energy correction. - */ -static void p3m_calc_influence_function_energy(); - -/**@}*/ - -/** @name P3M tuning helper functions */ -/**@{*/ - -/** Calculate the real space contribution to the rms error in the force (as - * described by Kolafa and Perram). - * \param prefac Prefactor of Coulomb interaction. - * \param r_cut_iL rescaled real space cutoff for p3m method. - * \param n_c_part number of charged particles in the system. - * \param sum_q2 sum of square of charges in the system - * \param alpha_L rescaled Ewald splitting parameter. - * \return real space error - */ -static double p3m_real_space_error(double prefac, double r_cut_iL, int n_c_part, - double sum_q2, double alpha_L); - -/** Calculate the analytic expression of the error estimate for the - * P3M method in @cite hockney88a (eq. 8-23 p. 275) in - * order to obtain the rms error in the force for a system of N - * randomly distributed particles in a cubic box (k-space part). - * \param prefac Prefactor of Coulomb interaction. - * \param mesh number of mesh points in one direction. - * \param cao charge assignment order. - * \param n_c_part number of charged particles in the system. - * \param sum_q2 sum of square of charges in the system - * \param alpha_L rescaled Ewald splitting parameter. - * \return reciprocal (k) space error - */ -static double p3m_k_space_error(double prefac, const int mesh[3], int cao, - int n_c_part, double sum_q2, double alpha_L); - -/** Aliasing sum used by \ref p3m_k_space_error. */ -static void p3m_tune_aliasing_sums(int nx, int ny, int nz, const int mesh[3], - const double mesh_i[3], int cao, - double alpha_L_i, double *alias1, - double *alias2); - -/**@}*/ - -p3m_data_struct::p3m_data_struct() { - /* local_mesh is uninitialized */ - /* sm is uninitialized */ - - sum_qpart = 0; - sum_q2 = 0.0; - square_sum_q = 0.0; - - ks_pnum = 0; -} - -void p3m_init() { - if (coulomb.prefactor <= 0.0) { - // prefactor is zero: electrostatics switched off - p3m.params.r_cut = 0.0; - p3m.params.r_cut_iL = 0.0; - return; - } - - if (p3m_sanity_checks_parameters()) { - return; - } - - p3m.params.cao3 = Utils::int_pow<3>(p3m.params.cao); - - /* initializes the (inverse) mesh constant p3m.params.a (p3m.params.ai) and - * the cutoff for charge assignment p3m.params.cao_cut */ - p3m_init_a_ai_cao_cut(); - - if (p3m_sanity_checks()) { - return; - } - - double elc_layer = 0.0; - if (coulomb.method == COULOMB_ELC_P3M) { - elc_layer = elc_params.space_layer; - } - - p3m_calc_local_ca_mesh(p3m.local_mesh, p3m.params, local_geo, skin, - elc_layer); - - p3m.sm.resize(comm_cart, p3m.local_mesh); - - int ca_mesh_size = - fft_init(p3m.local_mesh.dim, p3m.local_mesh.margin, p3m.params.mesh, - p3m.params.mesh_off, p3m.ks_pnum, p3m.fft, node_grid, comm_cart); - p3m.rs_mesh.resize(ca_mesh_size); - - for (auto &e : p3m.E_mesh) { - e.resize(ca_mesh_size); - } - - p3m.calc_differential_operator(); - - /* fix box length dependent constants */ - p3m_scaleby_box_l(); - - p3m_count_charged_particles(); -} - -void p3m_set_tune_params(double r_cut, const int mesh[3], int cao, - double accuracy) { - if (r_cut >= 0) { - p3m.params.r_cut = r_cut; - p3m.params.r_cut_iL = r_cut * box_geo.length_inv()[0]; - } else if (r_cut == -1.0) { - p3m.params.r_cut = 0; - p3m.params.r_cut_iL = 0; - } - - if (mesh[0] >= 0) { - p3m.params.mesh[0] = mesh[0]; - p3m.params.mesh[1] = mesh[1]; - p3m.params.mesh[2] = mesh[2]; - } - - if (cao >= 0) - p3m.params.cao = cao; - - if (accuracy >= 0) - p3m.params.accuracy = accuracy; -} - -void p3m_set_params(double r_cut, const int *mesh, int cao, double alpha, - double accuracy) { - if (r_cut < 0) - throw std::runtime_error("P3M: invalid r_cut"); - - if (mesh[0] < 0 || mesh[1] < 0 || mesh[2] < 0) - throw std::runtime_error("P3M: invalid mesh size"); - - if (cao < 1 || cao > 7) - throw std::runtime_error("P3M: invalid cao"); - - if (cao > mesh[0] || cao > mesh[1] || cao > mesh[2]) - throw std::runtime_error("P3M: cao larger than mesh size"); - - if (alpha <= 0.0 && alpha != -1.0) - throw std::runtime_error("P3M: invalid alpha"); - - if (accuracy <= 0.0 && accuracy != -1.0) - throw std::runtime_error("P3M: invalid accuracy"); - - if (coulomb.method != COULOMB_P3M && coulomb.method != COULOMB_ELC_P3M && - coulomb.method != COULOMB_P3M_GPU) - coulomb.method = COULOMB_P3M; - - p3m.params.r_cut = r_cut; - p3m.params.r_cut_iL = r_cut * box_geo.length_inv()[0]; - p3m.params.mesh[2] = mesh[2]; - p3m.params.mesh[1] = mesh[1]; - p3m.params.mesh[0] = mesh[0]; - p3m.params.cao = cao; - p3m.params.alpha = alpha; - p3m.params.alpha_L = alpha * box_geo.length()[0]; - p3m.params.accuracy = accuracy; - - mpi_bcast_coulomb_params(); -} - -void p3m_set_mesh_offset(double x, double y, double z) { - if (x == -1.0 && y == -1.0 && z == -1.0) - return; - - if (x < 0.0 || x > 1.0 || y < 0.0 || y > 1.0 || z < 0.0 || z > 1.0) - throw std::runtime_error("P3M: invalid mesh offset"); - - p3m.params.mesh_off[0] = x; - p3m.params.mesh_off[1] = y; - p3m.params.mesh_off[2] = z; - - mpi_bcast_coulomb_params(); -} - -void p3m_set_eps(double eps) { - p3m.params.epsilon = eps; - - mpi_bcast_coulomb_params(); -} - -namespace { -template struct AssignCharge { - void operator()(double q, const Utils::Vector3d &real_pos, - const Utils::Vector3d &ai, P3MLocalMesh const &local_mesh, - p3m_interpolation_cache &inter_weights) { - auto const w = - p3m_calculate_interpolation_weights(real_pos, ai, local_mesh); - - inter_weights.store(w); - - p3m_interpolate(local_mesh, w, - [q](int ind, double w) { p3m.rs_mesh[ind] += w * q; }); - } - - void operator()(double q, const Utils::Vector3d &real_pos, - const Utils::Vector3d &ai, P3MLocalMesh const &local_mesh) { - p3m_interpolate( - local_mesh, - p3m_calculate_interpolation_weights(real_pos, ai, local_mesh), - [q](int ind, double w) { p3m.rs_mesh[ind] += w * q; }); - } - - void operator()(const ParticleRange &particles) { - for (auto &p : particles) { - if (p.p.q != 0.0) { - this->operator()(p.p.q, p.r.p, p3m.params.ai, p3m.local_mesh, - p3m.inter_weights); - } - } - } -}; -} // namespace - -void p3m_charge_assign(const ParticleRange &particles) { - p3m.inter_weights.reset(p3m.params.cao); - - /* prepare local FFT mesh */ - for (int i = 0; i < p3m.local_mesh.size; i++) - p3m.rs_mesh[i] = 0.0; - - Utils::integral_parameter(p3m.params.cao, particles); -} - -void p3m_assign_charge(double q, const Utils::Vector3d &real_pos, - p3m_interpolation_cache &inter_weights) { - Utils::integral_parameter(p3m.params.cao, q, real_pos, - p3m.params.ai, p3m.local_mesh, - inter_weights); -} - -void p3m_assign_charge(double q, const Utils::Vector3d &real_pos) { - Utils::integral_parameter(p3m.params.cao, q, real_pos, - p3m.params.ai, p3m.local_mesh); -} - -namespace { -template struct AssignForces { - void operator()(double force_prefac, const ParticleRange &particles) const { - using Utils::make_const_span; - using Utils::Span; - using Utils::Vector; - - assert(cao == p3m.inter_weights.cao()); - - /* charged particle counter */ - int cp_cnt = 0; - - for (auto &p : particles) { - auto const q = p.p.q; - if (q != 0.0) { - auto const pref = q * force_prefac; - auto const w = p3m.inter_weights.load(cp_cnt++); - - Utils::Vector3d E{}; - p3m_interpolate(p3m.local_mesh, w, [&E](int ind, double w) { - E += w * Utils::Vector3d{p3m.E_mesh[0][ind], p3m.E_mesh[1][ind], - p3m.E_mesh[2][ind]}; - }); - - p.f.f -= pref * E; - } - } - } -}; - -auto dipole_moment(Particle const &p, BoxGeometry const &box) { - return p.p.q * unfolded_position(p.r.p, p.l.i, box.length()); -} - -auto calc_dipole_moment(boost::mpi::communicator const &comm, - const ParticleRange &particles, - BoxGeometry const &box) { - auto const local_dip = boost::accumulate( - particles, Utils::Vector3d{}, [&box](Utils::Vector3d dip, auto const &p) { - return dip + dipole_moment(p, box); - }); - - return boost::mpi::all_reduce(comm, local_dip, std::plus<>()); -} - -void add_dipole_correction(Utils::Vector3d const &box_dipole, - const ParticleRange &particles) { - auto const pref = coulomb.prefactor * 4 * Utils::pi() / box_geo.volume() / - (2 * p3m.params.epsilon + 1); - - auto const dm = pref * box_dipole; - - for (auto &p : particles) { - p.f.f -= p.p.q * dm; - } -} - -double dipole_correction_energy(Utils::Vector3d const &box_dipole) { - auto const pref = coulomb.prefactor * 4 * Utils::pi() / box_geo.volume() / - (2 * p3m.params.epsilon + 1); - - return pref * box_dipole.norm2(); -} -} // namespace - -/** @details Calculate the long range electrostatics part of the pressure - * tensor. This is part \f$\Pi_{\textrm{dir}, \alpha, \beta}\f$ eq. (2.6) - * in @cite essmann95a. The part \f$\Pi_{\textrm{corr}, \alpha, \beta}\f$ - * eq. (2.8) is not present here since M is the empty set in our simulations. - */ -Utils::Vector9d p3m_calc_kspace_pressure_tensor() { - using namespace detail::FFT_indexing; - - Utils::Vector9d node_k_space_pressure_tensor{}; - - if (p3m.sum_q2 > 0) { - p3m.sm.gather_grid(p3m.rs_mesh.data(), comm_cart, p3m.local_mesh.dim); - fft_perform_forw(p3m.rs_mesh.data(), p3m.fft, comm_cart); - - double diagonal = 0; - int ind = 0; - int j[3]; - auto const half_alpha_inv_sq = Utils::sqr(1.0 / 2.0 / p3m.params.alpha); - for (j[0] = 0; j[0] < p3m.fft.plan[3].new_mesh[RX]; j[0]++) { - for (j[1] = 0; j[1] < p3m.fft.plan[3].new_mesh[RY]; j[1]++) { - for (j[2] = 0; j[2] < p3m.fft.plan[3].new_mesh[RZ]; j[2]++) { - auto const kx = 2.0 * Utils::pi() * - p3m.d_op[RX][j[KX] + p3m.fft.plan[3].start[KX]] * - box_geo.length_inv()[RX]; - auto const ky = 2.0 * Utils::pi() * - p3m.d_op[RY][j[KY] + p3m.fft.plan[3].start[KY]] * - box_geo.length_inv()[RY]; - auto const kz = 2.0 * Utils::pi() * - p3m.d_op[RZ][j[KZ] + p3m.fft.plan[3].start[KZ]] * - box_geo.length_inv()[RZ]; - auto const sqk = Utils::sqr(kx) + Utils::sqr(ky) + Utils::sqr(kz); - - auto const node_k_space_energy = - (sqk == 0) - ? 0.0 - : p3m.g_energy[ind] * (Utils::sqr(p3m.rs_mesh[2 * ind]) + - Utils::sqr(p3m.rs_mesh[2 * ind + 1])); - ind++; - - auto const vterm = - (sqk == 0) ? 0. : -2.0 * (1 / sqk + half_alpha_inv_sq); - - diagonal += node_k_space_energy; - auto const prefactor = node_k_space_energy * vterm; - node_k_space_pressure_tensor[0] += prefactor * kx * kx; /* sigma_xx */ - node_k_space_pressure_tensor[1] += prefactor * kx * ky; /* sigma_xy */ - node_k_space_pressure_tensor[2] += prefactor * kx * kz; /* sigma_xz */ - node_k_space_pressure_tensor[3] += prefactor * ky * kx; /* sigma_yx */ - node_k_space_pressure_tensor[4] += prefactor * ky * ky; /* sigma_yy */ - node_k_space_pressure_tensor[5] += prefactor * ky * kz; /* sigma_yz */ - node_k_space_pressure_tensor[6] += prefactor * kz * kx; /* sigma_zx */ - node_k_space_pressure_tensor[7] += prefactor * kz * ky; /* sigma_zy */ - node_k_space_pressure_tensor[8] += prefactor * kz * kz; /* sigma_zz */ - } - } - } - node_k_space_pressure_tensor[0] += diagonal; - node_k_space_pressure_tensor[4] += diagonal; - node_k_space_pressure_tensor[8] += diagonal; - } - - auto const force_prefac = coulomb.prefactor / (2.0 * box_geo.volume()); - return force_prefac * node_k_space_pressure_tensor; -} - -double p3m_calc_kspace_forces(bool force_flag, bool energy_flag, - const ParticleRange &particles) { - /* Gather information for FFT grid inside the nodes domain (inner local mesh) - * and perform forward 3D FFT (Charge Assignment Mesh). */ - p3m.sm.gather_grid(p3m.rs_mesh.data(), comm_cart, p3m.local_mesh.dim); - fft_perform_forw(p3m.rs_mesh.data(), p3m.fft, comm_cart); - - // Note: after these calls, the grids are in the order yzx and not xyz - // anymore!!! - /* The dipole moment is only needed if we don't have metallic boundaries. */ - auto const box_dipole = (p3m.params.epsilon != P3M_EPSILON_METALLIC) - ? boost::make_optional(calc_dipole_moment( - comm_cart, particles, box_geo)) - : boost::none; - - /* === k-space force calculation === */ - if (force_flag) { - /* sqrt(-1)*k differentiation */ - int j[3]; - int ind = 0; - for (j[0] = 0; j[0] < p3m.fft.plan[3].new_mesh[0]; j[0]++) { - for (j[1] = 0; j[1] < p3m.fft.plan[3].new_mesh[1]; j[1]++) { - for (j[2] = 0; j[2] < p3m.fft.plan[3].new_mesh[2]; j[2]++) { - auto const rho_hat = std::complex(p3m.rs_mesh[2 * ind + 0], - p3m.rs_mesh[2 * ind + 1]); - auto const phi_hat = p3m.g_force[ind] * rho_hat; - - for (int d = 0; d < 3; d++) { - /* direction in r-space: */ - int d_rs = (d + p3m.ks_pnum) % 3; - /* directions */ - auto const k = 2.0 * Utils::pi() * - p3m.d_op[d_rs][j[d] + p3m.fft.plan[3].start[d]] * - box_geo.length_inv()[d_rs]; - - /* i*k*(Re+i*Im) = - Im*k + i*Re*k (i=sqrt(-1)) */ - p3m.E_mesh[d_rs][2 * ind + 0] = -k * phi_hat.imag(); - p3m.E_mesh[d_rs][2 * ind + 1] = +k * phi_hat.real(); - } - - ind++; - } - } - } - - /* Back FFT force component mesh */ - for (int d = 0; d < 3; d++) { - fft_perform_back(p3m.E_mesh[d].data(), - /* check_complex */ !p3m.params.tuning, p3m.fft, - comm_cart); - } - - { - std::array E_fields = { - {p3m.E_mesh[0].data(), p3m.E_mesh[1].data(), p3m.E_mesh[2].data()}}; - /* redistribute force component mesh */ - p3m.sm.spread_grid(Utils::make_span(E_fields), comm_cart, - p3m.local_mesh.dim); - } - - auto const force_prefac = coulomb.prefactor / box_geo.volume(); - Utils::integral_parameter(p3m.params.cao, force_prefac, - particles); - - if (p3m.params.epsilon != P3M_EPSILON_METALLIC) { - add_dipole_correction(box_dipole.value(), particles); - } - } /* if(force_flag) */ - - /* === k-space energy calculation === */ - if (energy_flag) { - double node_k_space_energy = 0.; - - for (int i = 0; i < p3m.fft.plan[3].new_size; i++) { - // Use the energy optimized influence function for energy! - node_k_space_energy += - p3m.g_energy[i] * - (Utils::sqr(p3m.rs_mesh[2 * i]) + Utils::sqr(p3m.rs_mesh[2 * i + 1])); - } - node_k_space_energy *= coulomb.prefactor / (2 * box_geo.volume()); - - double k_space_energy = 0.0; - boost::mpi::reduce(comm_cart, node_k_space_energy, k_space_energy, - std::plus<>(), 0); - if (this_node == 0) { - /* self energy correction */ - k_space_energy -= coulomb.prefactor * - (p3m.sum_q2 * p3m.params.alpha * Utils::sqrt_pi_i()); - /* net charge correction */ - k_space_energy -= coulomb.prefactor * p3m.square_sum_q * Utils::pi() / - (2.0 * box_geo.volume() * Utils::sqr(p3m.params.alpha)); - /* dipole correction */ - if (p3m.params.epsilon != P3M_EPSILON_METALLIC) { - k_space_energy += dipole_correction_energy(box_dipole.value()); - } - } - return k_space_energy; - } /* if (energy_flag) */ - - return 0.0; -} - -void p3m_calc_influence_function_force() { - auto const start = Utils::Vector3i{p3m.fft.plan[3].start}; - auto const size = Utils::Vector3i{p3m.fft.plan[3].new_mesh}; - - p3m.g_force = grid_influence_function<1>(p3m.params, start, start + size, - box_geo.length()); -} - -void p3m_calc_influence_function_energy() { - auto const start = Utils::Vector3i{p3m.fft.plan[3].start}; - auto const size = Utils::Vector3i{p3m.fft.plan[3].new_mesh}; - - p3m.g_energy = grid_influence_function<0>(p3m.params, start, start + size, - box_geo.length()); -} - -/** Get the minimal error for this combination of parameters. - * - * The real space error is tuned such that it contributes half of the - * total error, and then the Fourier space error is calculated. - * If an optimal alpha is not found, the value 0.1 is used as fallback. - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[out] _alpha_L @copybrief P3MParameters::alpha_L - * @param[out] _rs_err real space error - * @param[out] _ks_err Fourier space error - * @returns Error magnitude - */ -static double p3m_get_accuracy(const int mesh[3], int cao, double r_cut_iL, - double *_alpha_L, double *_rs_err, - double *_ks_err) { - double rs_err, ks_err; - double alpha_L; - - /* calc maximal real space error for setting */ - rs_err = p3m_real_space_error(coulomb.prefactor, r_cut_iL, p3m.sum_qpart, - p3m.sum_q2, 0); - - if (Utils::sqrt_2() * rs_err > p3m.params.accuracy) { - /* assume rs_err = ks_err -> rs_err = accuracy/sqrt(2.0) -> alpha_L */ - alpha_L = - sqrt(log(Utils::sqrt_2() * rs_err / p3m.params.accuracy)) / r_cut_iL; - } else { - /* even alpha=0 is ok, however, we cannot choose it since it kills the - k-space error formula. - Anyways, this very likely NOT the optimal solution */ - alpha_L = 0.1; - } - - *_alpha_L = alpha_L; - /* calculate real space and k-space error for this alpha_L */ - rs_err = p3m_real_space_error(coulomb.prefactor, r_cut_iL, p3m.sum_qpart, - p3m.sum_q2, alpha_L); -#ifdef CUDA - if (coulomb.method == COULOMB_P3M_GPU) - ks_err = - p3m_k_space_error_gpu(coulomb.prefactor, mesh, cao, p3m.sum_qpart, - p3m.sum_q2, alpha_L, box_geo.length().data()); - else -#endif - ks_err = p3m_k_space_error(coulomb.prefactor, mesh, cao, p3m.sum_qpart, - p3m.sum_q2, alpha_L); - - *_rs_err = rs_err; - *_ks_err = ks_err; - return sqrt(Utils::sqr(rs_err) + Utils::sqr(ks_err)); -} - -/** Get the computation time for some @p mesh, @p cao, @p r_cut and @p alpha. - * - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[in] alpha_L @copybrief P3MParameters::alpha_L - * @param[in] timings Number of test force calculations - * - * @returns The integration time in case of success, otherwise - * -@ref P3M_TUNE_FAIL - */ -static double p3m_mcr_time(const int mesh[3], int cao, double r_cut_iL, - double alpha_L, int timings) { - - /* broadcast p3m parameters for test run */ - if (coulomb.method != COULOMB_P3M && coulomb.method != COULOMB_ELC_P3M && - coulomb.method != COULOMB_P3M_GPU) - coulomb.method = COULOMB_P3M; - - p3m.params.r_cut = r_cut_iL * box_geo.length()[0]; - p3m.params.r_cut_iL = r_cut_iL; - p3m.params.mesh[0] = mesh[0]; - p3m.params.mesh[1] = mesh[1]; - p3m.params.mesh[2] = mesh[2]; - p3m.params.cao = cao; - p3m.params.alpha_L = alpha_L; - p3m.params.alpha = p3m.params.alpha_L * box_geo.length_inv()[0]; - - /* initialize p3m structures */ - mpi_bcast_coulomb_params(); - /* perform force calculation test */ - double const int_time = time_force_calc(timings); - if (int_time == -1) { - return -P3M_TUNE_FAIL; - } - return int_time; -} - -/** Get the optimal alpha and the corresponding computation time for a fixed - * @p mesh and @p cao. - * - * The @p _r_cut_iL is determined via a simple bisection. - * - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] r_cut_iL_min lower bound for @p _r_cut_iL - * @param[in] r_cut_iL_max upper bound for @p _r_cut_iL - * @param[out] _r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[out] _alpha_L @copybrief P3MParameters::alpha_L - * @param[out] _accuracy @copybrief P3MParameters::accuracy - * @param[in] timings Number of test force calculations - * @param[in] verbose printf output - * - * @returns The integration time in case of success, otherwise - * -@ref P3M_TUNE_FAIL, -@ref P3M_TUNE_ACCURACY_TOO_LARGE, - * -@ref P3M_TUNE_CAO_TOO_LARGE, or -@ref P3M_TUNE_ELCTEST - */ -static double p3m_mc_time(const int mesh[3], int cao, double r_cut_iL_min, - double r_cut_iL_max, double *_r_cut_iL, - double *_alpha_L, double *_accuracy, int timings, - bool verbose) { - double rs_err, ks_err; - - /* initial checks. */ - auto const k_cut = - std::max(box_geo.length()[0] * cao / (2.0 * mesh[0]), - std::max(box_geo.length()[1] * cao / (2.0 * mesh[1]), - box_geo.length()[2] * cao / (2.0 * mesh[2]))); - - auto const min_box_l = *boost::min_element(box_geo.length()); - auto const min_local_box_l = *boost::min_element(local_geo.length()); - - if (cao >= std::min(mesh[0], std::min(mesh[1], mesh[2])) || - k_cut >= (std::min(min_box_l, min_local_box_l) - skin)) { - if (verbose) { - std::printf("%-4d %-3d cao too large for this mesh\n", mesh[0], cao); - } - return -P3M_TUNE_CAO_TOO_LARGE; - } - - /* Either low and high boundary are equal (for fixed cut), or the low border - is initially 0 and therefore - has infinite error estimate, as required. Therefore if the high boundary - fails, there is no possible r_cut */ - if ((*_accuracy = p3m_get_accuracy(mesh, cao, r_cut_iL_max, _alpha_L, &rs_err, - &ks_err)) > p3m.params.accuracy) { - /* print result */ - if (verbose) { - std::printf("%-4d %-3d %.5e %.5e %.5e %.3e %.3e accuracy not achieved\n", - mesh[0], cao, r_cut_iL_max, *_alpha_L, *_accuracy, rs_err, - ks_err); - } - return -P3M_TUNE_ACCURACY_TOO_LARGE; - } - - double r_cut_iL; - for (;;) { - r_cut_iL = 0.5 * (r_cut_iL_min + r_cut_iL_max); - - if (r_cut_iL_max - r_cut_iL_min < P3M_RCUT_PREC) - break; - - /* bisection */ - if ((p3m_get_accuracy(mesh, cao, r_cut_iL, _alpha_L, &rs_err, &ks_err) > - p3m.params.accuracy)) - r_cut_iL_min = r_cut_iL; - else - r_cut_iL_max = r_cut_iL; - } - - /* final result is always the upper interval boundary, since only there - we know that the desired minimal accuracy is obtained */ - *_r_cut_iL = r_cut_iL = r_cut_iL_max; - - /* check whether we are running P3M+ELC, and whether we leave a reasonable - * gap - * space */ - if (coulomb.method == COULOMB_ELC_P3M && - elc_params.gap_size <= 1.1 * r_cut_iL * box_geo.length()[0]) { - /* print result */ - if (verbose) { - std::printf("%-4d %-3d %.5e %.5e %.5e %.3e %.3e conflict with ELC\n", - mesh[0], cao, r_cut_iL, *_alpha_L, *_accuracy, rs_err, - ks_err); - } - return -P3M_TUNE_ELCTEST; - } - - auto const int_time = p3m_mcr_time(mesh, cao, r_cut_iL, *_alpha_L, timings); - if (int_time == -P3M_TUNE_FAIL) { - if (verbose) { - std::printf("tuning failed, test integration not possible\n"); - } - return int_time; - } - - *_accuracy = - p3m_get_accuracy(mesh, cao, r_cut_iL, _alpha_L, &rs_err, &ks_err); - - /* print result */ - if (verbose) { - std::printf("%-4d %-3d %.5e %.5e %.5e %.3e %.3e %-8.2f\n", mesh[0], cao, - r_cut_iL, *_alpha_L, *_accuracy, rs_err, ks_err, int_time); - } - return int_time; -} - -/** Get the optimal alpha and the corresponding computation time for a fixed - * @p mesh. - * - * @p _cao should contain an initial guess, which is then adapted by stepping - * up and down. - * - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao_min lower bound for @p _cao - * @param[in] cao_max upper bound for @p _cao - * @param[in,out] _cao initial guess for the - * @copybrief P3MParameters::cao - * @param[in] r_cut_iL_min lower bound for @p _r_cut_iL - * @param[in] r_cut_iL_max upper bound for @p _r_cut_iL - * @param[out] _r_cut_iL @copybrief P3MParameters::r_cut_iL - * @param[out] _alpha_L @copybrief P3MParameters::alpha_L - * @param[out] _accuracy @copybrief P3MParameters::accuracy - * @param[in] timings Number of test force calculations - * @param[in] verbose printf output - * - * @returns The integration time in case of success, otherwise - * -@ref P3M_TUNE_FAIL or -@ref P3M_TUNE_CAO_TOO_LARGE - */ -static double p3m_m_time(const int mesh[3], int cao_min, int cao_max, int *_cao, - double r_cut_iL_min, double r_cut_iL_max, - double *_r_cut_iL, double *_alpha_L, double *_accuracy, - int timings, bool verbose) { - double best_time = -1, tmp_time, tmp_r_cut_iL = 0.0, tmp_alpha_L = 0.0, - tmp_accuracy = 0.0; - /* in which direction improvement is possible. Initially, we don't know it - * yet. - */ - int final_dir = 0; - int cao = *_cao; - - /* the initial step sets a timing mark. If there is no valid r_cut, we can - only try - to increase cao to increase the obtainable precision of the far formula. - */ - do { - tmp_time = p3m_mc_time(mesh, cao, r_cut_iL_min, r_cut_iL_max, &tmp_r_cut_iL, - &tmp_alpha_L, &tmp_accuracy, timings, verbose); - /* bail out if the force evaluation is not working */ - if (tmp_time == -P3M_TUNE_FAIL) - return tmp_time; - /* cao is too large for this grid, but still the accuracy cannot be - * achieved, give up */ - if (tmp_time == -P3M_TUNE_CAO_TOO_LARGE) { - return tmp_time; - } - /* we have a valid time, start optimising from there */ - if (tmp_time >= 0) { - best_time = tmp_time; - *_r_cut_iL = tmp_r_cut_iL; - *_alpha_L = tmp_alpha_L; - *_accuracy = tmp_accuracy; - *_cao = cao; - break; - } - /* the required accuracy could not be obtained, try higher caos */ - cao++; - final_dir = 1; - } while (cao <= cao_max); - /* with this mesh, the required accuracy cannot be obtained. */ - if (cao > cao_max) - return -P3M_TUNE_CAO_TOO_LARGE; - - /* at the boundaries, only the opposite direction can be used for - * optimisation - */ - if (cao == cao_min) - final_dir = 1; - else if (cao == cao_max) - final_dir = -1; - - if (final_dir == 0) { - /* check in which direction we can optimise. Both directions are possible - */ - double dir_times[3]; - for (final_dir = -1; final_dir <= 1; final_dir += 2) { - dir_times[final_dir + 1] = tmp_time = p3m_mc_time( - mesh, cao + final_dir, r_cut_iL_min, r_cut_iL_max, &tmp_r_cut_iL, - &tmp_alpha_L, &tmp_accuracy, timings, verbose); - /* bail out on errors, as usual */ - if (tmp_time == -P3M_TUNE_FAIL) - return tmp_time; - /* in this direction, we cannot optimise, since we get into precision - * trouble */ - if (tmp_time < 0) - continue; - - if (tmp_time < best_time) { - best_time = tmp_time; - *_r_cut_iL = tmp_r_cut_iL; - *_alpha_L = tmp_alpha_L; - *_accuracy = tmp_accuracy; - *_cao = cao + final_dir; - } - } - /* choose the direction which was optimal, if any of the two */ - if (dir_times[0] == best_time) { - final_dir = -1; - } else if (dir_times[2] == best_time) { - final_dir = 1; - } else { - /* no improvement in either direction, however if one is only marginally - * worse, we can still try */ - /* down is possible and not much worse, while up is either illegal or - * even - * worse */ - if ((dir_times[0] >= 0 && dir_times[0] < best_time + P3M_TIME_GRAN) && - (dir_times[2] < 0 || dir_times[2] > dir_times[0])) - final_dir = -1; - /* same for up */ - else if ((dir_times[2] >= 0 && - dir_times[2] < best_time + P3M_TIME_GRAN) && - (dir_times[0] < 0 || dir_times[0] > dir_times[2])) - final_dir = 1; - else { - /* really no chance for optimisation */ - return best_time; - } - } - /* we already checked the initial cao and its neighbor */ - cao += 2 * final_dir; - } else { - /* here some constraint is active, and we only checked the initial cao - * itself */ - cao += final_dir; - } - - /* move cao into the optimisation direction until we do not gain anymore. */ - for (; cao >= cao_min && cao <= cao_max; cao += final_dir) { - tmp_time = p3m_mc_time(mesh, cao, r_cut_iL_min, r_cut_iL_max, &tmp_r_cut_iL, - &tmp_alpha_L, &tmp_accuracy, timings, verbose); - /* bail out on errors, as usual */ - if (tmp_time == -P3M_TUNE_FAIL) - return tmp_time; - /* if we cannot meet the precision anymore, give up */ - if (tmp_time < 0) - break; - - if (tmp_time < best_time) { - best_time = tmp_time; - *_r_cut_iL = tmp_r_cut_iL; - *_alpha_L = tmp_alpha_L; - *_accuracy = tmp_accuracy; - *_cao = cao; - } - /* no hope of further optimisation */ - else if (tmp_time > best_time + P3M_TIME_GRAN) - break; - } - return best_time; -} - -int p3m_adaptive_tune(int timings, bool verbose) { - double r_cut_iL_min, r_cut_iL_max, r_cut_iL = -1, tmp_r_cut_iL = 0.0; - int cao_min, cao_max, cao = -1, tmp_cao; - double alpha_L = -1, tmp_alpha_L = 0.0; - double accuracy = -1, tmp_accuracy = 0.0; - double time_best = 1e20; - double mesh_density_min, mesh_density_max; - bool tune_mesh = false; // indicates if mesh should be tuned - - if (p3m.params.epsilon != P3M_EPSILON_METALLIC) { - if (!((box_geo.length()[0] == box_geo.length()[1]) && - (box_geo.length()[1] == box_geo.length()[2]))) { - runtimeErrorMsg() << "non-metallic epsilon requires cubic box"; - return ES_ERROR; - } - } - - if (p3m_sanity_checks_system(node_grid)) { - return ES_ERROR; - } - - /* preparation */ - mpi_call_all(p3m_count_charged_particles); - - if (p3m.sum_qpart == 0) { - runtimeErrorMsg() << "no charged particles in the system"; - return ES_ERROR; - } - - /* Print Status */ - if (verbose) { - std::printf("P3M tune parameters: Accuracy goal = %.5e prefactor = %.5e\n" - "System: box_l = %.5e # charged part = %d Sum[q_i^2] = %.5e\n", - p3m.params.accuracy, coulomb.prefactor, box_geo.length()[0], - p3m.sum_qpart, p3m.sum_q2); - } - - /* Activate tuning mode */ - p3m.params.tuning = true; - - /* parameter ranges */ - /* if at least the number of meshpoints in one direction is not set, we have - * to tune it. */ - if (p3m.params.mesh[0] == 0 || p3m.params.mesh[1] == 0 || - p3m.params.mesh[2] == 0) { - /* Medium-educated guess for the minimal mesh */ - mesh_density_min = pow(p3m.sum_qpart / box_geo.volume(), 1.0 / 3.0); - mesh_density_max = 512 / pow(box_geo.volume(), 1.0 / 3.0); - tune_mesh = true; - /* this limits the tried meshes if the accuracy cannot - be obtained with smaller meshes, but normally not all these - meshes have to be tested */ - /* avoid using more than 1 GB of FFT arrays (per default, see config.hpp) - */ - } else if (p3m.params.mesh[1] == -1 && p3m.params.mesh[2] == -1) { - double mesh_density = mesh_density_min = mesh_density_max = - p3m.params.mesh[0] * box_geo.length_inv()[0]; - p3m.params.mesh[1] = - static_cast(std::round(mesh_density * box_geo.length()[1])); - p3m.params.mesh[2] = - static_cast(std::round(mesh_density * box_geo.length()[2])); - if (p3m.params.mesh[1] % 2 == 1) - p3m.params.mesh[1]++; // Make sure that the mesh is even in all directions - if (p3m.params.mesh[2] % 2 == 1) - p3m.params.mesh[2]++; - - if (verbose) { - std::printf("fixed mesh %d %d %d\n", p3m.params.mesh[0], - p3m.params.mesh[1], p3m.params.mesh[2]); - } - } else { - mesh_density_min = mesh_density_max = - p3m.params.mesh[0] * box_geo.length_inv()[0]; - - if (verbose) { - std::printf("fixed mesh %d %d %d\n", p3m.params.mesh[0], - p3m.params.mesh[1], p3m.params.mesh[2]); - } - } - - if (p3m.params.r_cut_iL == 0.0) { - auto const min_box_l = *boost::min_element(box_geo.length()); - auto const min_local_box_l = *boost::min_element(local_geo.length()); - - r_cut_iL_min = 0; - r_cut_iL_max = std::min(min_local_box_l, min_box_l / 2.0) - skin; - r_cut_iL_min *= box_geo.length_inv()[0]; - r_cut_iL_max *= box_geo.length_inv()[0]; - } else { - r_cut_iL_min = r_cut_iL_max = p3m.params.r_cut_iL; - - if (verbose) { - std::printf("fixed r_cut_iL %f\n", p3m.params.r_cut_iL); - } - } - - if (p3m.params.cao == 0) { - cao_min = 1; - cao_max = 7; - cao = cao_max; - } else { - cao_min = cao_max = cao = p3m.params.cao; - - if (verbose) { - std::printf("fixed cao %d\n", p3m.params.cao); - } - } - - if (verbose) { - std::printf("mesh cao r_cut_iL alpha_L err " - "rs_err ks_err time [ms]\n"); - } - - /* mesh loop */ - /* we're tuning the density of mesh points, which is the same in every - * direction. */ - int mesh[3] = {0, 0, 0}; - for (auto mesh_density = mesh_density_min; mesh_density <= mesh_density_max; - mesh_density += 0.1) { - tmp_cao = cao; - - int tmp_mesh[3]; - if (tune_mesh) { - tmp_mesh[0] = - static_cast(std::round(box_geo.length()[0] * mesh_density)); - tmp_mesh[1] = - static_cast(std::round(box_geo.length()[1] * mesh_density)); - tmp_mesh[2] = - static_cast(std::round(box_geo.length()[2] * mesh_density)); - } else { - tmp_mesh[0] = p3m.params.mesh[0]; - tmp_mesh[1] = p3m.params.mesh[1]; - tmp_mesh[2] = p3m.params.mesh[2]; - } - - if (tmp_mesh[0] % 2) // Make sure that the mesh is even in all directions - tmp_mesh[0]++; - if (tmp_mesh[1] % 2) - tmp_mesh[1]++; - if (tmp_mesh[2] % 2) - tmp_mesh[2]++; - - auto const tmp_time = p3m_m_time( - tmp_mesh, cao_min, cao_max, &tmp_cao, r_cut_iL_min, r_cut_iL_max, - &tmp_r_cut_iL, &tmp_alpha_L, &tmp_accuracy, timings, verbose); - /* some error occurred during the tuning force evaluation */ - if (tmp_time == -P3M_TUNE_FAIL) - return ES_ERROR; - /* this mesh does not work at all */ - if (tmp_time < 0.0) - continue; - - /* the optimum r_cut for this mesh is the upper limit for higher meshes, - everything else is slower */ - if (coulomb.method == COULOMB_P3M) - r_cut_iL_max = tmp_r_cut_iL; - - /* new optimum */ - if (tmp_time < time_best) { - time_best = tmp_time; - mesh[0] = tmp_mesh[0]; - mesh[1] = tmp_mesh[1]; - mesh[2] = tmp_mesh[2]; - cao = tmp_cao; - r_cut_iL = tmp_r_cut_iL; - alpha_L = tmp_alpha_L; - accuracy = tmp_accuracy; - } - /* no hope of further optimisation */ - else if (tmp_time > time_best + P3M_TIME_GRAN) { - break; - } - } - - if (time_best == 1e20) { - runtimeErrorMsg() << "failed to reach requested accuracy"; - return ES_ERROR; - } - - /* set tuned p3m parameters */ - p3m.params.tuning = false; - p3m.params.r_cut = r_cut_iL * box_geo.length()[0]; - p3m.params.r_cut_iL = r_cut_iL; - p3m.params.mesh[0] = mesh[0]; - p3m.params.mesh[1] = mesh[1]; - p3m.params.mesh[2] = mesh[2]; - p3m.params.cao = cao; - p3m.params.alpha_L = alpha_L; - p3m.params.alpha = p3m.params.alpha_L * box_geo.length_inv()[0]; - p3m.params.accuracy = accuracy; - /* broadcast tuned p3m parameters */ - mpi_bcast_coulomb_params(); - - /* Tell the user about the outcome */ - if (verbose) { - std::printf( - "\nresulting parameters: mesh: (%d %d %d), cao: %d, r_cut_iL: %.4e," - "\n alpha_L: %.4e, accuracy: %.4e, time: %.2f\n", - mesh[0], mesh[1], mesh[2], cao, r_cut_iL, alpha_L, accuracy, time_best); - } - return ES_OK; -} - -void p3m_count_charged_particles() { - using boost::mpi::all_reduce; - int local_n = 0; - double local_q2 = 0.0; - double local_q = 0.0; - - for (auto const &p : cell_structure.local_particles()) { - if (p.p.q != 0.0) { - local_n++; - local_q2 += Utils::sqr(p.p.q); - local_q += p.p.q; - } - } - - p3m.sum_qpart = all_reduce(comm_cart, local_n, std::plus<>()); - p3m.sum_q2 = all_reduce(comm_cart, local_q2, std::plus<>()); - p3m.square_sum_q = Utils::sqr(all_reduce(comm_cart, local_q, std::plus<>())); -} - -REGISTER_CALLBACK(p3m_count_charged_particles) - -double p3m_real_space_error(double prefac, double r_cut_iL, int n_c_part, - double sum_q2, double alpha_L) { - return (2.0 * prefac * sum_q2 * exp(-Utils::sqr(r_cut_iL * alpha_L))) / - sqrt((double)n_c_part * r_cut_iL * box_geo.length()[0] * - box_geo.volume()); -} - -double p3m_k_space_error(double prefac, const int mesh[3], int cao, - int n_c_part, double sum_q2, double alpha_L) { - double const mesh_i[3] = {1.0 / mesh[0], 1.0 / mesh[1], 1.0 / mesh[2]}; - auto const alpha_L_i = 1. / alpha_L; - double he_q = 0.0; - - for (int nx = -mesh[0] / 2; nx < mesh[0] / 2; nx++) { - auto const ctan_x = p3m_analytic_cotangent_sum(nx, mesh_i[0], cao); - for (int ny = -mesh[1] / 2; ny < mesh[1] / 2; ny++) { - auto const ctan_y = - ctan_x * p3m_analytic_cotangent_sum(ny, mesh_i[1], cao); - for (int nz = -mesh[2] / 2; nz < mesh[2] / 2; nz++) { - if ((nx != 0) || (ny != 0) || (nz != 0)) { - auto const n2 = Utils::sqr(nx) + Utils::sqr(ny) + Utils::sqr(nz); - auto const cs = - p3m_analytic_cotangent_sum(nz, mesh_i[2], cao) * ctan_y; - double alias1, alias2; - p3m_tune_aliasing_sums(nx, ny, nz, mesh, mesh_i, cao, alpha_L_i, - &alias1, &alias2); - - auto const d = alias1 - Utils::sqr(alias2 / cs) / n2; - /* at high precision, d can become negative due to extinction; - also, don't take values that have no significant digits left*/ - if (d > 0 && (fabs(d / alias1) > ROUND_ERROR_PREC)) - he_q += d; - } - } - } - } - return 2.0 * prefac * sum_q2 * sqrt(he_q / (double)n_c_part) / - (box_geo.length()[1] * box_geo.length()[2]); -} - -void p3m_tune_aliasing_sums(int nx, int ny, int nz, const int mesh[3], - const double mesh_i[3], int cao, double alpha_L_i, - double *alias1, double *alias2) { - using Utils::sinc; - - auto const factor1 = Utils::sqr(Utils::pi() * alpha_L_i); - - *alias1 = *alias2 = 0.0; - for (int mx = -P3M_BRILLOUIN; mx <= P3M_BRILLOUIN; mx++) { - auto const nmx = nx + mx * mesh[0]; - auto const fnmx = mesh_i[0] * nmx; - for (int my = -P3M_BRILLOUIN; my <= P3M_BRILLOUIN; my++) { - auto const nmy = ny + my * mesh[1]; - auto const fnmy = mesh_i[1] * nmy; - for (int mz = -P3M_BRILLOUIN; mz <= P3M_BRILLOUIN; mz++) { - auto const nmz = nz + mz * mesh[2]; - auto const fnmz = mesh_i[2] * nmz; - - auto const nm2 = Utils::sqr(nmx) + Utils::sqr(nmy) + Utils::sqr(nmz); - auto const ex = exp(-factor1 * nm2); - auto const ex2 = Utils::sqr(ex); - - auto const U2 = pow(sinc(fnmx) * sinc(fnmy) * sinc(fnmz), 2.0 * cao); - - *alias1 += ex2 / nm2; - *alias2 += U2 * ex * (nx * nmx + ny * nmy + nz * nmz) / nm2; - } - } - } -} - -void p3m_init_a_ai_cao_cut() { - for (int i = 0; i < 3; i++) { - p3m.params.ai[i] = p3m.params.mesh[i] * box_geo.length_inv()[i]; - p3m.params.a[i] = 1.0 / p3m.params.ai[i]; - p3m.params.cao_cut[i] = 0.5 * p3m.params.a[i] * p3m.params.cao; - } -} - -bool p3m_sanity_checks_boxl() { - bool ret = false; - for (int i = 0; i < 3; i++) { - /* check k-space cutoff */ - if (p3m.params.cao_cut[i] >= box_geo.length_half()[i]) { - runtimeErrorMsg() << "P3M_init: k-space cutoff " << p3m.params.cao_cut[i] - << " is larger than half of box dimension " - << box_geo.length()[i]; - ret = true; - } - if (p3m.params.cao_cut[i] >= local_geo.length()[i]) { - runtimeErrorMsg() << "P3M_init: k-space cutoff " << p3m.params.cao_cut[i] - << " is larger than local box dimension " - << local_geo.length()[i]; - ret = true; - } - } - - return ret; -} - -/** - * @brief General sanity checks independent of p3m parameters. - * - * @return false if ok, true on error. - */ -bool p3m_sanity_checks_system(const Utils::Vector3i &grid) { - bool ret = false; - - if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { - runtimeErrorMsg() << "P3M requires periodicity (1, 1, 1)"; - ret = true; - } - - if (local_geo.cell_structure_type() != - CellStructureType::CELL_STRUCTURE_REGULAR) { - runtimeErrorMsg() << "P3M requires the regular decomposition cell system"; - ret = true; - } - - if (grid[0] < grid[1] || grid[1] < grid[2]) { - runtimeErrorMsg() << "P3M_init: node grid must be sorted, largest first"; - ret = true; - } - - if (p3m.params.epsilon != P3M_EPSILON_METALLIC) { - if (!((p3m.params.mesh[0] == p3m.params.mesh[1]) && - (p3m.params.mesh[1] == p3m.params.mesh[2]))) { - runtimeErrorMsg() << "P3M_init: non-metallic epsilon requires cubic box"; - ret = true; - } - } - - return ret; -} - -bool p3m_sanity_checks_parameters() { - bool ret{false}; - - if (p3m.params.mesh[0] == 0) { - runtimeErrorMsg() << "P3M_init: mesh size is not yet set"; - ret = true; - } - if (p3m.params.cao == 0) { - runtimeErrorMsg() << "P3M_init: cao is not yet set"; - ret = true; - } - if (p3m.params.alpha < 0.0) { - runtimeErrorMsg() << "P3M_init: alpha must be >0"; - ret = true; - } - return ret; -} - -bool p3m_sanity_checks() { - bool ret = false; - - if (p3m_sanity_checks_system(node_grid)) - ret = true; - - if (p3m_sanity_checks_boxl()) - ret = true; - - if (p3m_sanity_checks_parameters()) - ret = true; - - return ret; -} - -void p3m_scaleby_box_l() { - if (coulomb.prefactor < 0.0) { - runtimeErrorMsg() << "The Coulomb prefactor has to be >=0"; - return; - } - - p3m.params.r_cut = p3m.params.r_cut_iL * box_geo.length()[0]; - p3m.params.alpha = p3m.params.alpha_L * box_geo.length_inv()[0]; - p3m_init_a_ai_cao_cut(); - p3m_calc_lm_ld_pos(p3m.local_mesh, p3m.params); - p3m_sanity_checks_boxl(); - p3m_calc_influence_function_force(); - p3m_calc_influence_function_energy(); -} - -#endif /* of P3M */ diff --git a/src/core/electrostatics_magnetostatics/p3m.hpp b/src/core/electrostatics_magnetostatics/p3m.hpp deleted file mode 100644 index 7312ef07b26..00000000000 --- a/src/core/electrostatics_magnetostatics/p3m.hpp +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_HPP -/** \file - * P3M algorithm for long range Coulomb interaction. - * - * We use a P3M (Particle-Particle Particle-Mesh) method based on the - * Ewald summation. Details of the used method can be found in - * @cite hockney88a and @cite deserno98a @cite deserno98b. - * - * Further reading: @cite ewald21a, @cite hockney88a, @cite deserno98a, - * @cite deserno98b, @cite deserno00e, @cite deserno00b, @cite cerda08d. - * - * Implementation in p3m.cpp. - */ - -#include "config.hpp" - -#ifdef P3M - -#include "electrostatics_magnetostatics/fft.hpp" -#include "electrostatics_magnetostatics/p3m-common.hpp" -#include "electrostatics_magnetostatics/p3m-data_struct.hpp" -#include "electrostatics_magnetostatics/p3m_interpolation.hpp" -#include "electrostatics_magnetostatics/p3m_send_mesh.hpp" - -#include "ParticleRange.hpp" - -#include -#include -#include - -#include -#include - -/************************************************ - * data types - ************************************************/ - -struct p3m_data_struct : public p3m_data_struct_base { - p3m_data_struct(); - - /** local mesh. */ - P3MLocalMesh local_mesh; - /** real space mesh (local) for CA/FFT. */ - fft_vector rs_mesh; - /** mesh (local) for the electric field. */ - std::array, 3> E_mesh; - - /** number of charged particles (only on head node). */ - int sum_qpart; - /** Sum of square of charges (only on head node). */ - double sum_q2; - /** square of sum of charges (only on head node). */ - double square_sum_q; - - p3m_interpolation_cache inter_weights; - - /** send/recv mesh sizes */ - p3m_send_mesh sm; - - fft_data_struct fft; -}; - -/** P3M parameters. */ -extern p3m_data_struct p3m; - -/** Tune P3M parameters to desired accuracy. - * - * The parameters - * @ref P3MParameters::mesh "mesh", - * @ref P3MParameters::cao "cao", - * @ref P3MParameters::r_cut_iL "r_cut_iL" and - * @ref P3MParameters::alpha_L "alpha_L" - * are tuned to obtain the target accuracy (initially stored in - * @ref P3MParameters::accuracy "accuracy") in optimal time. - * These parameters are stored in the @ref p3m object. - * - * The function utilizes the analytic expression of the error estimate - * for the P3M method in @cite hockney88a (eq. (8.23)) in - * order to obtain the rms error in the force for a system of N randomly - * distributed particles in a cubic box. - * For the real space error the estimate of Kolafa/Perram is used. - * - * Parameter ranges if not given explicit values via p3m_set_tune_params(): - * - @p mesh is set up such that the number of mesh points is equal to the - * number of charged particles - * - @p cao explores all possible values - * - @p alpha_L is tuned for each tuple (@p r_cut_iL, @p mesh, @p cao) and - * calculated assuming that the error contributions of real and reciprocal - * space should be equal - * - * After checking if the total error lies below the target accuracy, the - * time needed for one force calculation (including Verlet list update) - * is measured via time_force_calc(). - * - * The function generates a log of the performed tuning. - * - * The function is based on routines of the program HE_Q.cpp written by M. - * Deserno. - * - * @param timings Number of test force calculations - * @param verbose printf output - * @retval ES_OK - * @retval ES_ERROR - */ -int p3m_adaptive_tune(int timings, bool verbose); - -/** Initialize all structures, parameters and arrays needed for the - * P3M algorithm for charge-charge interactions. - */ -void p3m_init(); - -/** Update @ref P3MParameters::alpha "alpha" and - * @ref P3MParameters::r_cut "r_cut" if box length changed - */ -void p3m_scaleby_box_l(); - -/** Compute the k-space part of forces and energies for the charge-charge - * interaction - */ -double p3m_calc_kspace_forces(bool force_flag, bool energy_flag, - const ParticleRange &particles); - -/** Compute the k-space part of the pressure tensor */ -Utils::Vector9d p3m_calc_kspace_pressure_tensor(); - -/** Sanity checks */ -bool p3m_sanity_checks(); - -/** Calculate number of charged particles, the sum of the squared - * charges and the squared sum of the charges. - */ -void p3m_count_charged_particles(); - -/** Assign the physical charges using the tabulated charge assignment function. - */ -void p3m_charge_assign(const ParticleRange &particles); - -/** Assign a single charge into the current charge grid. - * - * @param[in] q %Particle charge - * @param[in] real_pos %Particle position in real space - * @param[in] inter_weights Cached interpolation weights to be used. - */ -void p3m_assign_charge(double q, const Utils::Vector3d &real_pos, - p3m_interpolation_cache &inter_weights); -/** @overload */ -void p3m_assign_charge(double q, const Utils::Vector3d &real_pos); - -/** Calculate real space contribution of Coulomb pair forces. */ -inline void p3m_add_pair_force(double q1q2, Utils::Vector3d const &d, - double dist, Utils::Vector3d &force) { - if (dist < p3m.params.r_cut) { - if (dist > 0.0) { - double adist = p3m.params.alpha * dist; -#if USE_ERFC_APPROXIMATION - auto const erfc_part_ri = Utils::AS_erfc_part(adist) / dist; - auto const fac1 = q1q2 * exp(-adist * adist); - auto const fac2 = - fac1 * (erfc_part_ri + 2.0 * p3m.params.alpha * Utils::sqrt_pi_i()) / - (dist * dist); -#else - auto const erfc_part_ri = erfc(adist) / dist; - auto const fac1 = q1q2; - auto const fac2 = - fac1 * - (erfc_part_ri + - 2.0 * p3m.params.alpha * Utils::sqrt_pi_i() * exp(-adist * adist)) / - (dist * dist); -#endif - force += fac2 * d; - } - } -} - -/** Set initial values for p3m_adaptive_tune() - * - * @param[in] r_cut @copybrief P3MParameters::r_cut - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] accuracy @copybrief P3MParameters::accuracy - */ -void p3m_set_tune_params(double r_cut, const int mesh[3], int cao, - double accuracy); - -/** Set custom parameters - * - * @param[in] r_cut @copybrief P3MParameters::r_cut - * @param[in] mesh @copybrief P3MParameters::mesh - * @param[in] cao @copybrief P3MParameters::cao - * @param[in] alpha @copybrief P3MParameters::alpha - * @param[in] accuracy @copybrief P3MParameters::accuracy - */ -void p3m_set_params(double r_cut, const int *mesh, int cao, double alpha, - double accuracy); - -/** Set mesh offset - * - * @param[in] x , y , z Components of @ref P3MParameters::mesh_off - * "mesh_off" - */ -void p3m_set_mesh_offset(double x, double y, double z); - -/** Set @ref P3MParameters::epsilon "epsilon" parameter - * - * @param[in] eps @copybrief P3MParameters::epsilon - */ -void p3m_set_eps(double eps); - -/** Calculate real space contribution of Coulomb pair energy. */ -inline double p3m_pair_energy(double chgfac, double dist) { - if (dist < p3m.params.r_cut && dist != 0) { - double adist = p3m.params.alpha * dist; -#if USE_ERFC_APPROXIMATION - double erfc_part_ri = Utils::AS_erfc_part(adist) / dist; - return chgfac * erfc_part_ri * exp(-adist * adist); -#else - double erfc_part_ri = erfc(adist) / dist; - return chgfac * erfc_part_ri; -#endif - } - return 0.0; -} - -#endif /* P3M */ - -#endif /* CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_HPP */ diff --git a/src/core/electrostatics_magnetostatics/reaction_field.cpp b/src/core/electrostatics_magnetostatics/reaction_field.cpp deleted file mode 100644 index 4bd17dfba37..00000000000 --- a/src/core/electrostatics_magnetostatics/reaction_field.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -/** \file - * - * Implementation of \ref reaction_field.hpp - */ -#include "reaction_field.hpp" - -#ifdef ELECTROSTATICS -#include "common.hpp" - -ReactionFieldParameters rf_params{}; - -void rf_set_params(double kappa, double epsilon1, double epsilon2, - double r_cut) { - if (kappa < 0.0) - throw std::domain_error("kappa should be a non-negative number"); - if (epsilon1 < 0.0) - throw std::domain_error("epsilon1 should be a non-negative number"); - if (epsilon2 < 0.0) - throw std::domain_error("epsilon2 should be a non-negative number"); - if (r_cut < 0.0) - throw std::domain_error("r_cut should be a non-negative number"); - - auto const B = (2 * (epsilon1 - epsilon2) * (1 + kappa * r_cut) - - epsilon2 * kappa * kappa * r_cut * r_cut) / - ((epsilon1 + 2 * epsilon2) * (1 + kappa * r_cut) + - epsilon2 * kappa * kappa * r_cut * r_cut); - rf_params = {kappa, epsilon1, epsilon2, r_cut, B}; - - mpi_bcast_coulomb_params(); -} -#endif diff --git a/src/core/electrostatics_magnetostatics/reaction_field.hpp b/src/core/electrostatics_magnetostatics/reaction_field.hpp deleted file mode 100644 index b8398e207d4..00000000000 --- a/src/core/electrostatics_magnetostatics/reaction_field.hpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef REACTION_FIELD_H -#define REACTION_FIELD_H -/** \file - * Routines to calculate the Reaction Field energy or/and force - * for a particle pair @cite neumann85b, @cite tironi95a. - * - * Implementation in \ref reaction_field.cpp - */ - -#include "config.hpp" - -#ifdef ELECTROSTATICS - -#include -#include - -/** Reaction Field parameters. */ -struct ReactionFieldParameters { - /** Ionic strength. */ - double kappa; - /** Continuum dielectric constant inside the cavity. */ - double epsilon1; - /** Continuum dielectric constant outside the cavity. */ - double epsilon2; - /** Interaction cutoff. */ - double r_cut; - /** Interaction prefactor. Corresponds to the quantity - * @f$ 1 + B_1 @f$ from eq. 22 in @cite tironi95a. - */ - double B; -}; - -/** Global state of the Reaction Field method. */ -extern ReactionFieldParameters rf_params; - -void rf_set_params(double kappa, double epsilon1, double epsilon2, - double r_cut); - -/** Compute the Reaction Field pair force. - * @param[in] q1q2 Product of the charges on p1 and p2. - * @param[in] d Vector pointing from p1 to p2. - * @param[in] dist Distance between p1 and p2. - * @param[out] force Calculated force on p1. - */ -inline void add_rf_coulomb_pair_force(double const q1q2, - Utils::Vector3d const &d, - double const dist, - Utils::Vector3d &force) { - if (dist < rf_params.r_cut) { - auto fac = 1.0 / Utils::int_pow<3>(dist) + - rf_params.B / Utils::int_pow<3>(rf_params.r_cut); - fac *= q1q2; - force += fac * d; - } -} - -/** Compute the Reaction Field pair energy. - * @param q1q2 Product of the charges on p1 and p2. - * @param dist Distance between p1 and p2. - */ -inline double rf_coulomb_pair_energy(double const q1q2, double const dist) { - if (dist < rf_params.r_cut) { - auto fac = 1.0 / dist - (rf_params.B * dist * dist) / - (2 * Utils::int_pow<3>(rf_params.r_cut)); - // remove discontinuity at dist = r_cut - fac -= (1 - rf_params.B / 2) / rf_params.r_cut; - fac *= q1q2; - return fac; - } - return 0.0; -} - -#endif - -#endif diff --git a/src/core/electrostatics_magnetostatics/scafacos.cpp b/src/core/electrostatics_magnetostatics/scafacos.cpp deleted file mode 100644 index d8f1fd7aa5a..00000000000 --- a/src/core/electrostatics_magnetostatics/scafacos.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "config.hpp" - -#if defined(SCAFACOS) - -#include "electrostatics_magnetostatics/scafacos.hpp" - -#include "Scafacos.hpp" -#include "communication.hpp" -#include "electrostatics_magnetostatics/ScafacosContext.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" -#include "errorhandling.hpp" -#include "event.hpp" -#include "grid.hpp" - -#include -#include - -#include -#include -#include - -namespace Scafacos { - -/** Get available ScaFaCoS methods */ -std::list available_methods() { - return Scafacos::available_methods(); -} - -namespace { -#ifdef SCAFACOS_DIPOLES -ScafacosContextDipoles *dipoles_instance = nullptr; -#endif -ScafacosContextCoulomb *coulomb_instance = nullptr; -} // namespace - -#ifdef SCAFACOS_DIPOLES -ScafacosContextBase *fcs_dipoles() { - if (!dipoles_instance) { - throw std::runtime_error( - "Attempted access to uninitialized Scafacos Dipoles instance."); - } - return dipoles_instance; -} -#endif - -ScafacosContextBase *fcs_coulomb() { - if (!coulomb_instance) { - throw std::runtime_error( - "Attempted access to uninitialized Scafacos Coulomb instance."); - } - return coulomb_instance; -} - -#ifdef SCAFACOS_DIPOLES -static void set_parameters_dipoles_local(const std::string &method, - const std::string ¶ms) { - delete dipoles_instance; - dipoles_instance = nullptr; - - auto *instance = new ScafacosContextDipoles(method, comm_cart, params); - if (!instance) { - runtimeErrorMsg() << "Scafacos Dipoles failed to initialize"; - return; - } - dipoles_instance = instance; - - instance->set_dipolar(true); - instance->update_system_params(); - - dipole.method = DIPOLAR_SCAFACOS; - on_coulomb_change(); -} - -REGISTER_CALLBACK(set_parameters_dipoles_local) -#endif - -static void set_parameters_coulomb_local(const std::string &method, - const std::string ¶ms) { - delete coulomb_instance; - coulomb_instance = nullptr; - - auto *instance = new ScafacosContextCoulomb(method, comm_cart, params); - if (!instance) { - runtimeErrorMsg() << "Scafacos Coulomb failed to initialize"; - return; - } - coulomb_instance = instance; - - instance->set_dipolar(false); - instance->update_system_params(); - - coulomb.method = COULOMB_SCAFACOS; - on_coulomb_change(); - instance->tune(); -} - -REGISTER_CALLBACK(set_parameters_coulomb_local) - -void set_r_cut_and_tune(double r_cut) { - coulomb_instance->set_r_cut_and_tune(r_cut); -} - -void free_handle(bool dipolar) { - if (this_node == 0) - mpi_call(free_handle, dipolar); - if (dipolar) { -#ifdef SCAFACOS_DIPOLES - delete dipoles_instance; - dipoles_instance = nullptr; -#endif - } else { - delete coulomb_instance; - coulomb_instance = nullptr; - } -} - -REGISTER_CALLBACK(free_handle) - -void set_parameters(const std::string &method, const std::string ¶ms, - bool dipolar) { - if (dipolar) { -#ifdef SCAFACOS_DIPOLES - mpi_call_all(set_parameters_dipoles_local, method, params); -#endif - } else { - mpi_call_all(set_parameters_coulomb_local, method, params); - } -} - -std::string get_method_and_parameters(bool dipolar) { - if (dipolar) { -#ifdef SCAFACOS_DIPOLES - return fcs_dipoles()->get_method_and_parameters(); -#else - return std::string(); -#endif - } - return fcs_coulomb()->get_method_and_parameters(); -} - -} // namespace Scafacos -#endif /* SCAFACOS */ diff --git a/src/core/electrostatics_magnetostatics/scafacos.hpp b/src/core/electrostatics_magnetostatics/scafacos.hpp deleted file mode 100644 index a4fe21a3e16..00000000000 --- a/src/core/electrostatics_magnetostatics/scafacos.hpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/** \file - * This file contains the c-type wrapper interface to the (oop-) scafacos - * interface. - */ - -#ifndef ES_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOS_HPP -#define ES_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOS_HPP - -#include "config.hpp" - -#if defined(SCAFACOS) - -#include "electrostatics_magnetostatics/ScafacosContextBase.hpp" - -#include - -#include -#include - -namespace Scafacos { - -/** @brief Access the per-MPI-node ScaFaCoS Coulomb instance */ -ScafacosContextBase *fcs_coulomb(); -#ifdef SCAFACOS_DIPOLES -/** @brief Access the per-MPI-node ScaFaCoS dipoles instance */ -ScafacosContextBase *fcs_dipoles(); -#endif - -std::string get_method_and_parameters(bool dipolar); -void set_parameters(const std::string &method, const std::string ¶ms, - bool dipolar); -void free_handle(bool dipolar); - -void set_r_cut_and_tune(double r_cut); - -std::list available_methods(); - -} // namespace Scafacos -#endif // SCAFACOS -#endif diff --git a/src/core/energy.cpp b/src/core/energy.cpp index e5710b90491..daa720cb737 100644 --- a/src/core/energy.cpp +++ b/src/core/energy.cpp @@ -27,7 +27,6 @@ #include "communication.hpp" #include "constraints.hpp" #include "cuda_interface.hpp" -#include "electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp" #include "energy_inline.hpp" #include "event.hpp" #include "forces.hpp" @@ -37,19 +36,20 @@ #include "short_range_loop.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" +#include "electrostatics/coulomb.hpp" +#include "magnetostatics/dipoles.hpp" -#include +#include -ActorList energyActors; +#include static std::shared_ptr calculate_energy_local() { auto obs_energy_ptr = std::make_shared(1); - if (long_range_interactions_sanity_checks()) + if (long_range_interactions_sanity_checks()) { return obs_energy_ptr; + } auto &obs_energy = *obs_energy_ptr; @@ -60,10 +60,6 @@ static std::shared_ptr calculate_energy_local() { auto &espresso_system = EspressoSystemInterface::Instance(); espresso_system.update(); - // Compute the energies from the energyActors - for (auto &energyActor : energyActors) - energyActor->computeEnergy(espresso_system); - on_observable_calc(); auto const local_parts = cell_structure.local_particles(); @@ -72,19 +68,26 @@ static std::shared_ptr calculate_energy_local() { obs_energy.kinetic[0] += calc_kinetic_energy(p); } + auto const coulomb_kernel = Coulomb::pair_energy_kernel(); + auto const dipoles_kernel = Dipoles::pair_energy_kernel(); + short_range_loop( - [&obs_energy](Particle const &p1, int bond_id, - Utils::Span partners) { + [&obs_energy, coulomb_kernel_ptr = coulomb_kernel.get_ptr()]( + Particle const &p1, int bond_id, Utils::Span partners) { auto const &iaparams = *bonded_ia_params.at(bond_id); - auto const result = calc_bonded_energy(iaparams, p1, partners); + auto const result = + calc_bonded_energy(iaparams, p1, partners, coulomb_kernel_ptr); if (result) { obs_energy.bonded_contribution(bond_id)[0] += result.get(); return false; } return true; }, - [&obs_energy](Particle const &p1, Particle const &p2, Distance const &d) { + [&obs_energy, coulomb_kernel_ptr = coulomb_kernel.get_ptr(), + dipoles_kernel_ptr = dipoles_kernel.get_ptr()]( + Particle const &p1, Particle const &p2, Distance const &d) { add_non_bonded_pair_energy(p1, p2, d.vec21, sqrt(d.dist2), d.dist2, + coulomb_kernel_ptr, dipoles_kernel_ptr, obs_energy); }, maximal_cutoff(n_nodes), maximal_cutoff_bonded()); @@ -96,7 +99,7 @@ static std::shared_ptr calculate_energy_local() { #ifdef DIPOLES /* calculate k-space part of magnetostatic interaction. */ - obs_energy.dipolar[1] = Dipole::calc_energy_long_range(local_parts); + obs_energy.dipolar[1] = Dipoles::calc_energy_long_range(local_parts); #endif Constraints::constraints.add_energy(local_parts, get_sim_time(), obs_energy); @@ -136,18 +139,19 @@ double particle_short_range_energy_contribution_local(int pid) { cells_update_ghosts(global_ghost_flags()); } - auto const p = cell_structure.get_local_particle(pid); - - if (p) { - auto kernel = [&ret](Particle const &p, Particle const &p1, - Utils::Vector3d const &vec) { + if (auto const p = cell_structure.get_local_particle(pid)) { + auto const coulomb_kernel = Coulomb::pair_energy_kernel(); + auto kernel = [&ret, coulomb_kernel_ptr = coulomb_kernel.get_ptr()]( + Particle const &p, Particle const &p1, + Utils::Vector3d const &vec) { #ifdef EXCLUSIONS if (not do_nonbonded(p, p1)) return; #endif auto const &ia_params = *get_ia_param(p.type(), p1.type()); // Add energy for current particle pair to result - ret += calc_non_bonded_pair_energy(p, p1, ia_params, vec, vec.norm()); + ret += calc_non_bonded_pair_energy(p, p1, ia_params, vec, vec.norm(), + coulomb_kernel_ptr); }; cell_structure.run_on_particle_short_range_neighbors(*p, kernel); } diff --git a/src/core/energy.hpp b/src/core/energy.hpp index 2a4b5fd1eb3..4e719361109 100644 --- a/src/core/energy.hpp +++ b/src/core/energy.hpp @@ -27,12 +27,9 @@ */ #include "Observable_stat.hpp" -#include "actor/ActorList.hpp" #include -extern ActorList energyActors; - /** Parallel energy calculation. */ std::shared_ptr calculate_energy(); diff --git a/src/core/energy_inline.hpp b/src/core/energy_inline.hpp index 6ec95207668..095b03067cf 100644 --- a/src/core/energy_inline.hpp +++ b/src/core/energy_inline.hpp @@ -29,6 +29,8 @@ #include "energy.hpp" #include "bonded_interactions/bonded_interaction_data.hpp" +#include "electrostatics/coulomb_inline.hpp" +#include "magnetostatics/dipoles_inline.hpp" #include "nonbonded_interactions/bmhtf-nacl.hpp" #include "nonbonded_interactions/buckingham.hpp" #include "nonbonded_interactions/gaussian.hpp" @@ -47,14 +49,6 @@ #include "nonbonded_interactions/thole.hpp" #include "nonbonded_interactions/wca.hpp" -#ifdef ELECTROSTATICS -#include "electrostatics_magnetostatics/coulomb_inline.hpp" -#endif - -#ifdef DIPOLES -#include "electrostatics_magnetostatics/dipole_inline.hpp" -#endif - #include "Observable_stat.hpp" #include "Particle.hpp" #include "bond_error.hpp" @@ -74,13 +68,13 @@ * @param ia_params the interaction parameters between the two particles * @param d vector between p1 and p2. * @param dist distance between p1 and p2. + * @param coulomb_kernel %Coulomb energy kernel. * @return the short-range interaction energy between the two particles */ -inline double calc_non_bonded_pair_energy(Particle const &p1, - Particle const &p2, - IA_parameters const &ia_params, - Utils::Vector3d const &d, - double const dist) { +inline double calc_non_bonded_pair_energy( + Particle const &p1, Particle const &p2, IA_parameters const &ia_params, + Utils::Vector3d const &d, double const dist, + Coulomb::ShortRangeEnergyKernel::kernel_type const *coulomb_kernel) { double ret = 0; @@ -145,7 +139,7 @@ inline double calc_non_bonded_pair_energy(Particle const &p1, #ifdef THOLE /* Thole damping */ - ret += thole_pair_energy(p1, p2, ia_params, d, dist); + ret += thole_pair_energy(p1, p2, ia_params, d, dist, coulomb_kernel); #endif #ifdef TABULATED @@ -174,12 +168,16 @@ inline double calc_non_bonded_pair_energy(Particle const &p1, * @param d vector between p1 and p2. * @param dist distance between p1 and p2. * @param dist2 distance squared between p1 and p2. + * @param[in] coulomb_kernel %Coulomb energy kernel. + * @param[in] dipoles_kernel Dipolar energy kernel. * @param[in,out] obs_energy energy observable. */ -inline void add_non_bonded_pair_energy(Particle const &p1, Particle const &p2, - Utils::Vector3d const &d, - double const dist, double const dist2, - Observable_stat &obs_energy) { +inline void add_non_bonded_pair_energy( + Particle const &p1, Particle const &p2, Utils::Vector3d const &d, + double const dist, double const dist2, + Coulomb::ShortRangeEnergyKernel::kernel_type const *coulomb_kernel, + Dipoles::ShortRangeEnergyKernel::kernel_type const *dipoles_kernel, + Observable_stat &obs_energy) { IA_parameters const &ia_params = *get_ia_param(p1.type(), p2.type()); #ifdef EXCLUSIONS @@ -187,23 +185,26 @@ inline void add_non_bonded_pair_energy(Particle const &p1, Particle const &p2, #endif obs_energy.add_non_bonded_contribution( p1.type(), p2.type(), - calc_non_bonded_pair_energy(p1, p2, ia_params, d, dist)); + calc_non_bonded_pair_energy(p1, p2, ia_params, d, dist, + coulomb_kernel)); #ifdef ELECTROSTATICS - if (!obs_energy.coulomb.empty()) - obs_energy.coulomb[0] += - Coulomb::pair_energy(p1, p2, p1.q() * p2.q(), d, dist); + if (!obs_energy.coulomb.empty() and coulomb_kernel != nullptr) { + auto const q1q2 = p1.q() * p2.q(); + obs_energy.coulomb[0] += (*coulomb_kernel)(p1, p2, q1q2, d, dist); + } #endif #ifdef DIPOLES - if (!obs_energy.dipolar.empty()) - obs_energy.dipolar[0] += Dipole::pair_energy(p1, p2, d, dist, dist2); + if (!obs_energy.dipolar.empty() and dipoles_kernel != nullptr) + obs_energy.dipolar[0] += (*dipoles_kernel)(p1, p2, d, dist, dist2); #endif } inline boost::optional calc_bonded_energy(Bonded_IA_Parameters const &iaparams, Particle const &p1, - Utils::Span partners) { + Utils::Span partners, + Coulomb::ShortRangeEnergyKernel::kernel_type const *kernel) { auto const n_partners = static_cast(partners.size()); auto p2 = (n_partners > 0) ? partners[0] : nullptr; @@ -226,7 +227,7 @@ calc_bonded_energy(Bonded_IA_Parameters const &iaparams, Particle const &p1, return iap->energy(p1.q() * p2->q(), dx); } if (auto const *iap = boost::get(&iaparams)) { - return iap->energy(p1, *p2, dx); + return iap->energy(p1, *p2, dx, *kernel); } #endif #ifdef BOND_CONSTRAINT diff --git a/src/core/errorhandling.cpp b/src/core/errorhandling.cpp index 2373237c958..098a4b6be5c 100644 --- a/src/core/errorhandling.cpp +++ b/src/core/errorhandling.cpp @@ -72,6 +72,14 @@ std::vector mpi_gather_runtime_errors() { m_callbacks->call(mpi_gather_runtime_errors_local); return runtimeErrorCollector->gather(); } + +std::vector mpi_gather_runtime_errors_all(bool is_head_node) { + if (is_head_node) { + return runtimeErrorCollector->gather(); + } + runtimeErrorCollector->gather_local(); + return {}; +} } // namespace ErrorHandling void errexit() { diff --git a/src/core/errorhandling.hpp b/src/core/errorhandling.hpp index e73d9d1d336..370c1ac8ab1 100644 --- a/src/core/errorhandling.hpp +++ b/src/core/errorhandling.hpp @@ -102,7 +102,10 @@ RuntimeErrorStream _runtimeMessageStream(RuntimeError::ErrorLevel level, ErrorHandling::RuntimeError::ErrorLevel::WARNING, __FILE__, __LINE__, \ PRETTY_FUNCTION_EXTENSION) +/** @brief Gather messages on main rank. Only call from main rank. */ std::vector mpi_gather_runtime_errors(); +/** @brief Gather messages on main rank. Call on all ranks. */ +std::vector mpi_gather_runtime_errors_all(bool is_head_node); } // namespace ErrorHandling diff --git a/src/core/event.cpp b/src/core/event.cpp index 772c58017ef..063c78f845c 100644 --- a/src/core/event.cpp +++ b/src/core/event.cpp @@ -34,8 +34,8 @@ #include "cuda_init.hpp" #include "cuda_interface.hpp" #include "cuda_utils.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" +#include "electrostatics/coulomb.hpp" +#include "electrostatics/icc.hpp" #include "errorhandling.hpp" #include "grid.hpp" #include "grid_based_algorithms/electrokinetics.hpp" @@ -44,6 +44,7 @@ #include "immersed_boundaries.hpp" #include "integrate.hpp" #include "interactions.hpp" +#include "magnetostatics/dipoles.hpp" #include "nonbonded_interactions/nonbonded_interaction_data.hpp" #include "npt.hpp" #include "partCfg_global.hpp" @@ -51,18 +52,20 @@ #include "thermostat.hpp" #include "virtual_sites.hpp" -#ifdef SCAFACOS -#include "electrostatics_magnetostatics/scafacos.hpp" -#endif - #include #include /** whether the thermostat has to be reinitialized before integration */ static bool reinit_thermo = true; +#ifdef ELECTROSTATICS +/** whether electrostatics actor has to be reinitialized on observable calc */ static bool reinit_electrostatics = false; +#endif +#ifdef DIPOLES +/** whether magnetostatics actor has to be reinitialized on observable calc */ static bool reinit_magnetostatics = false; +#endif #if defined(OPEN_MPI) && \ (OMPI_MAJOR_VERSION == 2 && OMPI_MINOR_VERSION <= 1 || \ @@ -108,9 +111,7 @@ void on_integration_start(double time_step) { #ifdef NPT integrator_npt_sanity_checks(); #endif - if (long_range_interactions_sanity_checks()) { - runtimeErrorMsg() << "Long-range interactions returned an error."; - } + long_range_interactions_sanity_checks(); lb_lbfluid_sanity_checks(time_step); /********************************************/ @@ -144,12 +145,20 @@ void on_integration_start(double time_step) { } #ifndef OPENMPI_BUG_MPI_ALLOC_MEM #ifdef ELECTROSTATICS - if (!Utils::Mpi::all_compare(comm_cart, coulomb.method)) - runtimeErrorMsg() << "Nodes disagree about Coulomb long range method"; + { + auto const &actor = electrostatics_actor; + if (!Utils::Mpi::all_compare(comm_cart, static_cast(actor)) or + (actor and !Utils::Mpi::all_compare(comm_cart, (*actor).which()))) + runtimeErrorMsg() << "Nodes disagree about Coulomb long-range method"; + } #endif #ifdef DIPOLES - if (!Utils::Mpi::all_compare(comm_cart, dipole.method)) - runtimeErrorMsg() << "Nodes disagree about dipolar long range method"; + { + auto const &actor = magnetostatics_actor; + if (!Utils::Mpi::all_compare(comm_cart, static_cast(actor)) or + (actor and !Utils::Mpi::all_compare(comm_cart, (*actor).which()))) + runtimeErrorMsg() << "Nodes disagree about dipolar long-range method"; + } #endif #endif #endif /* ADDITIONAL_CHECKS */ @@ -171,7 +180,7 @@ void on_observable_calc() { #ifdef DIPOLES if (reinit_magnetostatics) { - Dipole::on_observable_calc(); + Dipoles::on_observable_calc(); reinit_magnetostatics = false; } #endif /* DIPOLES */ @@ -186,7 +195,9 @@ void on_observable_calc() { } void on_particle_charge_change() { +#ifdef ELECTROSTATICS reinit_electrostatics = true; +#endif /* the particle information is no longer valid */ partCfg().invalidate(); @@ -199,8 +210,12 @@ void on_particle_change() { } else { cell_structure.set_resort_particles(Cells::RESORT_LOCAL); } +#ifdef ELECTROSTATICS reinit_electrostatics = true; +#endif +#ifdef DIPOLES reinit_magnetostatics = true; +#endif recalc_forces = true; /* the particle information is no longer valid */ @@ -210,27 +225,36 @@ void on_particle_change() { invalidate_fetch_cache(); } -void on_coulomb_change() { - +void on_coulomb_and_dipoles_change() { #ifdef ELECTROSTATICS + reinit_electrostatics = true; Coulomb::on_coulomb_change(); -#endif /* ELECTROSTATICS */ - +#endif #ifdef DIPOLES - Dipole::on_coulomb_change(); -#endif /* ifdef DIPOLES */ + reinit_magnetostatics = true; + Dipoles::on_dipoles_change(); +#endif + on_short_range_ia_change(); +} - /* all Coulomb methods have a short range part, aka near field - correction. Even in case of switching off, we should call this, - since the required cutoff might have reduced. */ +void on_coulomb_change() { +#ifdef ELECTROSTATICS + reinit_electrostatics = true; + Coulomb::on_coulomb_change(); +#endif on_short_range_ia_change(); +} - recalc_forces = true; +void on_dipoles_change() { +#ifdef DIPOLES + reinit_magnetostatics = true; + Dipoles::on_dipoles_change(); +#endif + on_short_range_ia_change(); } void on_short_range_ia_change() { cells_re_init(cell_structure.decomposition_type()); - recalc_forces = true; } @@ -257,7 +281,7 @@ void on_boxl_change(bool skip_method_adaption) { #endif #ifdef DIPOLES - Dipole::on_boxl_change(); + Dipoles::on_boxl_change(); #endif lb_lbfluid_init(); @@ -274,29 +298,25 @@ void on_cell_structure_change() { * Most ES methods need to reinitialize, as they depend on skin, * node grid and so on. */ #ifdef ELECTROSTATICS - Coulomb::init(); -#endif /* ifdef ELECTROSTATICS */ + Coulomb::on_cell_structure_change(); +#endif #ifdef DIPOLES - Dipole::init(); -#endif /* ifdef DIPOLES */ + Dipoles::on_cell_structure_change(); +#endif } void on_temperature_change() { lb_lbfluid_reinit_parameters(); } void on_periodicity_change() { -#ifdef SCAFACOS #ifdef ELECTROSTATICS - if (coulomb.method == COULOMB_SCAFACOS) { - Scafacos::fcs_coulomb()->update_system_params(); - } -#endif -#ifdef SCAFACOS_DIPOLES - if (dipole.method == DIPOLAR_SCAFACOS) { - Scafacos::fcs_dipoles()->update_system_params(); - } + Coulomb::on_periodicity_change(); #endif + +#ifdef DIPOLES + Dipoles::on_periodicity_change(); #endif + #ifdef STOKESIAN_DYNAMICS if (integ_switch == INTEG_METHOD_SD) { if (box_geo.periodic(0) || box_geo.periodic(1) || box_geo.periodic(2)) @@ -311,7 +331,7 @@ void on_periodicity_change() { void on_skin_change() { cells_re_init(cell_structure.decomposition_type()); - on_coulomb_change(); + on_coulomb_and_dipoles_change(); } void on_thermostat_param_change() { reinit_thermo = true; } @@ -323,8 +343,14 @@ void on_timestep_change() { void on_forcecap_change() { recalc_forces = true; } -void on_nodegrid_change() { +void on_node_grid_change() { grid_changed_n_nodes(); +#ifdef ELECTROSTATICS + Coulomb::on_node_grid_change(); +#endif +#ifdef DIPOLES + Dipoles::on_node_grid_change(); +#endif cells_re_init(cell_structure.decomposition_type()); } @@ -364,7 +390,7 @@ void update_dependent_particles() { #endif #ifdef ELECTROSTATICS - Coulomb::update_dependent_particles(); + update_icc_particles(); #endif // Here we initialize volume conservation diff --git a/src/core/event.hpp b/src/core/event.hpp index 1cf2fae0b0c..926879851ce 100644 --- a/src/core/event.hpp +++ b/src/core/event.hpp @@ -66,8 +66,15 @@ void on_particle_change(); /** called every time the charge of a particle has changed. */ void on_particle_charge_change(); -/** called every time the Coulomb parameters are changed. */ +/** called every time the Coulomb parameters are changed. + +all Coulomb methods have a short range part, aka near field + correction. Even in case of switching off, we should call this, + since the required cutoff might have reduced. + */ void on_coulomb_change(); +/** called every time the dipolar parameters are changed. */ +void on_dipoles_change(); /** called every time short ranged interaction parameters are changed. */ void on_short_range_ia_change(); @@ -116,7 +123,7 @@ void on_forcecap_change(); /** @brief Called when the node_grid changed. */ -void on_nodegrid_change(); +void on_node_grid_change(); unsigned global_ghost_flags(); diff --git a/src/core/forces.cpp b/src/core/forces.cpp index 7df3be54002..788e0776a67 100644 --- a/src/core/forces.cpp +++ b/src/core/forces.cpp @@ -32,9 +32,8 @@ #include "comfixed_global.hpp" #include "communication.hpp" #include "constraints.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" -#include "electrostatics_magnetostatics/icc.hpp" -#include "electrostatics_magnetostatics/p3m_gpu.hpp" +#include "electrostatics/icc.hpp" +#include "electrostatics/p3m_gpu.hpp" #include "forcecap.hpp" #include "forces_inline.hpp" #include "grid_based_algorithms/electrokinetics.hpp" @@ -43,6 +42,7 @@ #include "immersed_boundaries.hpp" #include "integrate.hpp" #include "interactions.hpp" +#include "magnetostatics/dipoles.hpp" #include "nonbonded_interactions/VerletCriterion.hpp" #include "nonbonded_interactions/nonbonded_interaction_data.hpp" #include "npt.hpp" @@ -52,12 +52,12 @@ #include "thermostats/langevin_inline.hpp" #include "virtual_sites.hpp" +#include + #include #include -ActorList forceActors; - /** Initialize the forces for a ghost particle */ inline ParticleForce init_ghost_force(Particle const &) { return {}; } @@ -153,19 +153,21 @@ void force_calc(CellStructure &cell_structure, double time_step, double kT) { auto particles = cell_structure.local_particles(); auto ghost_particles = cell_structure.ghost_particles(); #ifdef ELECTROSTATICS - icc_iteration(cell_structure, particles, ghost_particles); + if (electrostatics_extension) { + if (auto icc = boost::get>( + electrostatics_extension.get_ptr())) { + (**icc).iteration(cell_structure, particles, ghost_particles); + } + } #endif init_forces(particles, ghost_particles, time_step, kT); - for (auto &forceActor : forceActors) { - forceActor->computeForces(espresso_system); -#ifdef ROTATION - forceActor->computeTorques(espresso_system); -#endif - } - calc_long_range_forces(particles); + auto const elc_kernel = Coulomb::pair_force_elc_kernel(); + auto const coulomb_kernel = Coulomb::pair_force_kernel(); + auto const dipoles_kernel = Dipoles::pair_force_kernel(); + #ifdef ELECTROSTATICS auto const coulomb_cutoff = Coulomb::cutoff(box_geo.length()); #else @@ -173,15 +175,23 @@ void force_calc(CellStructure &cell_structure, double time_step, double kT) { #endif #ifdef DIPOLES - auto const dipole_cutoff = Dipole::cutoff(box_geo.length()); + auto const dipole_cutoff = Dipoles::cutoff(box_geo.length()); #else auto const dipole_cutoff = INACTIVE_CUTOFF; #endif short_range_loop( - add_bonded_force, - [](Particle &p1, Particle &p2, Distance const &d) { - add_non_bonded_pair_force(p1, p2, d.vec21, sqrt(d.dist2), d.dist2); + [coulomb_kernel_ptr = coulomb_kernel.get_ptr()]( + Particle &p1, int bond_id, Utils::Span partners) { + return add_bonded_force(p1, bond_id, partners, coulomb_kernel_ptr); + }, + [coulomb_kernel_ptr = coulomb_kernel.get_ptr(), + dipoles_kernel_ptr = dipoles_kernel.get_ptr(), + elc_kernel_ptr = elc_kernel.get_ptr()](Particle &p1, Particle &p2, + Distance const &d) { + add_non_bonded_pair_force(p1, p2, d.vec21, sqrt(d.dist2), d.dist2, + coulomb_kernel_ptr, dipoles_kernel_ptr, + elc_kernel_ptr); #ifdef COLLISION_DETECTION if (collision_params.mode != CollisionModeType::OFF) detect_collision(p1, p2, d.dist2); @@ -242,7 +252,7 @@ void calc_long_range_forces(const ParticleRange &particles) { #ifdef DIPOLES /* calculate k-space part of the magnetostatic interaction. */ - Dipole::calc_long_range_force(particles); + Dipoles::calc_long_range_force(particles); #endif // DIPOLES } diff --git a/src/core/forces.hpp b/src/core/forces.hpp index 7e5018ac50b..36574ed1dc4 100644 --- a/src/core/forces.hpp +++ b/src/core/forces.hpp @@ -31,13 +31,10 @@ */ #include "ParticleRange.hpp" -#include "actor/ActorList.hpp" #include "cell_system/CellStructure.hpp" #include -extern ActorList forceActors; - /** initialize real particle forces with thermostat forces and ghost particle forces with zero. */ void init_forces(const ParticleRange &particles, double time_step); diff --git a/src/core/forces_inline.hpp b/src/core/forces_inline.hpp index 0a0f02123a5..49ef00d9a2c 100644 --- a/src/core/forces_inline.hpp +++ b/src/core/forces_inline.hpp @@ -28,11 +28,14 @@ #include "forces.hpp" +#include "actor/visitors.hpp" #include "bond_breakage/bond_breakage.hpp" #include "bonded_interactions/bonded_interaction_data.hpp" #include "bonded_interactions/thermalized_bond_kernel.hpp" +#include "electrostatics/coulomb_inline.hpp" #include "immersed_boundary/ibm_tribend.hpp" #include "immersed_boundary/ibm_triel.hpp" +#include "magnetostatics/dipoles_inline.hpp" #include "nonbonded_interactions/bmhtf-nacl.hpp" #include "nonbonded_interactions/buckingham.hpp" #include "nonbonded_interactions/gaussian.hpp" @@ -53,14 +56,6 @@ #include "object-in-fluid/oif_global_forces.hpp" #include "object-in-fluid/oif_local_forces.hpp" -#ifdef DIPOLES -#include "electrostatics_magnetostatics/dipole_inline.hpp" -#endif - -#ifdef ELECTROSTATICS -#include "electrostatics_magnetostatics/coulomb_inline.hpp" -#endif - #ifdef DPD #include "dpd.hpp" #endif @@ -79,11 +74,10 @@ #include -inline ParticleForce calc_non_bonded_pair_force(Particle const &p1, - Particle const &p2, - IA_parameters const &ia_params, - Utils::Vector3d const &d, - double const dist) { +inline ParticleForce calc_non_bonded_pair_force( + Particle const &p1, Particle const &p2, IA_parameters const &ia_params, + Utils::Vector3d const &d, double const dist, + Coulomb::ShortRangeForceKernel::kernel_type const *coulomb_kernel) { ParticleForce pf{}; double force_factor = 0; @@ -141,7 +135,7 @@ inline ParticleForce calc_non_bonded_pair_force(Particle const &p1, #endif /* Thole damping */ #ifdef THOLE - pf.f += thole_pair_force(p1, p2, ia_params, d, dist); + pf.f += thole_pair_force(p1, p2, ia_params, d, dist, coulomb_kernel); #endif /* tabulated */ #ifdef TABULATED @@ -175,15 +169,21 @@ inline ParticleForce calc_opposing_force(ParticleForce const &pf, /** Calculate non-bonded forces between a pair of particles and update their * forces and torques. - * @param[in,out] p1 particle 1. - * @param[in,out] p2 particle 2. - * @param[in] d vector between @p p1 and @p p2. - * @param dist distance between @p p1 and @p p2. - * @param dist2 distance squared between @p p1 and @p p2. + * @param[in,out] p1 particle 1. + * @param[in,out] p2 particle 2. + * @param[in] d vector between @p p1 and @p p2. + * @param[in] dist distance between @p p1 and @p p2. + * @param[in] dist2 distance squared between @p p1 and @p p2. + * @param[in] coulomb_kernel %Coulomb force kernel. + * @param[in] dipoles_kernel Dipolar force kernel. + * @param[in] elc_kernel ELC force correction kernel. */ -inline void add_non_bonded_pair_force(Particle &p1, Particle &p2, - Utils::Vector3d const &d, double dist, - double dist2) { +inline void add_non_bonded_pair_force( + Particle &p1, Particle &p2, Utils::Vector3d const &d, double dist, + double dist2, + Coulomb::ShortRangeForceKernel::kernel_type const *coulomb_kernel, + Dipoles::ShortRangeForceKernel::kernel_type const *dipoles_kernel, + Coulomb::ShortRangeForceCorrectionsKernel::kernel_type const *elc_kernel) { IA_parameters const &ia_params = *get_ia_param(p1.type(), p2.type()); ParticleForce pf{}; @@ -195,7 +195,8 @@ inline void add_non_bonded_pair_force(Particle &p1, Particle &p2, #ifdef EXCLUSIONS if (do_nonbonded(p1, p2)) #endif - pf += calc_non_bonded_pair_force(p1, p2, ia_params, d, dist); + pf += calc_non_bonded_pair_force(p1, p2, ia_params, d, dist, + coulomb_kernel); } /***********************************************/ @@ -203,16 +204,16 @@ inline void add_non_bonded_pair_force(Particle &p1, Particle &p2, /***********************************************/ #ifdef ELECTROSTATICS - { - auto const forces = Coulomb::pair_force(p1, p2, d, dist); - pf.f += std::get<0>(forces); + // real-space electrostatic charge-charge interaction + auto const q1q2 = p1.q() * p2.q(); + if (q1q2 != 0. and coulomb_kernel != nullptr) { + pf.f += (*coulomb_kernel)(q1q2, d, dist); #ifdef P3M - // forces from the virtual charges - p1.force() += std::get<1>(forces); - p2.force() += std::get<2>(forces); -#endif + if (elc_kernel) + (*elc_kernel)(p1, p2, q1q2); +#endif // P3M } -#endif +#endif // ELECTROSTATICS /*********************************************************************/ /* everything before this contributes to the virial pressure in NpT, */ @@ -236,12 +237,14 @@ inline void add_non_bonded_pair_force(Particle &p1, Particle &p2, #endif /***********************************************/ - /* long-range magnetostatics */ + /* short-range magnetostatics */ /***********************************************/ #ifdef DIPOLES - /* real space magnetic dipole-dipole */ - pf += Dipole::pair_force(p1, p2, d, dist, dist2); + // real-space magnetic dipole-dipole + if (dipoles_kernel) { + pf += (*dipoles_kernel)(p1, p2, d, dist, dist2); + } #endif /***********************************************/ @@ -258,11 +261,12 @@ inline void add_non_bonded_pair_force(Particle &p1, Particle &p2, * @param[in] p2 Second particle. * @param[in] iaparams Bonded parameters for the interaction. * @param[in] dx Vector between @p p1 and @p p2. + * @param[in] kernel %Coulomb force kernel. */ -inline boost::optional -calc_bond_pair_force(Particle const &p1, Particle const &p2, - Bonded_IA_Parameters const &iaparams, - Utils::Vector3d const &dx) { +inline boost::optional calc_bond_pair_force( + Particle const &p1, Particle const &p2, + Bonded_IA_Parameters const &iaparams, Utils::Vector3d const &dx, + Coulomb::ShortRangeForceKernel::kernel_type const *kernel) { if (auto const *iap = boost::get(&iaparams)) { return iap->force(dx); } @@ -277,7 +281,7 @@ calc_bond_pair_force(Particle const &p1, Particle const &p2, return iap->force(p1.q() * p2.q(), dx); } if (auto const *iap = boost::get(&iaparams)) { - return iap->force(dx); + return iap->force(dx, *kernel); } #endif #ifdef BOND_CONSTRAINT @@ -296,8 +300,9 @@ calc_bond_pair_force(Particle const &p1, Particle const &p2, throw BondUnknownTypeError(); } -inline bool add_bonded_two_body_force(Bonded_IA_Parameters const &iaparams, - Particle &p1, Particle &p2) { +inline bool add_bonded_two_body_force( + Bonded_IA_Parameters const &iaparams, Particle &p1, Particle &p2, + Coulomb::ShortRangeForceKernel::kernel_type const *kernel) { auto const dx = box_geo.get_mi_vector(p1.pos(), p2.pos()); if (auto const *iap = boost::get(&iaparams)) { @@ -310,7 +315,7 @@ inline bool add_bonded_two_body_force(Bonded_IA_Parameters const &iaparams, return false; } } else { - auto result = calc_bond_pair_force(p1, p2, iaparams, dx); + auto result = calc_bond_pair_force(p1, p2, iaparams, dx, kernel); if (result) { p1.force() += result.get(); p2.force() -= result.get(); @@ -410,8 +415,9 @@ inline bool add_bonded_four_body_force(Bonded_IA_Parameters const &iaparams, return true; } -inline bool add_bonded_force(Particle &p1, int bond_id, - Utils::Span partners) { +inline bool +add_bonded_force(Particle &p1, int bond_id, Utils::Span partners, + Coulomb::ShortRangeForceKernel::kernel_type const *kernel) { // Consider for bond breakage if (partners.size() == 1) { @@ -427,7 +433,7 @@ inline bool add_bonded_force(Particle &p1, int bond_id, case 0: return false; case 1: - return add_bonded_two_body_force(iaparams, p1, *partners[0]); + return add_bonded_two_body_force(iaparams, p1, *partners[0], kernel); case 2: return add_bonded_three_body_force(iaparams, p1, *partners[0], *partners[1]); diff --git a/src/core/grid.cpp b/src/core/grid.cpp index 06dda104654..74acd89f7a7 100644 --- a/src/core/grid.cpp +++ b/src/core/grid.cpp @@ -160,11 +160,12 @@ void mpi_set_periodicity(bool x, bool y, bool z) { void mpi_set_node_grid_local(const Utils::Vector3i &node_grid) { ::node_grid = node_grid; - on_nodegrid_change(); + grid_changed_n_nodes(); + on_node_grid_change(); } REGISTER_CALLBACK(mpi_set_node_grid_local) void mpi_set_node_grid(const Utils::Vector3i &node_grid) { mpi_call_all(mpi_set_node_grid_local, node_grid); -} \ No newline at end of file +} diff --git a/src/core/interactions.cpp b/src/core/interactions.cpp index c7a0bd57183..cd428b0f5e6 100644 --- a/src/core/interactions.cpp +++ b/src/core/interactions.cpp @@ -22,14 +22,15 @@ #include "bonded_interactions/bonded_interaction_data.hpp" #include "collision.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" +#include "electrostatics/coulomb.hpp" +#include "errorhandling.hpp" #include "event.hpp" #include "grid.hpp" +#include "magnetostatics/dipoles.hpp" #include "serialization/IA_parameters.hpp" -#include +#include #include @@ -42,7 +43,7 @@ static double recalc_long_range_cutoff() { #ifdef DIPOLES max_cut_long_range = - std::max(max_cut_long_range, Dipole::cutoff(box_geo.length())); + std::max(max_cut_long_range, Dipoles::cutoff(box_geo.length())); #endif return max_cut_long_range; @@ -62,23 +63,22 @@ double maximal_cutoff(bool single_node) { } max_cut = std::max(max_cut, max_cut_nonbonded); max_cut = std::max(max_cut, collision_detection_cutoff()); - return max_cut; } bool long_range_interactions_sanity_checks() { - /* set to zero if initialization was not successful. */ - bool failed = false; - + try { #ifdef ELECTROSTATICS - failed |= Coulomb::sanity_checks(); -#endif /* ifdef ELECTROSTATICS */ - + Coulomb::sanity_checks(); +#endif #ifdef DIPOLES - failed |= Dipole::sanity_checks(); -#endif /* ifdef DIPOLES */ - - return failed; + Dipoles::sanity_checks(); +#endif + } catch (std::runtime_error const &err) { + runtimeErrorMsg() << err.what(); + return true; + } + return false; } void mpi_bcast_ia_params_local(int i, int j) { diff --git a/testsuite/python/mmm1d_gpu.py b/src/core/magnetostatics/CMakeLists.txt similarity index 57% rename from testsuite/python/mmm1d_gpu.py rename to src/core/magnetostatics/CMakeLists.txt index fa0c990c998..e4136a56757 100644 --- a/testsuite/python/mmm1d_gpu.py +++ b/src/core/magnetostatics/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (C) 2013-2019 The ESPResSo project +# Copyright (C) 2018-2022 The ESPResSo project # # This file is part of ESPResSo. # @@ -16,22 +16,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import unittest as ut -import unittest_decorators as utx -import mmm1d -import espressomd.electrostatics - - -@utx.skipIfMissingFeatures(["ELECTROSTATICS", "MMM1D_GPU"]) -@utx.skipIfMissingGPU() -class MMM1D_GPU_Test(mmm1d.ElectrostaticInteractionsTests, ut.TestCase): - - allowed_error = 1e-4 - - def setUp(self): - self.MMM1D = espressomd.electrostatics.MMM1DGPU - super().setUp() - - -if __name__ == "__main__": - ut.main() +target_sources( + EspressoCore + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/dipoles.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/barnes_hut_gpu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/dds.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/dds_gpu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/dds_replica.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/dlc.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/dp3m.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/scafacos_impl.cpp) diff --git a/src/core/magnetostatics/barnes_hut_gpu.cpp b/src/core/magnetostatics/barnes_hut_gpu.cpp new file mode 100644 index 00000000000..43bf8f1544f --- /dev/null +++ b/src/core/magnetostatics/barnes_hut_gpu.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef DIPOLAR_BARNES_HUT + +#include "magnetostatics/barnes_hut_gpu.hpp" +#include "magnetostatics/barnes_hut_gpu_cuda.cuh" + +#include "EspressoSystemInterface.hpp" +#include "communication.hpp" +#include "cuda_interface.hpp" +#include "cuda_utils.hpp" +#include "errorhandling.hpp" + +DipolarBarnesHutGpu::DipolarBarnesHutGpu(double prefactor, double epssq, + double itolsq) + : prefactor{prefactor}, m_epssq{epssq}, m_itolsq{itolsq} { + if (this_node != 0) { + return; + } + auto &system = EspressoSystemInterface::Instance(); + system.requestFGpu(); + system.requestTorqueGpu(); + system.requestRGpu(); + system.requestDipGpu(); + setBHPrecision(static_cast(m_epssq), static_cast(m_itolsq)); +} + +template +int call_kernel(void (*fp)(Args...), ArgRef &&... args) { + int error_code = ES_ERROR; + try { + fp(args...); + error_code = ES_OK; + } catch (std::runtime_error const &err) { + runtimeErrorMsg() << "DipolarBarnesHutGpu: " << err.what(); + } + return error_code; +} + +int DipolarBarnesHutGpu::initialize_data_structure() { + auto &system = EspressoSystemInterface::Instance(); + auto const n_part = static_cast(system.npart_gpu()); + auto const error_code = call_kernel(allocBHmemCopy, n_part, &m_bh_data); + + if (error_code == ES_OK) { + fill_bh_data(system.rGpuBegin(), system.dipGpuBegin(), &m_bh_data); + initBHgpu(m_bh_data.blocks); + buildBoxBH(m_bh_data.blocks); + buildTreeBH(m_bh_data.blocks); + summarizeBH(m_bh_data.blocks); + sortBH(m_bh_data.blocks); + } + + return error_code; +} + +void DipolarBarnesHutGpu::add_long_range_forces() { + auto &system = EspressoSystemInterface::Instance(); + system.update(); + if (this_node == 0) { + if (initialize_data_structure() == ES_OK) { + call_kernel(forceBH, &m_bh_data, static_cast(prefactor), + system.fGpuBegin(), system.torqueGpuBegin()); + } + } +} + +void DipolarBarnesHutGpu::long_range_energy() { + auto &system = EspressoSystemInterface::Instance(); + system.update(); + if (this_node == 0) { + if (initialize_data_structure() == ES_OK) { + auto energy = &(reinterpret_cast(system.eGpu())->dipolar); + call_kernel(energyBH, &m_bh_data, static_cast(prefactor), energy); + } + } +} + +#endif // DIPOLAR_BARNES_HUT diff --git a/src/core/actor/DipolarBarnesHut.hpp b/src/core/magnetostatics/barnes_hut_gpu.hpp similarity index 59% rename from src/core/actor/DipolarBarnesHut.hpp rename to src/core/magnetostatics/barnes_hut_gpu.hpp index 4c380b39d63..767de7bd0ec 100644 --- a/src/core/actor/DipolarBarnesHut.hpp +++ b/src/core/magnetostatics/barnes_hut_gpu.hpp @@ -16,41 +16,42 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef ACTOR_DIPOLARBARNESHUT_HPP -#define ACTOR_DIPOLARBARNESHUT_HPP + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_BARNES_HUT_GPU_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_BARNES_HUT_GPU_HPP #include "config.hpp" #ifdef DIPOLAR_BARNES_HUT -#include "Actor.hpp" -#include "DipolarBarnesHut_cuda.cuh" -#include "SystemInterface.hpp" - -class DipolarBarnesHut : public Actor { -public: - DipolarBarnesHut(SystemInterface &s); - ~DipolarBarnesHut() override { deallocBH(&m_bh_data); } +#include "magnetostatics/barnes_hut_gpu_cuda.cuh" - void set_params(float epssq, float itolsq); +struct DipolarBarnesHutGpu { + double prefactor; + double m_epssq; + double m_itolsq; + DipolarBarnesHutGpu(double prefactor, double epssq, double itolsq); + ~DipolarBarnesHutGpu() { deallocBH(&m_bh_data); } - void computeForces(SystemInterface &s) override; - void computeEnergy(SystemInterface &s) override; + void on_activation() const {} + void on_boxl_change() const {} + void on_node_grid_change() const {} + void on_periodicity_change() const {} + void on_cell_structure_change() const {} + void init() const {} + void sanity_checks() const {} - void activate(); - void deactivate(); + void add_long_range_forces(); + void long_range_energy(); private: - float m_prefactor; - float m_epssq; - float m_itolsq; /// Container for pointers to device memory. BHData m_bh_data = {0, 0, 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; - int initialize_data_structure(SystemInterface &s); + int initialize_data_structure(); }; #endif // DIPOLAR_BARNES_HUT -#endif // ACTOR_DIPOLARBARNESHUT_HPP +#endif diff --git a/src/core/actor/DipolarBarnesHut_cuda.cu b/src/core/magnetostatics/barnes_hut_gpu_cuda.cu similarity index 99% rename from src/core/actor/DipolarBarnesHut_cuda.cu rename to src/core/magnetostatics/barnes_hut_gpu_cuda.cu index 67d083efdc0..42320dc18fa 100644 --- a/src/core/actor/DipolarBarnesHut_cuda.cu +++ b/src/core/magnetostatics/barnes_hut_gpu_cuda.cu @@ -17,15 +17,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + /** @file - * The method concept is revealed within @cite burtscher11a + * The method is based on @cite burtscher11a. */ #include "config.hpp" #ifdef DIPOLAR_BARNES_HUT -#include "DipolarBarnesHut_cuda.cuh" +#include "magnetostatics/barnes_hut_gpu_cuda.cuh" #include "cuda_init.hpp" #include "cuda_utils.cuh" @@ -37,6 +38,7 @@ #include #include +#include // Method performance/accuracy parameters __constant__ float epssqd[1], itolsqd[1]; @@ -1101,8 +1103,7 @@ void sortBH(int blocks) { } // Force calculation. -int forceBH(BHData *bh_data, float k, float *f, float *torque) { - int error_code = 0; +void forceBH(BHData *bh_data, float k, float *f, float *torque) { dim3 grid(1, 1, 1); dim3 block(1, 1, 1); @@ -1112,14 +1113,16 @@ int forceBH(BHData *bh_data, float k, float *f, float *torque) { KERNELCALL(forceCalculationKernel, grid, block, k, f, torque); cuda_safe_mem(cudaDeviceSynchronize()); + int error_code = 0; cuda_safe_mem(cudaMemcpy(&error_code, bh_data->err, sizeof(int), cudaMemcpyDeviceToHost)); - return error_code; + if (error_code) { + throw std::runtime_error("force kernel encountered a functional error"); + } } // Energy calculation. -int energyBH(BHData *bh_data, float k, float *E) { - int error_code = 0; +void energyBH(BHData *bh_data, float k, float *E) { dim3 grid(1, 1, 1); dim3 block(1, 1, 1); @@ -1142,9 +1145,13 @@ int energyBH(BHData *bh_data, float k, float *E) { cuda_safe_mem(cudaMemcpy(E, &x, sizeof(float), cudaMemcpyHostToDevice)); cuda_safe_mem(cudaFree(energySum)); + + int error_code = 0; cuda_safe_mem(cudaMemcpy(&error_code, bh_data->err, sizeof(int), cudaMemcpyDeviceToHost)); - return error_code; + if (error_code) { + throw std::runtime_error("force kernel encountered a functional error"); + } } void setBHPrecision(float epssq, float itolsq) { diff --git a/src/core/actor/DipolarBarnesHut_cuda.cuh b/src/core/magnetostatics/barnes_hut_gpu_cuda.cuh similarity index 94% rename from src/core/actor/DipolarBarnesHut_cuda.cuh rename to src/core/magnetostatics/barnes_hut_gpu_cuda.cuh index 9ad9437042c..4891aaa77b4 100644 --- a/src/core/actor/DipolarBarnesHut_cuda.cuh +++ b/src/core/magnetostatics/barnes_hut_gpu_cuda.cuh @@ -18,8 +18,8 @@ * along with this program. If not, see . */ -#ifndef DIPOLARBARNESHUT_CUH_ -#define DIPOLARBARNESHUT_CUH_ +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_BARNES_HUT_GPU_CUH +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_BARNES_HUT_GPU_CUH #include "config.hpp" @@ -120,10 +120,10 @@ void summarizeBH(int blocks); void sortBH(int blocks); /// Barnes-Hut force calculation. -int forceBH(BHData *bh_data, float k, float *f, float *torque); +void forceBH(BHData *bh_data, float k, float *f, float *torque); /// Barnes-Hut energy calculation. -int energyBH(BHData *bh_data, float k, float *E); +void energyBH(BHData *bh_data, float k, float *E); #endif // DIPOLAR_BARNES_HUT -#endif /* DIPOLARBARNESHUT_CUH_ */ +#endif diff --git a/src/core/magnetostatics/dds.cpp b/src/core/magnetostatics/dds.cpp new file mode 100644 index 00000000000..6125a8e762d --- /dev/null +++ b/src/core/magnetostatics/dds.cpp @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef DIPOLES + +#include "magnetostatics/dds.hpp" + +#include "cells.hpp" +#include "communication.hpp" +#include "errorhandling.hpp" +#include "grid.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +/** + * Calculate dipolar energy and optionally force between two particles. + * @param[in,out] p1 First particle + * @param[in] dip1 Cached dipole moment of the first particle + * @param[in,out] p2 Second particle + * @param[in] force_flag If true, update the particle forces and torques + */ +double DipolarDirectSum::calc_dipole_dipole_ia(Particle &p1, + Utils::Vector3d const &dip1, + Particle &p2, + bool force_flag) const { + + // Cache dipole moment + auto const dip2 = p2.calc_dip(); + + // Distance between particles + auto const dr = box_geo.get_mi_vector(p1.r.p, p2.r.p); + + // Powers of distance + auto const r2 = dr.norm2(); + auto const r = sqrt(r2); + auto const r3 = r2 * r; + auto const r5 = r3 * r2; + auto const r7 = r5 * r2; + + // Dot products + auto const pe1 = dip1 * dip2; + auto const pe2 = dip1 * dr; + auto const pe3 = dip2 * dr; + auto const pe4 = 3.0 / r5; + + // Energy + auto const energy = prefactor * (pe1 / r3 - pe4 * pe2 * pe3); + + // Forces, if requested + if (force_flag) { + auto const a = pe4 * pe1; + auto const b = -15.0 * pe2 * pe3 / r7; + auto const ab = a + b; + auto const cc = pe4 * pe3; + auto const dd = pe4 * pe2; + + // Forces + auto const ff = ab * dr + cc * dip1 + dd * dip2; + p1.force() += prefactor * ff; + p2.force() -= prefactor * ff; + + // Torques + auto const aa = vector_product(dip1, dip2); + auto const b1 = vector_product(dip1, dr); + auto const b2 = vector_product(dip2, dr); + p1.torque() += prefactor * (-aa / r3 + b1 * cc); + p2.torque() += prefactor * (aa / r3 + b2 * dd); + } + + return energy; +} + +double DipolarDirectSum::kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles) const { + + assert(n_nodes == 1); + assert(force_flag || energy_flag); + + double energy = 0.0; + // Iterate over all particles + for (auto it = particles.begin(), end = particles.end(); it != end; ++it) { + // If the particle has no dipole moment, ignore it + if (it->dipm() == 0.0) + continue; + + auto const dip1 = it->calc_dip(); + auto jt = it; + /* Skip diagonal */ + ++jt; + for (; jt != end; ++jt) { + // If the particle has no dipole moment, ignore it + if (jt->dipm() == 0.0) + continue; + // Calculate energy and/or force between the particles + energy += calc_dipole_dipole_ia(*it, dip1, *jt, force_flag); + } + } + + return energy; +} + +DipolarDirectSum::DipolarDirectSum(double prefactor) : prefactor{prefactor} { + if (n_nodes > 1) { + throw std::runtime_error( + "MPI parallelization not supported by DipolarDirectSumCpu."); + } + if (prefactor <= 0.) { + throw std::domain_error("Parameter 'prefactor' must be > 0"); + } +} + +#endif // DIPOLES diff --git a/src/core/magnetostatics/dds.hpp b/src/core/magnetostatics/dds.hpp new file mode 100644 index 00000000000..8f31bf64aec --- /dev/null +++ b/src/core/magnetostatics/dds.hpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "ParticleRange.hpp" + +/** + * @brief Dipolar all with all and no replica. + * Handling of a system of dipoles where no replicas exist. + * Assumes minimum image convention for those axis in which the + * system is periodic. + */ +struct DipolarDirectSum { + double prefactor; + DipolarDirectSum(double prefactor); + + void on_activation() const {} + void on_boxl_change() const {} + void on_node_grid_change() const {} + void on_periodicity_change() const {} + void on_cell_structure_change() const {} + void init() const {} + void sanity_checks() const {} + + double kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles) const; + +private: + double calc_dipole_dipole_ia(Particle &p1, Utils::Vector3d const &dip1, + Particle &p2, bool force_flag) const; +}; + +#endif // DIPOLES +#endif diff --git a/src/core/magnetostatics/dds_gpu.cpp b/src/core/magnetostatics/dds_gpu.cpp new file mode 100644 index 00000000000..42edd0821c7 --- /dev/null +++ b/src/core/magnetostatics/dds_gpu.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef DIPOLAR_DIRECT_SUM + +#include "magnetostatics/dds_gpu.hpp" +#include "magnetostatics/dds_gpu_cuda.cuh" + +#include "EspressoSystemInterface.hpp" +#include "communication.hpp" +#include "cuda_interface.hpp" +#include "grid.hpp" + +static void get_simulation_box(float *box, int *per) { + for (int i = 0; i < 3; i++) { + box[i] = static_cast(box_geo.length()[i]); + per[i] = box_geo.periodic(i); + } +} + +DipolarDirectSumGpu::DipolarDirectSumGpu(double prefactor) + : prefactor{prefactor} { + if (this_node != 0) { + return; + } + auto &system = EspressoSystemInterface::Instance(); + system.requestFGpu(); + system.requestTorqueGpu(); + system.requestRGpu(); + system.requestDipGpu(); +} + +void DipolarDirectSumGpu::add_long_range_forces() const { + auto &system = EspressoSystemInterface::Instance(); + system.update(); + if (this_node != 0) { + return; + } + float box[3]; + int periodicity[3]; + get_simulation_box(box, periodicity); + DipolarDirectSum_kernel_wrapper_force( + static_cast(prefactor), static_cast(system.npart_gpu()), + system.rGpuBegin(), system.dipGpuBegin(), system.fGpuBegin(), + system.torqueGpuBegin(), box, periodicity); +} + +void DipolarDirectSumGpu::long_range_energy() const { + auto &system = EspressoSystemInterface::Instance(); + system.update(); + if (this_node != 0) { + return; + } + float box[3]; + int periodicity[3]; + get_simulation_box(box, periodicity); + auto energy = &(reinterpret_cast(system.eGpu())->dipolar); + DipolarDirectSum_kernel_wrapper_energy( + static_cast(prefactor), static_cast(system.npart_gpu()), + system.rGpuBegin(), system.dipGpuBegin(), box, periodicity, energy); +} + +#endif diff --git a/src/core/actor/DipolarDirectSum.hpp b/src/core/magnetostatics/dds_gpu.hpp similarity index 60% rename from src/core/actor/DipolarDirectSum.hpp rename to src/core/magnetostatics/dds_gpu.hpp index 940cc9082d9..8304471b2b5 100644 --- a/src/core/actor/DipolarDirectSum.hpp +++ b/src/core/magnetostatics/dds_gpu.hpp @@ -16,33 +16,30 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef ACTOR_DIPOLARDIRECTSUM_HPP -#define ACTOR_DIPOLARDIRECTSUM_HPP + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_GPU_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_GPU_HPP #include "config.hpp" #ifdef DIPOLAR_DIRECT_SUM -#include "Actor.hpp" -#include "SystemInterface.hpp" - -class DipolarDirectSum : public Actor { -public: - DipolarDirectSum(SystemInterface &s); - ~DipolarDirectSum() override = default; +struct DipolarDirectSumGpu { + double prefactor; - void set_params(); + DipolarDirectSumGpu(double prefactor); - void computeForces(SystemInterface &s) override; - void computeEnergy(SystemInterface &s) override; + void on_activation() const {} + void on_boxl_change() const {} + void on_node_grid_change() const {} + void on_periodicity_change() const {} + void on_cell_structure_change() const {} + void init() const {} + void sanity_checks() const {} - void activate(); - void deactivate(); - -private: - float m_prefactor; + void add_long_range_forces() const; + void long_range_energy() const; }; #endif // DIPOLAR_DIRECT_SUM - -#endif // ACTOR_DIPOLARDIRECTSUM_HPP +#endif diff --git a/src/core/actor/DipolarDirectSum_cuda.cu b/src/core/magnetostatics/dds_gpu_cuda.cu similarity index 99% rename from src/core/actor/DipolarDirectSum_cuda.cu rename to src/core/magnetostatics/dds_gpu_cuda.cu index 532f551e85a..783733d800e 100644 --- a/src/core/actor/DipolarDirectSum_cuda.cu +++ b/src/core/magnetostatics/dds_gpu_cuda.cu @@ -21,7 +21,7 @@ #ifdef DIPOLAR_DIRECT_SUM -#include "DipolarDirectSum_cuda.cuh" +#include "magnetostatics/dds_gpu_cuda.cuh" #include "cuda_utils.cuh" diff --git a/src/core/actor/DipolarDirectSum_cuda.cuh b/src/core/magnetostatics/dds_gpu_cuda.cuh similarity index 89% rename from src/core/actor/DipolarDirectSum_cuda.cuh rename to src/core/magnetostatics/dds_gpu_cuda.cuh index 31b3171a45b..3d11d23f42f 100644 --- a/src/core/actor/DipolarDirectSum_cuda.cuh +++ b/src/core/magnetostatics/dds_gpu_cuda.cuh @@ -17,8 +17,8 @@ * along with this program. If not, see . */ -#ifndef DIPOLARDIRECTSUM_CUH_ -#define DIPOLARDIRECTSUM_CUH_ +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_GPU_CUH +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_GPU_CUH #include "config.hpp" @@ -32,5 +32,4 @@ void DipolarDirectSum_kernel_wrapper_force(float k, unsigned int n, float *pos, float box_l[3], int periodic[3]); #endif // DIPOLAR_DIRECT_SUM - -#endif /* DIPOLARDIRECTSUM_CUH_ */ +#endif diff --git a/src/core/electrostatics_magnetostatics/magnetic_non_p3m_methods.cpp b/src/core/magnetostatics/dds_replica.cpp similarity index 54% rename from src/core/electrostatics_magnetostatics/magnetic_non_p3m_methods.cpp rename to src/core/magnetostatics/dds_replica.cpp index 619af521368..04835393488 100644 --- a/src/core/electrostatics_magnetostatics/magnetic_non_p3m_methods.cpp +++ b/src/core/magnetostatics/dds_replica.cpp @@ -23,10 +23,7 @@ #ifdef DIPOLES -#include "electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp" - -#include "electrostatics_magnetostatics/common.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" +#include "magnetostatics/dds_replica.hpp" #include "cells.hpp" #include "communication.hpp" @@ -42,106 +39,7 @@ #include #include -/** - * Calculate dipolar energy and optionally force between two particles. - * @param[in,out] p1 First particle - * @param[in] dip1 Cached dipole moment of the first particle - * @param[in,out] p2 Second particle - * @param[in] force_flag If true, update the particle forces and torques - */ -static double calc_dipole_dipole_ia(Particle &p1, Utils::Vector3d const &dip1, - Particle &p2, bool force_flag) { - - // Cache dipole moment - auto const dip2 = p2.calc_dip(); - - // Distance between particles - auto const dr = box_geo.get_mi_vector(p1.r.p, p2.r.p); - - // Powers of distance - auto const r2 = dr.norm2(); - auto const r = sqrt(r2); - auto const r3 = r2 * r; - auto const r5 = r3 * r2; - auto const r7 = r5 * r2; - - // Dot products - auto const pe1 = dip1 * dip2; - auto const pe2 = dip1 * dr; - auto const pe3 = dip2 * dr; - auto const pe4 = 3.0 / r5; - - // Energy - auto const energy = dipole.prefactor * (pe1 / r3 - pe4 * pe2 * pe3); - - // Forces, if requested - if (force_flag) { - auto const a = pe4 * pe1; - auto const b = -15.0 * pe2 * pe3 / r7; - auto const ab = a + b; - auto const cc = pe4 * pe3; - auto const dd = pe4 * pe2; - - // Forces - auto const ff = ab * dr + cc * dip1 + dd * dip2; - p1.f.f += dipole.prefactor * ff; - p2.f.f -= dipole.prefactor * ff; - - // Torques - auto const aa = vector_product(dip1, dip2); - auto const b1 = vector_product(dip1, dr); - auto const b2 = vector_product(dip2, dr); - p1.f.torque += dipole.prefactor * (-aa / r3 + b1 * cc); - p2.f.torque += dipole.prefactor * (aa / r3 + b2 * dd); - } - - return energy; -} - -/* ============================================================================= - DAWAANR => DIPOLAR ALL WITH ALL AND NO REPLICA - ============================================================================= -*/ - -double dawaanr_calculations(bool force_flag, bool energy_flag, - const ParticleRange &particles) { - - assert(n_nodes == 1); - assert(force_flag || energy_flag); - - double energy = 0.0; - // Iterate over all particles - for (auto it = particles.begin(), end = particles.end(); it != end; ++it) { - // If the particle has no dipole moment, ignore it - if (it->p.dipm == 0.0) - continue; - - const Utils::Vector3d dip1 = it->calc_dip(); - auto jt = it; - /* Skip diagonal */ - ++jt; - for (; jt != end; ++jt) { - // If the particle has no dipole moment, ignore it - if (jt->p.dipm == 0.0) - continue; - // Calculate energy and/or force between the particles - energy += calc_dipole_dipole_ia(*it, dip1, *jt, force_flag); - } - } - - return energy; -} - -/* ============================================================================= - DIRECT SUM FOR MAGNETIC SYSTEMS - ============================================================================= -*/ - -static int mdds_n_replica = 0; - -int mdds_get_n_replica() { return mdds_n_replica; } - -void sanity_checks(int n_replica) { +void DipolarDirectSumWithReplica::sanity_checks_node_grid() const { if (box_geo.periodic(0) and box_geo.periodic(1) and box_geo.periodic(2) and n_replica == 0) { throw std::runtime_error("Dipolar direct sum with replica does not " @@ -149,11 +47,9 @@ void sanity_checks(int n_replica) { } } -void mdds_sanity_checks() { sanity_checks(mdds_n_replica); } - double -magnetic_dipolar_direct_sum_calculations(bool force_flag, bool energy_flag, - ParticleRange const &particles) { +DipolarDirectSumWithReplica::kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles) const { assert(n_nodes == 1); assert(force_flag || energy_flag); @@ -184,7 +80,7 @@ magnetic_dipolar_direct_sum_calculations(bool force_flag, bool energy_flag, int dip_particles = 0; for (auto const &p : particles) { - if (p.p.dipm != 0.0) { + if (p.dipm() != 0.0) { const Utils::Vector3d dip = p.calc_dip(); mx[dip_particles] = dip[0]; @@ -215,9 +111,9 @@ magnetic_dipolar_direct_sum_calculations(bool force_flag, bool energy_flag, int NCUT[3]; for (int i = 0; i < 3; i++) { - NCUT[i] = box_geo.periodic(i) ? mdds_n_replica : 0; + NCUT[i] = box_geo.periodic(i) ? n_replica : 0; } - auto const NCUT2 = Utils::sqr(mdds_n_replica); + auto const NCUT2 = Utils::sqr(n_replica); for (int i = 0; i < dip_particles; i++) { for (int j = 0; j < dip_particles; j++) { @@ -286,58 +182,44 @@ magnetic_dipolar_direct_sum_calculations(bool force_flag, bool energy_flag, dip_particles = 0; for (auto &p : particles) { - if (p.p.dipm != 0.0) { + if (p.dipm() != 0.) { + auto &force = p.force(); + auto &torque = p.torque(); - p.f.f[0] += dipole.prefactor * fx[dip_particles]; - p.f.f[1] += dipole.prefactor * fy[dip_particles]; - p.f.f[2] += dipole.prefactor * fz[dip_particles]; + force[0] += prefactor * fx[dip_particles]; + force[1] += prefactor * fy[dip_particles]; + force[2] += prefactor * fz[dip_particles]; - p.f.torque[0] += dipole.prefactor * tx[dip_particles]; - p.f.torque[1] += dipole.prefactor * ty[dip_particles]; - p.f.torque[2] += dipole.prefactor * tz[dip_particles]; + torque[0] += prefactor * tx[dip_particles]; + torque[1] += prefactor * ty[dip_particles]; + torque[2] += prefactor * tz[dip_particles]; dip_particles++; } } } /* if force_flag */ - return 0.5 * dipole.prefactor * energy; + return 0.5 * prefactor * energy; } -void dawaanr_set_params() { - if (n_nodes > 1) { - throw std::runtime_error( - "MPI parallelization not supported by DipolarDirectSumCpu."); - } - if (dipole.method != DIPOLAR_ALL_WITH_ALL_AND_NO_REPLICA) { - Dipole::set_method_local(DIPOLAR_ALL_WITH_ALL_AND_NO_REPLICA); - } - // also necessary on 1 CPU, does more than just broadcasting - mpi_bcast_coulomb_params(); -} - -void mdds_set_params(int n_replica) { +DipolarDirectSumWithReplica::DipolarDirectSumWithReplica(double prefactor, + int n_replica) + : prefactor{prefactor}, n_replica{n_replica} { if (n_nodes > 1) { throw std::runtime_error( "MPI parallelization not supported by DipolarDirectSumWithReplicaCpu."); } + if (prefactor <= 0.) { + throw std::domain_error("Parameter 'prefactor' must be > 0"); + } if (n_replica < 0) { - throw std::runtime_error("Dipolar direct sum requires n_replica >= 0."); + throw std::domain_error("Parameter 'n_replica' must be >= 0"); } - sanity_checks(n_replica); + sanity_checks(); if (n_replica == 0) { fprintf(stderr, "Careful: the number of extra replicas to take into " "account during the direct sum calculation is zero\n"); } - - mdds_n_replica = n_replica; - - if (dipole.method != DIPOLAR_DS && dipole.method != DIPOLAR_MDLC_DS) { - Dipole::set_method_local(DIPOLAR_DS); - } - - // also necessary on 1 CPU, does more than just broadcasting - mpi_bcast_coulomb_params(); } -#endif +#endif // DIPOLES diff --git a/src/core/magnetostatics/dds_replica.hpp b/src/core/magnetostatics/dds_replica.hpp new file mode 100644 index 00000000000..6e43537d007 --- /dev/null +++ b/src/core/magnetostatics/dds_replica.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_REPLICA_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_REPLICA_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "ParticleRange.hpp" + +/** + * @brief Dipolar direct sum with replica. + * Calculate dipole-dipole interaction of a periodic system by explicitly + * summing the dipole-dipole interaction over several copies of the system. + * Uses spherical summation order. + */ +struct DipolarDirectSumWithReplica { + double prefactor; + int n_replica; + DipolarDirectSumWithReplica(double prefactor, int n_replica); + + void on_activation() const { sanity_checks(); } + void on_boxl_change() const {} + void on_node_grid_change() const { sanity_checks_node_grid(); } + void on_periodicity_change() const {} + void on_cell_structure_change() const {} + void init() const {} + + void sanity_checks() const { sanity_checks_node_grid(); } + + double kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles) const; + +private: + void sanity_checks_system() const; + void sanity_checks_node_grid() const; +}; + +#endif // DIPOLES +#endif diff --git a/src/core/magnetostatics/dipoles.cpp b/src/core/magnetostatics/dipoles.cpp new file mode 100644 index 00000000000..f4d3eb58b4b --- /dev/null +++ b/src/core/magnetostatics/dipoles.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef DIPOLES + +#include "magnetostatics/dipoles.hpp" + +#include "ParticleRange.hpp" +#include "actor/traits.hpp" +#include "actor/visit_try_catch.hpp" +#include "actor/visitors.hpp" +#include "communication.hpp" +#include "errorhandling.hpp" +#include "grid.hpp" +#include "integrate.hpp" +#include "npt.hpp" + +#include +#include + +#include +#include + +#include +#include +#include + +boost::optional magnetostatics_actor; + +namespace Dipoles { + +void sanity_checks() { + if (magnetostatics_actor) { + boost::apply_visitor([](auto &actor) { actor->sanity_checks(); }, + *magnetostatics_actor); + } +} + +void on_dipoles_change() { + visit_active_actor_try_catch([](auto &actor) { actor->init(); }, + magnetostatics_actor); +} + +void on_boxl_change() { + visit_active_actor_try_catch([](auto &actor) { actor->on_boxl_change(); }, + magnetostatics_actor); +} + +void on_node_grid_change() { + if (magnetostatics_actor) { + boost::apply_visitor([](auto &actor) { actor->on_node_grid_change(); }, + *magnetostatics_actor); + } +} + +void on_periodicity_change() { + visit_active_actor_try_catch( + [](auto &actor) { actor->on_periodicity_change(); }, + magnetostatics_actor); +} + +void on_cell_structure_change() { + visit_active_actor_try_catch( + [](auto &actor) { actor->on_cell_structure_change(); }, + magnetostatics_actor); +} + +void calc_pressure_long_range() { + if (magnetostatics_actor) { + runtimeWarningMsg() << "pressure calculated, but pressure not implemented."; + } +} + +double cutoff(Utils::Vector3d const &box_l) { +#ifdef DP3M + if (auto dp3m = get_actor_by_type(magnetostatics_actor)) { + return dp3m->dp3m.params.r_cut; + } +#endif + return -1.; +} + +void on_observable_calc() { +#ifdef DP3M + if (auto dp3m = get_actor_by_type(magnetostatics_actor)) { + dp3m->count_magnetic_particles(); + } +#endif +} + +struct LongRangeForce : public boost::static_visitor { + ParticleRange const &m_particles; + explicit LongRangeForce(ParticleRange const &particles) + : m_particles(particles) {} + +#ifdef DP3M + void operator()(std::shared_ptr const &actor) const { + actor->dipole_assign(m_particles); +#ifdef NPT + if (integ_switch == INTEG_METHOD_NPT_ISO) { + auto const energy = actor->kernel(true, true, m_particles); + npt_add_virial_contribution(energy); + fprintf(stderr, "dipolar_P3M at this moment is added to p_vir[0]\n"); + } else +#endif // NPT + actor->kernel(true, false, m_particles); + } +#endif // DP3M + void operator()(std::shared_ptr const &actor) const { + actor->add_force_corrections(m_particles); + boost::apply_visitor(*this, actor->base_solver); + } + void operator()(std::shared_ptr const &actor) const { + actor->kernel(true, false, m_particles); + } + void + operator()(std::shared_ptr const &actor) const { + actor->kernel(true, false, m_particles); + } +#ifdef DIPOLAR_DIRECT_SUM + void operator()(std::shared_ptr const &actor) const { + actor->add_long_range_forces(); + } +#endif +#ifdef DIPOLAR_BARNES_HUT + void operator()(std::shared_ptr const &actor) const { + actor->add_long_range_forces(); + } +#endif +#ifdef SCAFACOS_DIPOLES + void operator()(std::shared_ptr const &actor) const { + actor->add_long_range_forces(); + } +#endif +}; + +struct LongRangeEnergy : public boost::static_visitor { + ParticleRange const &m_particles; + explicit LongRangeEnergy(ParticleRange const &particles) + : m_particles(particles) {} + +#ifdef DP3M + double operator()(std::shared_ptr const &actor) const { + actor->dipole_assign(m_particles); + return actor->kernel(false, true, m_particles); + } +#endif // DP3M + double + operator()(std::shared_ptr const &actor) const { + auto energy = boost::apply_visitor(*this, actor->base_solver); + return energy + actor->energy_correction(m_particles); + } + double operator()(std::shared_ptr const &actor) const { + return actor->kernel(false, true, m_particles); + } + double + operator()(std::shared_ptr const &actor) const { + return actor->kernel(false, true, m_particles); + } +#ifdef DIPOLAR_DIRECT_SUM + double operator()(std::shared_ptr const &actor) const { + actor->long_range_energy(); + return 0.; + } +#endif +#ifdef DIPOLAR_BARNES_HUT + double operator()(std::shared_ptr const &actor) const { + actor->long_range_energy(); + return 0.; + } +#endif +#ifdef SCAFACOS_DIPOLES + double operator()(std::shared_ptr const &actor) const { + return actor->long_range_energy(); + } +#endif +}; + +void calc_long_range_force(ParticleRange const &particles) { + if (magnetostatics_actor) { + boost::apply_visitor(LongRangeForce{particles}, *magnetostatics_actor); + } +} + +double calc_energy_long_range(ParticleRange const &particles) { + if (magnetostatics_actor) { + return boost::apply_visitor(LongRangeEnergy{particles}, + *magnetostatics_actor); + } + return 0.; +} + +namespace detail { +bool flag_all_reduce(bool flag) { + return boost::mpi::all_reduce(comm_cart, flag, std::logical_or<>()); +} +} // namespace detail + +} // namespace Dipoles +#endif // DIPOLES diff --git a/src/core/magnetostatics/dipoles.hpp b/src/core/magnetostatics/dipoles.hpp new file mode 100644 index 00000000000..44a68ea3e4d --- /dev/null +++ b/src/core/magnetostatics/dipoles.hpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLES_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLES_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "actor/traits.hpp" + +#include "magnetostatics/barnes_hut_gpu.hpp" +#include "magnetostatics/dds.hpp" +#include "magnetostatics/dds_gpu.hpp" +#include "magnetostatics/dds_replica.hpp" +#include "magnetostatics/dlc.hpp" +#include "magnetostatics/dp3m.hpp" +#include "magnetostatics/scafacos.hpp" + +#include "ParticleRange.hpp" + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +using MagnetostaticsActor = + boost::variant, +#ifdef DIPOLAR_DIRECT_SUM + std::shared_ptr, +#endif +#ifdef DIPOLAR_BARNES_HUT + std::shared_ptr, +#endif +#ifdef DP3M + std::shared_ptr, +#endif +#ifdef SCAFACOS_DIPOLES + std::shared_ptr, +#endif + std::shared_ptr, + std::shared_ptr>; + +extern boost::optional magnetostatics_actor; + +/** Get the magnetostatics prefactor. */ +struct GetDipolesPrefactor : public boost::static_visitor { + template + double operator()(std::shared_ptr const &actor) const { + return actor->prefactor; + } +}; + +/** Run actor sanity checks. */ +struct DipolesSanityChecks : public boost::static_visitor { + template void operator()(std::shared_ptr const &actor) const { + actor->sanity_checks(); + } +}; + +namespace Dipoles { + +namespace traits { + +/** @brief Whether an actor is a solver. */ +template +using is_solver = std::is_convertible, MagnetostaticsActor>; + +} // namespace traits + +void calc_pressure_long_range(); + +void sanity_checks(); +double cutoff(Utils::Vector3d const &box_l); + +void on_observable_calc(); +void on_dipoles_change(); +void on_boxl_change(); +void on_node_grid_change(); +void on_periodicity_change(); +void on_cell_structure_change(); + +void calc_long_range_force(ParticleRange const &particles); +double calc_energy_long_range(ParticleRange const &particles); + +namespace detail { +bool flag_all_reduce(bool flag); +} // namespace detail + +} // namespace Dipoles +#else // DIPOLES +#include +namespace Dipoles { +constexpr std::size_t pressure_n() { return 0; } +constexpr std::size_t energy_n() { return 0; } +} // namespace Dipoles +#endif // DIPOLES +#endif diff --git a/src/core/magnetostatics/dipoles_inline.hpp b/src/core/magnetostatics/dipoles_inline.hpp new file mode 100644 index 00000000000..be928295827 --- /dev/null +++ b/src/core/magnetostatics/dipoles_inline.hpp @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLES_INLINE_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLES_INLINE_HPP + +#include "config.hpp" + +#include "Particle.hpp" + +#include "actor/traits.hpp" +#include "actor/visitors.hpp" + +#include "magnetostatics/dipoles.hpp" +#include "magnetostatics/dp3m.hpp" + +#include + +#include + +#include + +namespace Dipoles { + +struct ShortRangeForceKernel + : public boost::static_visitor>> { + + using kernel_type = result_type::value_type; + +#ifdef DIPOLES + template + result_type operator()(std::shared_ptr const &) const { + return {}; + } + +#ifdef P3M + result_type operator()(std::shared_ptr const &ptr) const { + auto const &actor = *ptr; + return kernel_type{[&actor](Particle const &p1, Particle const &p2, + Utils::Vector3d const &d, double dist, + double dist2) { + return actor.pair_force(p1, p2, d, dist2, dist); + }}; + } +#endif // P3M + + result_type + operator()(std::shared_ptr const &ptr) const { + return boost::apply_visitor(*this, ptr->base_solver); + } +#endif // DIPOLES +}; + +struct ShortRangeEnergyKernel + : public boost::static_visitor>> { + + using kernel_type = result_type::value_type; + +#ifdef DIPOLES + template + result_type operator()(std::shared_ptr const &) const { + return {}; + } + +#ifdef P3M + result_type operator()(std::shared_ptr const &ptr) const { + auto const &actor = *ptr; + return kernel_type{[&actor](Particle const &p1, Particle const &p2, + Utils::Vector3d const &d, double dist, + double dist2) { + return actor.pair_energy(p1, p2, d, dist2, dist); + }}; + } +#endif // P3M + + result_type + operator()(std::shared_ptr const &ptr) const { + return boost::apply_visitor(*this, ptr->base_solver); + } +#endif // DIPOLES +}; + +inline ShortRangeForceKernel::result_type pair_force_kernel() { +#ifdef DIPOLES + if (magnetostatics_actor) { + auto const visitor = ShortRangeForceKernel{}; + return boost::apply_visitor(visitor, *magnetostatics_actor); + } +#endif // DIPOLES + return {}; +} + +inline ShortRangeEnergyKernel::result_type pair_energy_kernel() { +#ifdef DIPOLES + if (magnetostatics_actor) { + auto const visitor = ShortRangeEnergyKernel{}; + return boost::apply_visitor(visitor, *magnetostatics_actor); + } +#endif // DIPOLES + return {}; +} + +} // namespace Dipoles + +#endif diff --git a/src/core/magnetostatics/dlc.cpp b/src/core/magnetostatics/dlc.cpp new file mode 100644 index 00000000000..7cdad58d390 --- /dev/null +++ b/src/core/magnetostatics/dlc.cpp @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef DIPOLES + +#include "magnetostatics/dlc.hpp" + +#include "magnetostatics/dds_replica.hpp" +#include "magnetostatics/dipoles.hpp" +#include "magnetostatics/dp3m.hpp" +#include "p3m/common.hpp" + +#include "Particle.hpp" +#include "cells.hpp" +#include "communication.hpp" +#include "errorhandling.hpp" +#include "grid.hpp" +#include "particle_data.hpp" + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +void DipolarLayerCorrection::check_gap(Particle const &p) const { + if (p.dipm() != 0.) { + auto const z = p.pos()[2]; + if (z < 0. or z > dlc.box_h) { + runtimeErrorMsg() << "Particle " << p.id() << " entered DLC gap region " + << "by " << z - ((z < 0.) ? 0. : dlc.box_h); + } + } +} + +/** Calculate the maximal dipole moment in the system */ +static double calc_mu_max() { + auto const local_particles = cell_structure.local_particles(); + auto const mu_max_local = std::accumulate( + local_particles.begin(), local_particles.end(), 0., + [](double mu, Particle const &p) { return std::max(mu, p.dipm()); }); + + return boost::mpi::all_reduce(comm_cart, mu_max_local, + boost::mpi::maximum()); +} + +static double g1_DLC_dip(double g, double x) { + auto const c = g / x; + auto const cc2 = c * c; + auto const x3 = x * x * x; + auto const a = g * g * g / x + 1.5 * cc2 + 1.5 * g / x3 + 0.75 / (x3 * x); + return a; +} + +static double g2_DLC_dip(double g, double x) { + auto const x2 = x * x; + auto const a = g * g / x + 2.0 * g / x2 + 2.0 / (x2 * x); + return a; +} + +/** Compute the box magnetic dipole. */ +static Utils::Vector3d calc_slab_dipole(ParticleRange const &particles) { + + Utils::Vector3d local_dip{}; + for (auto const &p : particles) { + if (p.dipm() != 0.) { + local_dip += p.calc_dip(); + } + } + + return boost::mpi::all_reduce(comm_cart, local_dip, std::plus<>()); +} + +/** + * @brief Compute the dipolar force and torque corrections. + * %Algorithm implemented accordingly to @cite brodka04a. + */ +static void dipolar_force_corrections(int kcut, + std::vector &fs, + std::vector &ts, + ParticleRange const &particles) { + auto const facux = 2. * Utils::pi() * box_geo.length_inv()[0]; + auto const facuy = 2. * Utils::pi() * box_geo.length_inv()[1]; + + auto const n_local_particles = particles.size(); + std::vector ReSjp(n_local_particles); + std::vector ReSjm(n_local_particles); + std::vector ImSjp(n_local_particles); + std::vector ImSjm(n_local_particles); + std::vector ReGrad_Mup(n_local_particles); + std::vector ImGrad_Mup(n_local_particles); + std::vector ReGrad_Mum(n_local_particles); + std::vector ImGrad_Mum(n_local_particles); + + for (int ix = -kcut; ix <= +kcut; ix++) { + for (int iy = -kcut; iy <= +kcut; iy++) { + if (ix == 0 and iy == 0) { + continue; + } + auto const gx = static_cast(ix) * facux; + auto const gy = static_cast(iy) * facuy; + auto const gr = sqrt(gx * gx + gy * gy); + + // We assume short slab direction is in the z-direction + auto const fa1 = 1. / (gr * (exp(gr * box_geo.length()[2]) - 1.)); + + // ... Compute S+,(S+)*,S-,(S-)*, and Spj,Smj for the current g + + std::size_t ip = 0; + double S[4] = {0., 0., 0., 0.}; // S of Brodka method, or is S[4] = + // {Re(S+), Im(S+), Re(S-), Im(S-)} + for (auto const &p : particles) { + if (p.dipm() != 0.) { + auto const &pos = p.pos(); + auto const dip = p.calc_dip(); + + auto const a = gx * dip[0] + gy * dip[1]; + auto const b = gr * dip[2]; + auto const er = gx * pos[0] + gy * pos[1]; + auto const ez = gr * pos[2]; + auto const c = cos(er); + auto const d = sin(er); + auto const f = exp(ez); + + ReSjp[ip] = (b * c - a * d) * f; + ImSjp[ip] = (c * a + b * d) * f; + ReSjm[ip] = (-b * c - a * d) / f; + ImSjm[ip] = (c * a - b * d) / f; + ReGrad_Mup[ip] = c * f; + ReGrad_Mum[ip] = c / f; + ImGrad_Mup[ip] = d * f; + ImGrad_Mum[ip] = d / f; + + S[0] += ReSjp[ip]; + S[1] += ImSjp[ip]; + S[2] += ReSjm[ip]; + S[3] += ImSjm[ip]; + } + ip++; + } + + MPI_Allreduce(MPI_IN_PLACE, S, 4, MPI_DOUBLE, MPI_SUM, comm_cart); + + // ... Now we can compute the contributions to E,Fj,Ej for the current + // g-value + ip = 0; + for (auto &p : particles) { + if (p.dipm() != 0.) { + { + // compute contributions to the forces + auto const s1 = -(-ReSjp[ip] * S[3] + ImSjp[ip] * S[2]); + auto const s2 = +(ReSjm[ip] * S[1] - ImSjm[ip] * S[0]); + auto const s3 = -(-ReSjm[ip] * S[1] + ImSjm[ip] * S[0]); + auto const s4 = +(ReSjp[ip] * S[3] - ImSjp[ip] * S[2]); + + auto const s1z = +(ReSjp[ip] * S[2] + ImSjp[ip] * S[3]); + auto const s2z = -(ReSjm[ip] * S[0] + ImSjm[ip] * S[1]); + auto const s3z = -(ReSjm[ip] * S[0] + ImSjm[ip] * S[1]); + auto const s4z = +(ReSjp[ip] * S[2] + ImSjp[ip] * S[3]); + + auto const ss = s1 + s2 + s3 + s4; + fs[ip][0] += fa1 * gx * ss; + fs[ip][1] += fa1 * gy * ss; + fs[ip][2] += fa1 * gr * (s1z + s2z + s3z + s4z); + } + { + // compute contributions to the electrical field + auto const s1 = -(-ReGrad_Mup[ip] * S[3] + ImGrad_Mup[ip] * S[2]); + auto const s2 = +(ReGrad_Mum[ip] * S[1] - ImGrad_Mum[ip] * S[0]); + auto const s3 = -(-ReGrad_Mum[ip] * S[1] + ImGrad_Mum[ip] * S[0]); + auto const s4 = +(ReGrad_Mup[ip] * S[3] - ImGrad_Mup[ip] * S[2]); + + auto const s1z = +(ReGrad_Mup[ip] * S[2] + ImGrad_Mup[ip] * S[3]); + auto const s2z = -(ReGrad_Mum[ip] * S[0] + ImGrad_Mum[ip] * S[1]); + auto const s3z = -(ReGrad_Mum[ip] * S[0] + ImGrad_Mum[ip] * S[1]); + auto const s4z = +(ReGrad_Mup[ip] * S[2] + ImGrad_Mup[ip] * S[3]); + + auto const ss = s1 + s2 + s3 + s4; + ts[ip][0] += fa1 * gx * ss; + ts[ip][1] += fa1 * gy * ss; + ts[ip][2] += fa1 * gr * (s1z + s2z + s3z + s4z); + } + } + ++ip; + } + } + } + + // Convert from the corrections to the electrical field to the corrections + // for the torques + std::size_t ip = 0; + for (auto const &p : particles) { + if (p.dipm() != 0.) { + ts[ip] = vector_product(p.calc_dip(), ts[ip]); + } + ++ip; + } + + // Multiply by the factors we have left during the loops + + auto const piarea = + Utils::pi() * box_geo.length_inv()[0] * box_geo.length_inv()[1]; + for (std::size_t j = 0; j < n_local_particles; ++j) { + fs[j] *= piarea; + ts[j] *= piarea; + } +} + +/** + * @brief Compute the dipolar DLC energy correction. + * %Algorithm implemented accordingly to @cite brodka04a. + */ +static double dipolar_energy_correction(int kcut, + ParticleRange const &particles) { + auto const facux = 2. * Utils::pi() * box_geo.length_inv()[0]; + auto const facuy = 2. * Utils::pi() * box_geo.length_inv()[1]; + + double energy = 0.; + double sum_S[4] = {0., 0., 0., 0.}; + for (int ix = -kcut; ix <= +kcut; ix++) { + for (int iy = -kcut; iy <= +kcut; iy++) { + if ((ix == 0 && iy == 0)) { + continue; + } + auto const gx = static_cast(ix) * facux; + auto const gy = static_cast(iy) * facuy; + auto const gr = sqrt(gx * gx + gy * gy); + + // We assume short slab direction is in the z-direction + auto const fa1 = 1. / (gr * (exp(gr * box_geo.length()[2]) - 1.)); + + // ... Compute S+,(S+)*,S-,(S-)*, and Spj,Smj for the current g + + double S[4] = {0., 0., 0., 0.}; // S of Brodka method, or is S[4] = + // {Re(S+), Im(S+), Re(S-), Im(S-)} + for (auto const &p : particles) { + if (p.dipm() != 0.) { + auto const &pos = p.pos(); + auto const dip = p.calc_dip(); + + auto const a = gx * dip[0] + gy * dip[1]; + auto const b = gr * dip[2]; + auto const er = gx * pos[0] + gy * pos[1]; + auto const ez = gr * pos[2]; + auto const c = cos(er); + auto const d = sin(er); + auto const f = exp(ez); + + S[0] += (b * c - a * d) * f; + S[1] += (c * a + b * d) * f; + S[2] += (-b * c - a * d) / f; + S[3] += (c * a - b * d) / f; + } + } + boost::mpi::reduce(comm_cart, S, 4, sum_S, std::plus<>(), 0); + + // compute contribution to the energy + // s2=(ReSm*ReSp+ImSm*ImSp); s2=s1!!! + energy += fa1 * 2. * (sum_S[0] * sum_S[2] + sum_S[1] * sum_S[3]); + } + } + + auto const piarea = + Utils::pi() * box_geo.length_inv()[0] * box_geo.length_inv()[1]; + energy *= -piarea; + return (this_node == 0) ? energy : 0.; +} + +void DipolarLayerCorrection::add_force_corrections( + ParticleRange const &particles) const { + assert(dlc.far_cut > 0.); + auto const volume = box_geo.volume(); + auto const correc = 4. * Utils::pi() / volume; + + // --- Create arrays that should contain the corrections to + // the forces and torques, and set them to zero. + std::vector dip_DLC_f(particles.size()); + std::vector dip_DLC_t(particles.size()); + + //---- Compute the corrections ---------------------------------- + + // First the DLC correction + auto const kcut = static_cast(std::round(dlc.far_cut)); + dipolar_force_corrections(kcut, dip_DLC_f, dip_DLC_t, particles); + + // Now we compute the correction like Yeh and Klapp to take into account + // the fact that you are using a 3D PBC method which uses spherical + // summation instead of slab-wise summation. + // Slab-wise summation is the one required to apply DLC correction. + // This correction is often called SDC = Shape Dependent Correction. + // See @cite brodka04a. + + auto const box_dip = calc_slab_dipole(particles); + + // --- Transfer the computed corrections to the Forces, Energy and torques + // of the particles + + std::size_t ip = 0; + for (auto &p : particles) { + check_gap(p); + + if (p.dipm() != 0.) { + // SDC correction term is zero for the forces + p.force() += prefactor * dip_DLC_f[ip]; + + auto const dip = p.calc_dip(); + // SDC correction for the torques + auto d = Utils::Vector3d{0., 0., -correc * box_dip[2]}; +#ifdef DP3M + if (epsilon != P3M_EPSILON_METALLIC) { + d += correc * epsilon_correction * box_dip; + } +#endif + p.torque() += prefactor * (dip_DLC_t[ip] + vector_product(dip, d)); + } + ++ip; + } +} + +double DipolarLayerCorrection::energy_correction( + ParticleRange const &particles) const { + assert(dlc.far_cut > 0.); + auto const volume = box_geo.volume(); + auto const pref = prefactor * 2. * Utils::pi() / volume; + + // Check if particles aren't in the forbidden gap region + // This loop is needed, because there is no other guaranteed + // single pass over all particles in this function. + for (auto const &p : particles) { + check_gap(p); + } + + //---- Compute the corrections ---------------------------------- + + // First the DLC correction + auto const k_cut = static_cast(std::round(dlc.far_cut)); + auto dip_DLC_energy = prefactor * dipolar_energy_correction(k_cut, particles); + + // Now we compute the correction like Yeh and Klapp to take into account + // the fact that you are using a 3D PBC method which uses spherical + // summation instead of slab-wise summation. + // Slab-wise summation is the one required to apply DLC correction. + // This correction is often called SDC = Shape Dependent Correction. + // See @cite brodka04a. + + auto const box_dip = calc_slab_dipole(particles); + + if (this_node == 0) { + dip_DLC_energy += pref * Utils::sqr(box_dip[2]); +#ifdef DP3M + if (epsilon != P3M_EPSILON_METALLIC) { + dip_DLC_energy -= pref * epsilon_correction * box_dip.norm2(); + } +#endif + return dip_DLC_energy; + } + return 0.; +} + +static int count_magnetic_particles() { + int local_n = 0; + + for (auto const &p : cell_structure.local_particles()) { + if (p.dipm() != 0.) { + local_n++; + } + } + + return boost::mpi::all_reduce(comm_cart, local_n, std::plus<>()); +} + +/** Compute the cut-off in the DLC dipolar part to get a certain accuracy. + * We assume particles to have all the same value of the dipolar momentum + * modulus, which is taken as the largest value of mu inside the system. + * If we assume the gap has a width @c gap_size (within + * which there is no particles): Lz = h + gap_size + */ +double DipolarLayerCorrection::tune_far_cut() const { + /* we take the maximum dipole in the system, to be sure that the errors + * in the other case will be equal or less than for this one */ + auto const mu_max_sq = Utils::sqr(calc_mu_max()); + auto const lx = box_geo.length()[0]; + auto const ly = box_geo.length()[1]; + auto const lz = box_geo.length()[2]; + + if (std::abs(lx - ly) > 0.001) { + throw std::runtime_error("DLC tuning: box size in x direction is " + "different from y direction. The tuning " + "formula requires both to be equal."); + } + + auto constexpr limitkc = 200; + auto const piarea = Utils::pi() / (lx * ly); + auto const nmp = static_cast(count_magnetic_particles()); + auto const h = dlc.box_h; + auto far_cut = -1.; + for (int kc = 1; kc < limitkc; kc++) { + auto const gc = kc * 2. * Utils::pi() / lx; + auto const fa0 = sqrt(9. * exp(+2. * gc * h) * g1_DLC_dip(gc, lz - h) + + 9. * exp(-2. * gc * h) * g1_DLC_dip(gc, lz + h) + + 22. * g1_DLC_dip(gc, lz)); + auto const fa1 = sqrt(0.125 * piarea) * fa0; + auto const fa2 = g2_DLC_dip(gc, lz); + auto const de = nmp * mu_max_sq / (4. * (exp(gc * lz) - 1.)) * (fa1 + fa2); + if (de < dlc.maxPWerror) { + far_cut = static_cast(kc); + break; + } + } + if (far_cut <= 0.) { + throw std::runtime_error("DLC tuning failed: maxPWerror too small"); + } + return far_cut; +} + +/** @brief Lock an actor and modify its parameters. */ +struct AdaptSolver : public boost::static_visitor { + explicit AdaptSolver(DipolarLayerCorrection *this_ptr) : m_actor{this_ptr} {} + + void operator()(std::shared_ptr const &solver) { + m_actor->prefactor = solver->prefactor; + m_actor->epsilon = P3M_EPSILON_METALLIC; + } + +#ifdef DP3M + void operator()(std::shared_ptr const &solver) { + m_actor->prefactor = solver->prefactor; + m_actor->epsilon = solver->dp3m.params.epsilon; + } +#endif + +private: + DipolarLayerCorrection *m_actor; +}; + +void DipolarLayerCorrection::adapt_solver() { + auto visitor = AdaptSolver{this}; + boost::apply_visitor(visitor, base_solver); + epsilon_correction = + (epsilon == P3M_EPSILON_METALLIC) ? 0. : 1. / (2. * epsilon + 1.); +} + +void DipolarLayerCorrection::sanity_checks_node_grid() const { + if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { + throw std::runtime_error("DLC: requires periodicity (1 1 1)"); + } +} + +dlc_data::dlc_data(double maxPWerror, double gap_size, double far_cut) + : maxPWerror{maxPWerror}, gap_size{gap_size}, box_h{box_geo.length()[2] - + gap_size}, + far_cut{far_cut}, far_calculated{far_cut == -1.} { + if (far_cut <= 0. and not far_calculated) { + throw std::domain_error("Parameter 'far_cut' must be > 0"); + } + if (maxPWerror <= 0.) { + throw std::domain_error("Parameter 'maxPWerror' must be > 0"); + } + if (gap_size <= 0.) { + throw std::domain_error("Parameter 'gap_size' must be > 0"); + } +} + +void DipolarLayerCorrection::recalc_box_h() { + auto const new_box_h = box_geo.length()[2] - dlc.gap_size; + if (new_box_h < 0.) { + throw std::runtime_error("DLC gap size (" + std::to_string(dlc.gap_size) + + ") larger than box length in z-direction (" + + std::to_string(box_geo.length()[2]) + ")"); + } + dlc.box_h = new_box_h; +} + +DipolarLayerCorrection::DipolarLayerCorrection(dlc_data &¶meters, + BaseSolver &&solver) + : dlc{parameters}, base_solver{solver} { + adapt_solver(); +} + +#endif // DIPOLES diff --git a/src/core/magnetostatics/dlc.hpp b/src/core/magnetostatics/dlc.hpp new file mode 100644 index 00000000000..768c438398d --- /dev/null +++ b/src/core/magnetostatics/dlc.hpp @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DLC_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DLC_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "actor/traits.hpp" + +#include "magnetostatics/dds_replica.hpp" +#include "magnetostatics/dp3m.hpp" + +#include + +#include +#include + +#include + +struct DipolarLayerCorrection; + +namespace traits { +template <> +struct is_layer_correction : std::true_type {}; +} // namespace traits + +/** @brief Parameters for the DLC method */ +struct dlc_data { + dlc_data(double maxPWerror, double gap_size, double far_cut); + + /** maximal pairwise error of the potential and force */ + double maxPWerror; + + /** Size of the empty gap. Note that MDLC relies on the user to make sure + * that this condition is fulfilled. + */ + double gap_size; + + /** Up to where particles can be found */ + double box_h; + + /** Cutoff of the exponential sum. Since in all other MMM methods this is + * the far formula, we call it here the same, although in the ELC context + * it does not make much sense. + */ + double far_cut; + + /** Flag whether #far_cut was set by the user, or calculated by ESPResSo. + * In the latter case, the cutoff will be adapted if important parameters, + * such as the box dimensions, change. + */ + bool far_calculated; +}; + +/** + * @brief Adapt a magnetostatics solver to remove contributions from the + * z-direction. For details see @cite brodka04a. + */ +struct DipolarLayerCorrection { + using BaseSolver = boost::variant< +#ifdef DP3M + std::shared_ptr, +#endif + std::shared_ptr>; + + /** @name Variables from the adapted solver. */ + /**@{*/ + double prefactor; + double epsilon; + double epsilon_correction; + /**@}*/ + dlc_data dlc; + + /** Magnetostatics solver that is adapted. */ + BaseSolver base_solver; + + DipolarLayerCorrection(dlc_data &¶meters, BaseSolver &&solver); + + void on_activation() { + sanity_checks_node_grid(); + /* None of the DLC parameters depend on the DP3M parameters, + * but the DP3M parameters depend on the DLC parameters during tuning, + * therefore DLC needs to be tuned before DP3M. */ + recalc_box_h(); + recalc_far_cut(); + visit_base_solver([](auto &solver) { solver->on_activation(); }); + } + /** @brief Recalculate all box-length-dependent parameters. */ + void on_boxl_change() { + recalc_box_h(); + recalc_far_cut(); + visit_base_solver([](auto &actor) { actor->on_boxl_change(); }); + } + void on_node_grid_change() const { + sanity_checks_node_grid(); + visit_base_solver([](auto &solver) { solver->on_node_grid_change(); }); + } + void on_periodicity_change() const { + visit_base_solver([](auto &solver) { solver->on_periodicity_change(); }); + } + void on_cell_structure_change() const { + visit_base_solver([](auto &solver) { solver->on_cell_structure_change(); }); + } + void init() { + recalc_box_h(); + recalc_far_cut(); + visit_base_solver([](auto &solver) { solver->init(); }); + } + + void sanity_checks() const { + sanity_checks_node_grid(); + visit_base_solver([](auto &actor) { actor->sanity_checks(); }); + } + + void recalc_box_h(); + void recalc_far_cut() { + if (dlc.far_calculated) { + dlc.far_cut = tune_far_cut(); + } + } + + /** @brief Calculate the dipolar energy correction. */ + double energy_correction(ParticleRange const &particles) const; + /** @brief Add the dipolar force and torque corrections. */ + void add_force_corrections(ParticleRange const &particles) const; + + void adapt_solver(); + + void release_solver() { + prefactor = -1.; + epsilon = -1.; + epsilon_correction = 0.; + } + +private: + /** Check if a magnetic particle is in the gap region. */ + void check_gap(Particle const &p) const; + double tune_far_cut() const; + + void sanity_checks_node_grid() const; + + template void visit_base_solver(Visitor &&visitor) const { + boost::apply_visitor(visitor, base_solver); + } +}; + +#endif // DIPOLES + +#endif diff --git a/src/core/magnetostatics/dp3m.cpp b/src/core/magnetostatics/dp3m.cpp new file mode 100644 index 00000000000..5ca7e2654db --- /dev/null +++ b/src/core/magnetostatics/dp3m.cpp @@ -0,0 +1,925 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** @file + * P3M algorithm for long-range magnetic dipole-dipole interaction. + * + * By default the magnetic epsilon is metallic = 0. + */ + +#include "config.hpp" + +#ifdef DP3M + +#include "magnetostatics/dp3m.hpp" + +#include "p3m/TuningAlgorithm.hpp" +#include "p3m/TuningLogger.hpp" +#include "p3m/common.hpp" +#include "p3m/fft.hpp" +#include "p3m/influence_function_dipolar.hpp" +#include "p3m/interpolation.hpp" +#include "p3m/send_mesh.hpp" + +#include "Particle.hpp" +#include "ParticleRange.hpp" +#include "cell_system/CellStructureType.hpp" +#include "cells.hpp" +#include "communication.hpp" +#include "errorhandling.hpp" +#include "event.hpp" +#include "grid.hpp" +#include "integrate.hpp" +#include "npt.hpp" +#include "tuning.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +void DipolarP3M::count_magnetic_particles() { + int local_n = 0; + double local_mu2 = 0.; + + for (auto const &p : cell_structure.local_particles()) { + if (p.dipm() != 0.) { + local_mu2 += p.calc_dip().norm2(); + local_n++; + } + } + + boost::mpi::all_reduce(comm_cart, local_mu2, dp3m.sum_mu2, std::plus<>()); + boost::mpi::all_reduce(comm_cart, local_n, dp3m.sum_dip_part, std::plus<>()); +} + +static double dp3m_k_space_error(double box_size, int mesh, int cao, + int n_c_part, double sum_q2, double alpha_L); + +static double dp3m_real_space_error(double box_size, double r_cut_iL, + int n_c_part, double sum_q2, + double alpha_L); +static void dp3m_tune_aliasing_sums(int nx, int ny, int nz, int mesh, + double mesh_i, int cao, double alpha_L_i, + double *alias1, double *alias2); + +/** Compute the value of alpha through a bisection method. + * Based on eq. (33) @cite wang01a. + */ +double dp3m_rtbisection(double box_size, double r_cut_iL, int n_c_part, + double sum_q2, double x1, double x2, double xacc, + double tuned_accuracy); + +double DipolarP3M::calc_average_self_energy_k_space() const { + auto const start = Utils::Vector3i{dp3m.fft.plan[3].start}; + auto const size = Utils::Vector3i{dp3m.fft.plan[3].new_mesh}; + + auto const node_phi = grid_influence_function_self_energy( + dp3m.params, start, start + size, dp3m.g_energy); + + double phi = 0.; + boost::mpi::reduce(comm_cart, node_phi, phi, std::plus<>(), 0); + phi /= 3. * box_geo.length()[0] * Utils::int_pow<3>(dp3m.params.mesh[0]); + return phi * Utils::pi(); +} + +void DipolarP3M::init() { + assert(dp3m.params.mesh >= Utils::Vector3i::broadcast(1)); + assert(dp3m.params.cao >= 1 and dp3m.params.cao <= 7); + assert(dp3m.params.alpha > 0.); + + dp3m.params.cao3 = Utils::int_pow<3>(dp3m.params.cao); + dp3m.params.recalc_a_ai_cao_cut(box_geo.length()); + dp3m.local_mesh.calc_local_ca_mesh(dp3m.params, local_geo, skin, 0.); + + dp3m.sm.resize(comm_cart, dp3m.local_mesh); + + int ca_mesh_size = fft_init(dp3m.local_mesh.dim, dp3m.local_mesh.margin, + dp3m.params.mesh, dp3m.params.mesh_off, + dp3m.ks_pnum, dp3m.fft, node_grid, comm_cart); + dp3m.rs_mesh.resize(ca_mesh_size); + dp3m.ks_mesh.resize(ca_mesh_size); + + for (auto &val : dp3m.rs_mesh_dip) { + val.resize(ca_mesh_size); + } + + dp3m.calc_differential_operator(); + + /* fix box length dependent constants */ + scaleby_box_l(); + + count_magnetic_particles(); +} + +DipolarP3M::DipolarP3M(P3MParameters &¶meters, double prefactor, + int tune_timings, bool tune_verbose) + : dp3m{std::move(parameters)}, prefactor{prefactor}, + tune_timings{tune_timings}, tune_verbose{tune_verbose} { + + m_is_tuned = !dp3m.params.tuning; + dp3m.params.tuning = false; + + if (prefactor <= 0.) { + throw std::domain_error("Parameter 'prefactor' must be > 0"); + } + + if (dp3m.params.mesh != Utils::Vector3i::broadcast(dp3m.params.mesh[0])) { + throw std::domain_error("DipolarP3M requires a cubic mesh"); + } +} + +namespace { +template struct AssignDipole { + void operator()(dp3m_data_struct &dp3m, Utils::Vector3d const &real_pos, + Utils::Vector3d const &dip) const { + auto const weights = p3m_calculate_interpolation_weights( + real_pos, dp3m.params.ai, dp3m.local_mesh); + p3m_interpolate(dp3m.local_mesh, weights, + [&dip, &dp3m](int ind, double w) { + dp3m.rs_mesh_dip[0][ind] += w * dip[0]; + dp3m.rs_mesh_dip[1][ind] += w * dip[1]; + dp3m.rs_mesh_dip[2][ind] += w * dip[2]; + }); + + dp3m.inter_weights.store(weights); + } +}; +} // namespace + +void DipolarP3M::dipole_assign(ParticleRange const &particles) { + dp3m.inter_weights.reset(dp3m.params.cao); + + /* prepare local FFT mesh */ + for (auto &i : dp3m.rs_mesh_dip) + for (int j = 0; j < dp3m.local_mesh.size; j++) + i[j] = 0.; + + for (auto const &p : particles) { + if (p.dipm() != 0.) { + Utils::integral_parameter(dp3m.params.cao, dp3m, + p.pos(), p.calc_dip()); + } + } +} + +namespace { +template struct AssignTorques { + void operator()(dp3m_data_struct const &dp3m, double prefac, int d_rs, + ParticleRange const &particles) const { + + /* magnetic particle index */ + auto p_index = std::size_t{0ul}; + + for (auto &p : particles) { + if (p.dipm() != 0.) { + auto const w = dp3m.inter_weights.load(p_index); + + Utils::Vector3d E{}; + p3m_interpolate(dp3m.local_mesh, w, + [&E, &dp3m, d_rs](int ind, double w) { + E[d_rs] += w * dp3m.rs_mesh[ind]; + }); + + p.torque() -= vector_product(p.calc_dip(), prefac * E); + ++p_index; + } + } + } +}; + +template struct AssignForces { + void operator()(dp3m_data_struct const &dp3m, double prefac, int d_rs, + ParticleRange const &particles) const { + + /* magnetic particle index */ + auto p_index = std::size_t{0ul}; + + for (auto &p : particles) { + if (p.dipm() != 0.) { + auto const w = dp3m.inter_weights.load(p_index); + + Utils::Vector3d E{}; + p3m_interpolate(dp3m.local_mesh, w, [&E, &dp3m](int ind, double w) { + E[0] += w * dp3m.rs_mesh_dip[0][ind]; + E[1] += w * dp3m.rs_mesh_dip[1][ind]; + E[2] += w * dp3m.rs_mesh_dip[2][ind]; + }); + + p.force()[d_rs] += p.calc_dip() * prefac * E; + ++p_index; + } + } + } +}; +} // namespace + +double DipolarP3M::kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles) { + int i, ind, j[3]; + /* k-space energy */ + double k_space_energy_dip = 0.; + double tmp0, tmp1; + + auto const dipole_prefac = prefactor / Utils::int_pow<3>(dp3m.params.mesh[0]); + + if (dp3m.sum_mu2 > 0) { + /* Gather information for FFT grid inside the nodes domain (inner local + * mesh) and perform forward 3D FFT (Charge Assignment Mesh). */ + std::array meshes = {{dp3m.rs_mesh_dip[0].data(), + dp3m.rs_mesh_dip[1].data(), + dp3m.rs_mesh_dip[2].data()}}; + + dp3m.sm.gather_grid(Utils::make_span(meshes), comm_cart, + dp3m.local_mesh.dim); + + fft_perform_forw(dp3m.rs_mesh_dip[0].data(), dp3m.fft, comm_cart); + fft_perform_forw(dp3m.rs_mesh_dip[1].data(), dp3m.fft, comm_cart); + fft_perform_forw(dp3m.rs_mesh_dip[2].data(), dp3m.fft, comm_cart); + // Note: after these calls, the grids are in the order yzx and not xyz + // anymore!!! + } + + /* === k-space energy calculation === */ + if (energy_flag) { + /********************* + Dipolar energy + **********************/ + if (dp3m.sum_mu2 > 0) { + /* i*k differentiation for dipolar gradients: + * |(\Fourier{\vect{mu}}(k)\cdot \vect{k})|^2 */ + ind = 0; + i = 0; + double node_k_space_energy_dip = 0.0; + for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { + for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; j[1]++) { + for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; j[2]++) { + node_k_space_energy_dip += + dp3m.g_energy[i] * + (Utils::sqr( + dp3m.rs_mesh_dip[0][ind] * + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + + dp3m.rs_mesh_dip[1][ind] * + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + + dp3m.rs_mesh_dip[2][ind] * + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]) + + Utils::sqr( + dp3m.rs_mesh_dip[0][ind + 1] * + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + + dp3m.rs_mesh_dip[1][ind + 1] * + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + + dp3m.rs_mesh_dip[2][ind + 1] * + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]])); + ind += 2; + i++; + } + } + } + node_k_space_energy_dip *= + dipole_prefac * Utils::pi() * box_geo.length_inv()[0]; + boost::mpi::reduce(comm_cart, node_k_space_energy_dip, k_space_energy_dip, + std::plus<>(), 0); + + if (dp3m.energy_correction == 0.) + calc_energy_correction(); + + if (this_node == 0) { + /* self energy correction */ + k_space_energy_dip -= prefactor * dp3m.sum_mu2 * 2. * + Utils::int_pow<3>(dp3m.params.alpha) * + Utils::sqrt_pi_i() / 3.; + + /* dipolar energy correction due to systematic Madelung-self effects */ + auto const volume = box_geo.volume(); + k_space_energy_dip += prefactor * dp3m.energy_correction / volume; + } + } + } // if (energy_flag) + + /* === k-space force calculation === */ + if (force_flag) { + /**************************** + * DIPOLAR TORQUES (k-space) + ****************************/ + if (dp3m.sum_mu2 > 0) { + auto const two_pi_L_i = 2. * Utils::pi() * box_geo.length_inv()[0]; + /* fill in ks_mesh array for torque calculation */ + ind = 0; + i = 0; + + for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { // j[0]=n_y + for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; + j[1]++) { // j[1]=n_z + for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; + j[2]++) { // j[2]=n_x + // tmp0 = Re(mu)*k, tmp1 = Im(mu)*k + + tmp0 = dp3m.rs_mesh_dip[0][ind] * + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + + dp3m.rs_mesh_dip[1][ind] * + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + + dp3m.rs_mesh_dip[2][ind] * + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; + + tmp1 = dp3m.rs_mesh_dip[0][ind + 1] * + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + + dp3m.rs_mesh_dip[1][ind + 1] * + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + + dp3m.rs_mesh_dip[2][ind + 1] * + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; + + /* the optimal influence function is the same for torques + and energy */ + dp3m.ks_mesh[ind] = tmp0 * dp3m.g_energy[i]; + dp3m.ks_mesh[ind + 1] = tmp1 * dp3m.g_energy[i]; + ind += 2; + i++; + } + } + } + + /* Force component loop */ + for (int d = 0; d < 3; d++) { + auto const d_rs = (d + dp3m.ks_pnum) % 3; + ind = 0; + for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { + for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; j[1]++) { + for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; j[2]++) { + dp3m.rs_mesh[ind] = + dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * + dp3m.ks_mesh[ind]; + ind++; + dp3m.rs_mesh[ind] = + dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * + dp3m.ks_mesh[ind]; + ind++; + } + } + } + + /* Back FFT force component mesh */ + fft_perform_back(dp3m.rs_mesh.data(), false, dp3m.fft, comm_cart); + /* redistribute force component mesh */ + dp3m.sm.spread_grid(dp3m.rs_mesh.data(), comm_cart, + dp3m.local_mesh.dim); + /* Assign force component from mesh to particle */ + Utils::integral_parameter( + dp3m.params.cao, dp3m, dipole_prefac * two_pi_L_i, d_rs, particles); + } + + /*************************** + DIPOLAR FORCES (k-space) + ****************************/ + + // Compute forces after torques because the algorithm below overwrites the + // grids dp3m.rs_mesh_dip ! + // Note: I'll do here 9 inverse FFTs. By symmetry, we can reduce this + // number to 6 ! + /* fill in ks_mesh array for force calculation */ + ind = 0; + i = 0; + for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; j[0]++) { // j[0]=n_y + for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; + j[1]++) { // j[1]=n_z + for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; + j[2]++) { // j[2]=n_x + // tmp0 = Im(mu)*k, tmp1 = -Re(mu)*k + tmp0 = dp3m.rs_mesh_dip[0][ind + 1] * + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + + dp3m.rs_mesh_dip[1][ind + 1] * + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + + dp3m.rs_mesh_dip[2][ind + 1] * + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; + tmp1 = dp3m.rs_mesh_dip[0][ind] * + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] + + dp3m.rs_mesh_dip[1][ind] * + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] + + dp3m.rs_mesh_dip[2][ind] * + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]]; + dp3m.ks_mesh[ind] = tmp0 * dp3m.g_force[i]; + dp3m.ks_mesh[ind + 1] = -tmp1 * dp3m.g_force[i]; + ind += 2; + i++; + } + } + } + + /* Force component loop */ + for (int d = 0; d < 3; d++) { /* direction in k-space: */ + auto const d_rs = (d + dp3m.ks_pnum) % 3; + ind = 0; + for (j[0] = 0; j[0] < dp3m.fft.plan[3].new_mesh[0]; + j[0]++) { // j[0]=n_y + for (j[1] = 0; j[1] < dp3m.fft.plan[3].new_mesh[1]; + j[1]++) { // j[1]=n_z + for (j[2] = 0; j[2] < dp3m.fft.plan[3].new_mesh[2]; + j[2]++) { // j[2]=n_x + tmp0 = dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * + dp3m.ks_mesh[ind]; + dp3m.rs_mesh_dip[0][ind] = + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] * tmp0; + dp3m.rs_mesh_dip[1][ind] = + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] * tmp0; + dp3m.rs_mesh_dip[2][ind] = + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]] * tmp0; + ind++; + tmp0 = dp3m.d_op[0][j[d] + dp3m.fft.plan[3].start[d]] * + dp3m.ks_mesh[ind]; + dp3m.rs_mesh_dip[0][ind] = + dp3m.d_op[0][j[2] + dp3m.fft.plan[3].start[2]] * tmp0; + dp3m.rs_mesh_dip[1][ind] = + dp3m.d_op[0][j[0] + dp3m.fft.plan[3].start[0]] * tmp0; + dp3m.rs_mesh_dip[2][ind] = + dp3m.d_op[0][j[1] + dp3m.fft.plan[3].start[1]] * tmp0; + ind++; + } + } + } + /* Back FFT force component mesh */ + fft_perform_back(dp3m.rs_mesh_dip[0].data(), false, dp3m.fft, + comm_cart); + fft_perform_back(dp3m.rs_mesh_dip[1].data(), false, dp3m.fft, + comm_cart); + fft_perform_back(dp3m.rs_mesh_dip[2].data(), false, dp3m.fft, + comm_cart); + /* redistribute force component mesh */ + std::array meshes = {{dp3m.rs_mesh_dip[0].data(), + dp3m.rs_mesh_dip[1].data(), + dp3m.rs_mesh_dip[2].data()}}; + + dp3m.sm.spread_grid(Utils::make_span(meshes), comm_cart, + dp3m.local_mesh.dim); + /* Assign force component from mesh to particle */ + Utils::integral_parameter( + dp3m.params.cao, dp3m, dipole_prefac * Utils::sqr(two_pi_L_i), d_rs, + particles); + } + } /* if (dp3m.sum_mu2 > 0) */ + } /* if (force_flag) */ + + if (dp3m.params.epsilon != P3M_EPSILON_METALLIC) { + auto const surface_term = + calc_surface_term(force_flag, energy_flag, particles); + if (this_node == 0) + k_space_energy_dip += surface_term; + } + + return k_space_energy_dip; +} + +double DipolarP3M::calc_surface_term(bool force_flag, bool energy_flag, + ParticleRange const &particles) { + auto const pref = prefactor * 4. * Utils::pi() / box_geo.volume() / + (2. * dp3m.params.epsilon + 1.); + auto const n_local_part = particles.size(); + + // We put all the dipolar momenta in a the arrays mx,my,mz according to the + // id-number of the particles + std::vector mx(n_local_part); + std::vector my(n_local_part); + std::vector mz(n_local_part); + + int ip = 0; + for (auto const &p : particles) { + auto const dip = p.calc_dip(); + mx[ip] = dip[0]; + my[ip] = dip[1]; + mz[ip] = dip[2]; + ip++; + } + + // we will need the sum of all dipolar momenta vectors + auto local_dip = Utils::Vector3d{}; + for (int i = 0; i < n_local_part; i++) { + local_dip[0] += mx[i]; + local_dip[1] += my[i]; + local_dip[2] += mz[i]; + } + auto const box_dip = + boost::mpi::all_reduce(comm_cart, local_dip, std::plus<>()); + + double energy = 0.; + if (energy_flag) { + double sum_e = 0.; + for (int i = 0; i < n_local_part; i++) { + sum_e += mx[i] * box_dip[0] + my[i] * box_dip[1] + mz[i] * box_dip[2]; + } + energy = + 0.5 * pref * boost::mpi::all_reduce(comm_cart, sum_e, std::plus<>()); + } + + if (force_flag) { + + std::vector sumix(n_local_part); + std::vector sumiy(n_local_part); + std::vector sumiz(n_local_part); + + for (int i = 0; i < n_local_part; i++) { + sumix[i] = my[i] * box_dip[2] - mz[i] * box_dip[1]; + sumiy[i] = mz[i] * box_dip[0] - mx[i] * box_dip[2]; + sumiz[i] = mx[i] * box_dip[1] - my[i] * box_dip[0]; + } + + ip = 0; + for (auto &p : particles) { + auto &torque = p.torque(); + torque[0] -= pref * sumix[ip]; + torque[1] -= pref * sumiy[ip]; + torque[2] -= pref * sumiz[ip]; + ip++; + } + } + + return energy; +} + +void DipolarP3M::calc_influence_function_force() { + auto const start = Utils::Vector3i{dp3m.fft.plan[3].start}; + auto const size = Utils::Vector3i{dp3m.fft.plan[3].new_mesh}; + + dp3m.g_force = grid_influence_function<3>(dp3m.params, start, start + size, + box_geo.length()); +} + +void DipolarP3M::calc_influence_function_energy() { + auto const start = Utils::Vector3i{dp3m.fft.plan[3].start}; + auto const size = Utils::Vector3i{dp3m.fft.plan[3].new_mesh}; + + dp3m.g_energy = grid_influence_function<2>(dp3m.params, start, start + size, + box_geo.length()); +} + +class DipolarTuningAlgorithm : public TuningAlgorithm { + dp3m_data_struct &dp3m; + int m_mesh_max = -1, m_mesh_min = -1; + +public: + DipolarTuningAlgorithm(dp3m_data_struct &input_dp3m, double prefactor, + int timings) + : TuningAlgorithm{prefactor, timings}, dp3m{input_dp3m} {} + + P3MParameters &get_params() override { return dp3m.params; } + + void on_solver_change() const override { on_dipoles_change(); } + + boost::optional + layer_correction_veto_r_cut(double) const override { + return {}; + } + + void setup_logger(bool verbose) override { + m_logger = std::make_unique( + verbose and this_node == 0, "DipolarP3M", TuningLogger::Mode::Dipolar); + m_logger->tuning_goals(dp3m.params.accuracy, m_prefactor, + box_geo.length()[0], dp3m.sum_dip_part, + dp3m.sum_mu2); + m_logger->log_tuning_start(); + } + + std::tuple + calculate_accuracy(Utils::Vector3i const &mesh, int cao, + double r_cut_iL) const override { + + double alpha_L, rs_err, ks_err; + + /* calc maximal real space error for setting */ + rs_err = dp3m_real_space_error(box_geo.length()[0], r_cut_iL, + dp3m.sum_dip_part, dp3m.sum_mu2, 0.001); + // alpha cannot be zero for dipoles because real-space formula breaks down + + if (Utils::sqrt_2() * rs_err > dp3m.params.accuracy) { + /* assume rs_err = ks_err -> rs_err = accuracy/sqrt(2.0) -> alpha_L */ + alpha_L = dp3m_rtbisection( + box_geo.length()[0], r_cut_iL, dp3m.sum_dip_part, dp3m.sum_mu2, + 0.0001 * box_geo.length()[0], 5. * box_geo.length()[0], 0.0001, + dp3m.params.accuracy); + } else { + /* even alpha=0 is ok, however, we cannot choose it since it kills the + k-space error formula. + Anyways, this very likely NOT the optimal solution */ + alpha_L = 0.1; + } + + /* calculate real-space and k-space error for this alpha_L */ + rs_err = dp3m_real_space_error(box_geo.length()[0], r_cut_iL, + dp3m.sum_dip_part, dp3m.sum_mu2, alpha_L); + ks_err = dp3m_k_space_error(box_geo.length()[0], mesh[0], cao, + dp3m.sum_dip_part, dp3m.sum_mu2, alpha_L); + + return {Utils::Vector2d{rs_err, ks_err}.norm(), rs_err, ks_err, alpha_L}; + } + + void determine_mesh_limits() override { + if (dp3m.params.mesh[0] == -1) { + /* simple heuristic to limit the tried meshes if the accuracy cannot + be obtained with smaller meshes, but normally not all these + meshes have to be tested */ + auto const expo = std::log(std::cbrt(dp3m.sum_dip_part)) / std::log(2.); + /* Medium-educated guess for the minimal mesh */ + m_mesh_min = static_cast(std::round(std::pow(2., std::floor(expo)))); + /* avoid using more than 1 GB of FFT arrays */ + m_mesh_max = 128; + } else { + m_mesh_min = m_mesh_max = dp3m.params.mesh[0]; + m_logger->report_fixed_mesh(dp3m.params.mesh); + } + } + + TuningAlgorithm::Parameters get_time() override { + auto tuned_params = TuningAlgorithm::Parameters{}; + auto time_best = time_sentinel; + for (auto tmp_mesh = m_mesh_min; tmp_mesh <= m_mesh_max; tmp_mesh += 2) { + auto trial_params = TuningAlgorithm::Parameters{}; + trial_params.mesh = Utils::Vector3i::broadcast(tmp_mesh); + trial_params.cao = cao_best; + + auto const trial_time = + get_m_time(trial_params.mesh, trial_params.cao, trial_params.r_cut_iL, + trial_params.alpha_L, trial_params.accuracy); + + /* this mesh does not work at all */ + if (trial_time < 0.) + continue; + + /* the optimum r_cut for this mesh is the upper limit for higher meshes, + everything else is slower */ + m_r_cut_iL_max = trial_params.r_cut_iL; + + if (trial_time < time_best) { + /* new optimum */ + reset_n_trials(); + tuned_params = trial_params; + time_best = tuned_params.time = trial_time; + } else if (trial_time > time_best + time_granularity or + get_n_trials() > max_n_consecutive_trials) { + /* no hope of further optimisation */ + break; + } + } + return tuned_params; + } +}; + +void DipolarP3M::tune() { + if (dp3m.params.alpha_L == 0. and dp3m.params.alpha != 0.) { + dp3m.params.alpha_L = dp3m.params.alpha * box_geo.length()[0]; + } + if (dp3m.params.r_cut_iL == 0. and dp3m.params.r_cut != 0.) { + dp3m.params.r_cut_iL = dp3m.params.r_cut * box_geo.length_inv()[0]; + } + if (not is_tuned()) { + count_magnetic_particles(); + if (dp3m.sum_dip_part == 0) { + throw std::runtime_error( + "DipolarP3M: no dipolar particles in the system"); + } + try { + DipolarTuningAlgorithm parameters(dp3m, prefactor, tune_timings); + parameters.setup_logger(tune_verbose); + // parameter ranges + parameters.determine_mesh_limits(); + parameters.determine_r_cut_limits(); + parameters.determine_cao_limits(3); + // run tuning algorithm + parameters.tune(); + m_is_tuned = true; + on_dipoles_change(); + } catch (...) { + dp3m.params.tuning = false; + throw; + } + } + init(); +} + +/** Calculate the k-space error of dipolar-P3M */ +static double dp3m_k_space_error(double box_size, int mesh, int cao, + int n_c_part, double sum_q2, double alpha_L) { + double he_q = 0.; + auto const mesh_i = 1. / mesh; + auto const alpha_L_i = 1. / alpha_L; + + for (int nx = -mesh / 2; nx < mesh / 2; nx++) + for (int ny = -mesh / 2; ny < mesh / 2; ny++) + for (int nz = -mesh / 2; nz < mesh / 2; nz++) + if ((nx != 0) || (ny != 0) || (nz != 0)) { + auto const n2 = Utils::sqr(nx) + Utils::sqr(ny) + Utils::sqr(nz); + auto const cs = p3m_analytic_cotangent_sum(nx, mesh_i, cao) * + p3m_analytic_cotangent_sum(ny, mesh_i, cao) * + p3m_analytic_cotangent_sum(nz, mesh_i, cao); + double alias1, alias2; + dp3m_tune_aliasing_sums(nx, ny, nz, mesh, mesh_i, cao, alpha_L_i, + &alias1, &alias2); + double d = alias1 - Utils::sqr(alias2 / cs) / + Utils::int_pow<3>(static_cast(n2)); + /* at high precision, d can become negative due to extinction; + also, don't take values that have no significant digits left*/ + if (d > 0 && (fabs(d / alias1) > ROUND_ERROR_PREC)) + he_q += d; + } + + return 8. * Utils::sqr(Utils::pi()) / 3. * sum_q2 * sqrt(he_q / n_c_part) / + Utils::int_pow<4>(box_size); +} + +/** Tuning dipolar-P3M */ +void dp3m_tune_aliasing_sums(int nx, int ny, int nz, int mesh, double mesh_i, + int cao, double alpha_L_i, double *alias1, + double *alias2) { + using Utils::sinc; + + auto const factor1 = Utils::sqr(Utils::pi() * alpha_L_i); + + *alias1 = *alias2 = 0.; + for (int mx = -P3M_BRILLOUIN; mx <= P3M_BRILLOUIN; mx++) { + auto const nmx = nx + mx * mesh; + auto const fnmx = mesh_i * nmx; + for (int my = -P3M_BRILLOUIN; my <= P3M_BRILLOUIN; my++) { + auto const nmy = ny + my * mesh; + auto const fnmy = mesh_i * nmy; + for (int mz = -P3M_BRILLOUIN; mz <= P3M_BRILLOUIN; mz++) { + auto const nmz = nz + mz * mesh; + auto const fnmz = mesh_i * nmz; + + auto const nm2 = Utils::sqr(nmx) + Utils::sqr(nmy) + Utils::sqr(nmz); + auto const ex = std::exp(-factor1 * nm2); + + auto const U2 = pow(sinc(fnmx) * sinc(fnmy) * sinc(fnmz), 2. * cao); + + *alias1 += Utils::sqr(ex) * nm2; + *alias2 += U2 * ex * pow((nx * nmx + ny * nmy + nz * nmz), 3.) / nm2; + } + } + } +} + +/** Calculate the value of the errors for the REAL part of the force in terms + * of the splitting parameter alpha of Ewald. Based on eq. (33) @cite wang01a. + * + * Please note that in this more refined approach we don't use + * eq. (37), but eq. (33) which maintains all the powers in alpha. + */ +double dp3m_real_space_error(double box_size, double r_cut_iL, int n_c_part, + double sum_q2, double alpha_L) { + double d_error_f, d_cc, d_dc, d_con; + + auto const d_rcut = r_cut_iL * box_size; + auto const d_rcut2 = Utils::sqr(d_rcut); + auto const d_rcut4 = Utils::sqr(d_rcut2); + + auto const d_a2 = Utils::sqr(alpha_L) / Utils::sqr(box_size); + + auto const d_c = sum_q2 * exp(-d_a2 * d_rcut2); + + d_cc = 4. * Utils::sqr(d_a2) * Utils::sqr(d_rcut2) + 6. * d_a2 * d_rcut2 + 3.; + + d_dc = 8. * Utils::int_pow<3>(d_a2) * Utils::int_pow<3>(d_rcut2) + + 20. * Utils::sqr(d_a2) * d_rcut4 + 30. * d_a2 * d_rcut2 + 15.; + + d_con = 1. / sqrt(Utils::int_pow<3>(box_size) * Utils::sqr(d_a2) * d_rcut * + Utils::sqr(d_rcut4) * static_cast(n_c_part)); + + d_error_f = d_c * d_con * + sqrt((13. / 6.) * Utils::sqr(d_cc) + + (2. / 15.) * Utils::sqr(d_dc) - (13. / 15.) * d_cc * d_dc); + + return d_error_f; +} + +/** Using bisection, find the root of a function "func-tuned_accuracy/sqrt(2.)" + * known to lie between x1 and x2. The root, returned as rtbis, will be + * refined until its accuracy is \f$\pm\f$ @p xacc. + */ +double dp3m_rtbisection(double box_size, double r_cut_iL, int n_c_part, + double sum_q2, double x1, double x2, double xacc, + double tuned_accuracy) { + constexpr int JJ_RTBIS_MAX = 40; + + auto const constant = tuned_accuracy / Utils::sqrt_2(); + + auto const f1 = + dp3m_real_space_error(box_size, r_cut_iL, n_c_part, sum_q2, x1) - + constant; + auto const f2 = + dp3m_real_space_error(box_size, r_cut_iL, n_c_part, sum_q2, x2) - + constant; + if (f1 * f2 >= 0.0) { + throw std::runtime_error( + "Root must be bracketed for bisection in dp3m_rtbisection"); + } + // Orient the search dx, and set rtb to x1 or x2 ... + double dx; + double rtb = f1 < 0.0 ? (dx = x2 - x1, x1) : (dx = x1 - x2, x2); + for (int j = 1; j <= JJ_RTBIS_MAX; j++) { + auto const xmid = rtb + (dx *= 0.5); + auto const fmid = + dp3m_real_space_error(box_size, r_cut_iL, n_c_part, sum_q2, xmid) - + constant; + if (fmid <= 0.0) + rtb = xmid; + if (fabs(dx) < xacc || fmid == 0.0) + return rtb; + } + throw std::runtime_error("Too many bisections in dp3m_rtbisection"); +} + +void DipolarP3M::sanity_checks_boxl() const { + for (int i = 0; i < 3; i++) { + /* check k-space cutoff */ + if (dp3m.params.cao_cut[i] >= box_geo.length_half()[i]) { + std::stringstream msg; + msg << "dipolar P3M_init: k-space cutoff " << dp3m.params.cao_cut[i] + << " is larger than half of box dimension " << box_geo.length()[i]; + throw std::runtime_error(msg.str()); + } + if (dp3m.params.cao_cut[i] >= local_geo.length()[i]) { + std::stringstream msg; + msg << "dipolar P3M_init: k-space cutoff " << dp3m.params.cao_cut[i] + << " is larger than local box dimension " << local_geo.length()[i]; + throw std::runtime_error(msg.str()); + } + } + + if ((box_geo.length()[0] != box_geo.length()[1]) || + (box_geo.length()[1] != box_geo.length()[2])) { + throw std::runtime_error("DipolarP3M: requires a cubic box"); + } +} + +void DipolarP3M::sanity_checks_periodicity() const { + if (!box_geo.periodic(0) || !box_geo.periodic(1) || !box_geo.periodic(2)) { + throw std::runtime_error("DipolarP3M: requires periodicity (1 1 1)"); + } +} + +void DipolarP3M::sanity_checks_cell_structure() const { + if (local_geo.cell_structure_type() != + CellStructureType::CELL_STRUCTURE_REGULAR) { + throw std::runtime_error( + "DipolarP3M: requires the regular decomposition cell system"); + } +} + +void DipolarP3M::sanity_checks_node_grid() const { + if (node_grid[0] < node_grid[1] || node_grid[1] < node_grid[2]) { + throw std::runtime_error( + "DipolarP3M: node grid must be sorted, largest first"); + } +} + +void DipolarP3M::scaleby_box_l() { + dp3m.params.r_cut = dp3m.params.r_cut_iL * box_geo.length()[0]; + dp3m.params.alpha = dp3m.params.alpha_L * box_geo.length_inv()[0]; + dp3m.params.recalc_a_ai_cao_cut(box_geo.length()); + dp3m.local_mesh.recalc_ld_pos(dp3m.params); + sanity_checks_boxl(); + calc_influence_function_force(); + calc_influence_function_energy(); + dp3m.energy_correction = 0.0; +} + +void DipolarP3M::calc_energy_correction() { + auto const Ukp3m = calc_average_self_energy_k_space() * box_geo.volume(); + auto const Ewald_volume = Utils::int_pow<3>(dp3m.params.alpha_L); + auto const Eself = -2. * Ewald_volume * Utils::sqrt_pi_i() / 3.; + dp3m.energy_correction = + -dp3m.sum_mu2 * (Ukp3m + Eself + 2. * Utils::pi() / 3.); +} + +#ifdef NPT +void npt_add_virial_magnetic_contribution(double energy) { + npt_add_virial_contribution(energy); +} +#endif // NPT +#endif // DP3M diff --git a/src/core/magnetostatics/dp3m.hpp b/src/core/magnetostatics/dp3m.hpp new file mode 100644 index 00000000000..8a6aff6606a --- /dev/null +++ b/src/core/magnetostatics/dp3m.hpp @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** @file + * P3M algorithm for long-range magnetic dipole-dipole interaction. + * + * We use here a P3M (Particle-Particle Particle-Mesh) method based + * on the dipolar Ewald summation. Details of the used method can be + * found in @cite hockney88a and @cite deserno98a @cite deserno98b. + * + * Further reading: @cite cerda08d + */ + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_P3M_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLAR_P3M_HPP + +#include "config.hpp" + +#ifdef DP3M + +#include "p3m/common.hpp" +#include "p3m/data_struct.hpp" +#include "p3m/fft.hpp" +#include "p3m/interpolation.hpp" +#include "p3m/send_mesh.hpp" + +#include "Particle.hpp" +#include "ParticleRange.hpp" + +#include +#include +#include + +#include +#include +#include + +#ifdef NPT +/** Update the NpT virial */ +void npt_add_virial_magnetic_contribution(double energy); +#endif + +struct dp3m_data_struct : public p3m_data_struct_base { + explicit dp3m_data_struct(P3MParameters &¶meters) + : p3m_data_struct_base{std::move(parameters)} {} + + /** local mesh. */ + P3MLocalMesh local_mesh; + /** real space mesh (local) for CA/FFT. */ + fft_vector rs_mesh; + /** real space mesh (local) for CA/FFT of the dipolar field. */ + std::array, 3> rs_mesh_dip; + /** k-space mesh (local) for k-space calculation and FFT. */ + std::vector ks_mesh; + + /** number of dipolar particles (only on head node). */ + int sum_dip_part = 0; + /** Sum of square of magnetic dipoles (only on head node). */ + double sum_mu2 = 0.; + + /** position shift for calculation of first assignment mesh point. */ + double pos_shift = 0.; + + p3m_interpolation_cache inter_weights; + + /** send/recv mesh sizes */ + p3m_send_mesh sm; + + /** cached k-space self-energy correction */ + double energy_correction = 0.; + + fft_data_struct fft; +}; + +/** @brief Dipolar P3M solver. */ +struct DipolarP3M { + /** Dipolar P3M parameters. */ + dp3m_data_struct dp3m; + + /** Magnetostatics prefactor. */ + double prefactor; + int tune_timings; + bool tune_verbose; + + DipolarP3M(P3MParameters &¶meters, double prefactor, int tune_timings, + bool tune_verbose); + + void on_activation() { + sanity_checks(); + tune(); + } + /** @brief Recalculate all box-length-dependent parameters. */ + void on_boxl_change() { scaleby_box_l(); } + void on_node_grid_change() const { sanity_checks_node_grid(); } + void on_periodicity_change() const { sanity_checks_periodicity(); } + void on_cell_structure_change() { + sanity_checks_cell_structure(); + init(); + } + /** @brief Recalculate all derived parameters. */ + void init(); + + void sanity_checks() const { + sanity_checks_boxl(); + sanity_checks_node_grid(); + sanity_checks_periodicity(); + sanity_checks_cell_structure(); + } + + /** + * @brief Count the number of magnetic particles and calculate + * the sum of the squared dipole moments. + */ + void count_magnetic_particles(); + + /** Assign the physical dipoles using the tabulated assignment function. */ + void dipole_assign(ParticleRange const &particles); + + /** + * @brief Tune dipolar P3M parameters to desired accuracy. + * + * The parameters + * @ref P3MParameters::mesh "mesh", + * @ref P3MParameters::cao "cao", + * @ref P3MParameters::r_cut_iL "r_cut_iL" and + * @ref P3MParameters::alpha_L "alpha_L" are tuned to obtain the target + * @ref P3MParameters::accuracy "accuracy" in optimal time. + * These parameters are stored in the @ref dp3m object. + * + * The function utilizes the analytic expression of the error estimate + * for the dipolar P3M method in the paper of @cite cerda08d in + * order to obtain the rms error in the force for a system of N randomly + * distributed particles in a cubic box. For the real space error, the + * estimate in @cite kolafa92a is used. + * + * Parameter ranges if not given explicitly in the constructor: + * - @p mesh is set up such that the number of mesh points is equal to the + * number of magnetic dipolar particles + * - @p cao explores all possible values + * - @p alpha_L is tuned for each tuple (@p r_cut_iL, @p mesh, @p cao) and + * calculated assuming that the error contributions of real and reciprocal + * space should be equal + * + * After checking if the total error lies below the target accuracy, + * the time needed for one force calculation is measured. Parameters + * that minimize the runtime are kept. + * + * The function is based on routines of the program HE_Q.cpp for charges + * written by M. Deserno. + */ + void tune(); + bool is_tuned() const { return m_is_tuned; } + + /** Compute the k-space part of forces and energies. */ + double kernel(bool force_flag, bool energy_flag, + ParticleRange const &particles); + + /** Calculate real-space contribution of p3m dipolar pair forces and torques. + * If NPT is compiled in, update the NpT virial. + */ + inline ParticleForce pair_force(Particle const &p1, Particle const &p2, + Utils::Vector3d const &d, double dist2, + double dist) const { + if ((p1.p.dipm == 0.) || (p2.p.dipm == 0.) || dist >= dp3m.params.r_cut || + dist <= 0.) + return {}; + + auto const dip1 = p1.calc_dip(); + auto const dip2 = p2.calc_dip(); + auto const alpsq = dp3m.params.alpha * dp3m.params.alpha; + auto const adist = dp3m.params.alpha * dist; +#if USE_ERFC_APPROXIMATION + auto const erfc_part_ri = Utils::AS_erfc_part(adist) / dist; +#else + auto const erfc_part_ri = erfc(adist) / dist; +#endif + + // Calculate scalar multiplications for vectors mi, mj, rij + auto const mimj = dip1 * dip2; + + auto const mir = dip1 * d; + auto const mjr = dip2 * d; + + auto const coeff = 2. * dp3m.params.alpha * Utils::sqrt_pi_i(); + auto const dist2i = 1. / dist2; + auto const exp_adist2 = exp(-Utils::sqr(adist)); + + auto const B_r = (dp3m.params.accuracy > 5e-06) + ? (erfc_part_ri + coeff) * exp_adist2 * dist2i + : (erfc(adist) / dist + coeff * exp_adist2) * dist2i; + + auto const common_term = alpsq * coeff * exp_adist2; + auto const C_r = dist2i * (3. * B_r + 2. * common_term); + auto const D_r = dist2i * (5. * C_r + 4. * common_term * alpsq); + + // Calculate real-space forces + auto const force = prefactor * ((mimj * d + dip1 * mjr + dip2 * mir) * C_r - + mir * mjr * D_r * d); + + // Calculate vector multiplications for vectors mi, mj, rij + auto const mixmj = vector_product(dip1, dip2); + auto const mixr = vector_product(dip1, d); + + // Calculate real-space torques + auto const torque = prefactor * (-mixmj * B_r + mixr * (mjr * C_r)); +#ifdef NPT +#if USE_ERFC_APPROXIMATION + auto const fac = prefactor * p1.p.dipm * p2.p.dipm * exp_adist2; +#else + auto const fac = prefactor * p1.p.dipm * p2.p.dipm; +#endif + auto const energy = fac * (mimj * B_r - mir * mjr * C_r); + npt_add_virial_magnetic_contribution(energy); +#endif // NPT + return ParticleForce{force, torque}; + } + + /** Calculate real-space contribution of dipolar pair energy. */ + inline double pair_energy(Particle const &p1, Particle const &p2, + Utils::Vector3d const &d, double dist2, + double dist) const { + if ((p1.p.dipm == 0.) || (p2.p.dipm == 0.) || dist >= dp3m.params.r_cut || + dist <= 0.) + return {}; + + auto const dip1 = p1.calc_dip(); + auto const dip2 = p2.calc_dip(); + + auto const alpsq = dp3m.params.alpha * dp3m.params.alpha; + auto const adist = dp3m.params.alpha * dist; + +#if USE_ERFC_APPROXIMATION + auto const erfc_part_ri = Utils::AS_erfc_part(adist) / dist; +#else + auto const erfc_part_ri = erfc(adist) / dist; +#endif + + // Calculate scalar multiplications for vectors mi, mj, rij + auto const mimj = dip1 * dip2; + auto const mir = dip1 * d; + auto const mjr = dip2 * d; + + auto const coeff = 2. * dp3m.params.alpha * Utils::sqrt_pi_i(); + auto const dist2i = 1. / dist2; + auto const exp_adist2 = exp(-Utils::sqr(adist)); + + auto const B_r = (dp3m.params.accuracy > 5e-06) + ? dist2i * (erfc_part_ri + coeff) * exp_adist2 + : dist2i * (erfc(adist) / dist + coeff * exp_adist2); + auto const C_r = (3. * B_r + 2. * alpsq * coeff * exp_adist2) * dist2i; + + return prefactor * (mimj * B_r - mir * mjr * C_r); + } + +private: + bool m_is_tuned; + + /** Calculate self-energy in k-space. */ + double calc_average_self_energy_k_space() const; + + /** Calculate energy correction that minimizes the error. + * This quantity is only calculated once and is cached. + */ + void calc_energy_correction(); + + /** Calculate the influence function for the dipolar forces and torques. */ + void calc_influence_function_force(); + + /** Calculate the influence function for the dipolar energy. */ + void calc_influence_function_energy(); + + /** Compute the dipolar surface terms */ + double calc_surface_term(bool force_flag, bool energy_flag, + ParticleRange const &particles); + + /** Checks for correctness of the k-space cutoff. */ + void sanity_checks_boxl() const; + void sanity_checks_node_grid() const; + void sanity_checks_periodicity() const; + void sanity_checks_cell_structure() const; + + void scaleby_box_l(); +}; + +#endif // DP3M +#endif diff --git a/src/core/magnetostatics/registration.hpp b/src/core/magnetostatics/registration.hpp new file mode 100644 index 00000000000..8daa094eb5f --- /dev/null +++ b/src/core/magnetostatics/registration.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLE_REGISTRATION_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_DIPOLE_REGISTRATION_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "magnetostatics/dipoles.hpp" + +#include "actor/registration.hpp" +#include "actor/visitors.hpp" + +#include "event.hpp" + +#include +#include + +#include +#include +#include + +namespace Dipoles { + +template ::value> * = nullptr> +void add_actor(std::shared_ptr const &actor) { + if (::magnetostatics_actor) { + auto const name = get_actor_name(*::magnetostatics_actor); + throw std::runtime_error("A magnetostatics solver is already active (" + + name + ")"); + } + add_actor(::magnetostatics_actor, actor, ::on_dipoles_change, + detail::flag_all_reduce); +} + +template ::value> * = nullptr> +void remove_actor(std::shared_ptr const &actor) { + if (not is_already_stored(actor, ::magnetostatics_actor)) { + throw std::runtime_error( + "The given magnetostatics solver is not currently active"); + } + remove_actor(::magnetostatics_actor, actor, ::on_dipoles_change); +} + +} // namespace Dipoles + +#endif // DIPOLES +#endif diff --git a/src/core/electrostatics_magnetostatics/common.cpp b/src/core/magnetostatics/scafacos.hpp similarity index 51% rename from src/core/electrostatics_magnetostatics/common.cpp rename to src/core/magnetostatics/scafacos.hpp index 25d98eb0683..9c2c90aa7e2 100644 --- a/src/core/electrostatics_magnetostatics/common.cpp +++ b/src/core/magnetostatics/scafacos.hpp @@ -19,38 +19,35 @@ * along with this program. If not, see . */ -#include "electrostatics_magnetostatics/common.hpp" +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_SCAFACOS_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_SCAFACOS_HPP -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" - -#include "communication.hpp" #include "config.hpp" -#include "event.hpp" -#include +#ifdef SCAFACOS_DIPOLES -static void mpi_bcast_coulomb_params_local() { -#ifdef ELECTROSTATICS - MPI_Bcast(&coulomb, sizeof(Coulomb_parameters), MPI_BYTE, 0, comm_cart); - Coulomb::bcast_coulomb_params(); -#endif +#include "scafacos/ScafacosContextBase.hpp" -#ifdef DIPOLES - MPI_Bcast(&dipole, sizeof(Dipole_parameters), MPI_BYTE, 0, comm_cart); - Dipole::set_method_local(dipole.method); - Dipole::bcast_params(comm_cart); -#endif +#include +#include -#if defined(ELECTROSTATICS) || defined(DIPOLES) - on_coulomb_change(); -#endif -} +struct DipolarScafacos : virtual public ScafacosContextBase { + ~DipolarScafacos() override = default; + double prefactor = 0.; + + void on_activation() { update_system_params(); } + /** @brief Recalculate all box-length-dependent parameters. */ + void on_boxl_change() { update_system_params(); } + void on_node_grid_change() const {} + void on_periodicity_change() { update_system_params(); } + void on_cell_structure_change() const {} + void init() const {} + + void sanity_checks() const override {} +}; -REGISTER_CALLBACK(mpi_bcast_coulomb_params_local) +std::shared_ptr +make_dipolar_scafacos(std::string const &method, std::string const ¶meters); -void mpi_bcast_coulomb_params() { -#if defined(ELECTROSTATICS) || defined(DIPOLES) - mpi_call_all(mpi_bcast_coulomb_params_local); +#endif // SCAFACOS_DIPOLES #endif -} diff --git a/src/core/magnetostatics/scafacos_impl.cpp b/src/core/magnetostatics/scafacos_impl.cpp new file mode 100644 index 00000000000..803ed852528 --- /dev/null +++ b/src/core/magnetostatics/scafacos_impl.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef SCAFACOS_DIPOLES + +#include "magnetostatics/scafacos.hpp" +#include "magnetostatics/scafacos_impl.hpp" + +#include "cells.hpp" +#include "communication.hpp" +#include "grid.hpp" +#include "particle_data.hpp" + +#include +#include +#include + +#include +#include +#include + +std::shared_ptr +make_dipolar_scafacos(std::string const &method, + std::string const ¶meters) { + return std::make_shared(comm_cart, method, parameters); +} + +void DipolarScafacosImpl::update_particle_data() { + positions.clear(); + dipoles.clear(); + + for (auto const &p : cell_structure.local_particles()) { + auto const pos = folded_position(p.pos(), box_geo); + positions.push_back(pos[0]); + positions.push_back(pos[1]); + positions.push_back(pos[2]); + auto const dip = p.calc_dip(); + dipoles.push_back(dip[0]); + dipoles.push_back(dip[1]); + dipoles.push_back(dip[2]); + } +} + +void DipolarScafacosImpl::update_particle_forces() const { + if (positions.empty()) + return; + + auto it_potentials = potentials.begin(); + auto it_f = std::size_t{0ul}; + for (auto &p : cell_structure.local_particles()) { + // The scafacos term "potential" here in fact refers to the magnetic + // field. So, the torques are given by m \times B + auto const dip = p.calc_dip(); + auto const t = vector_product( + dip, Utils::Vector3d(Utils::Span(&*it_potentials, 3))); + // The force is given by G m, where G is a matrix + // which comes from the "fields" output of scafacos like this + // 0 1 2 + // 1 3 4 + // 2 4 5 + // where the numbers refer to indices in the "field" output from scafacos + auto const G = Utils::Matrix{ + {fields[it_f + 0ul], fields[it_f + 1ul], fields[it_f + 2ul]}, + {fields[it_f + 1ul], fields[it_f + 3ul], fields[it_f + 4ul]}, + {fields[it_f + 2ul], fields[it_f + 4ul], fields[it_f + 5ul]}}; + auto const f = G * dip; + + // Add to particles + p.force() += prefactor * f; + p.torque() += prefactor * t; + it_f += 6ul; + it_potentials += 3; + } + + /* Check that the particle number did not change */ + assert(it_potentials == potentials.end()); +} + +#endif // SCAFACOS_DIPOLES diff --git a/src/core/magnetostatics/scafacos_impl.hpp b/src/core/magnetostatics/scafacos_impl.hpp new file mode 100644 index 00000000000..d6a86a72879 --- /dev/null +++ b/src/core/magnetostatics/scafacos_impl.hpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_MAGNETOSTATICS_SCAFACOS_IMPL_HPP +#define ESPRESSO_SRC_CORE_MAGNETOSTATICS_SCAFACOS_IMPL_HPP + +#include "config.hpp" + +#ifdef SCAFACOS_DIPOLES + +#include "magnetostatics/scafacos.hpp" + +#include "scafacos/ScafacosContext.hpp" + +#include + +#include + +#include + +#if !defined(FCS_ENABLE_DIPOLES) +#error \ + "SCAFACOS_DIPOLES requires dipoles support in scafacos library (FCS_ENABLE_DIPOLES)." +#endif + +struct DipolarScafacosImpl : public DipolarScafacos, + public ScafacosContext<::Scafacos::Dipoles> { + using ScafacosContext = ScafacosContext<::Scafacos::Dipoles>; + using DipolarScafacos::DipolarScafacos; + using ScafacosContext::ScafacosContext; + + void update_particle_data() override; + void update_particle_forces() const override; + + double long_range_energy() override { + update_particle_data(); + run(dipoles, positions, fields, potentials); + return -0.5 * prefactor * boost::inner_product(dipoles, potentials, 0.0); + } + + void add_long_range_forces() override { + update_particle_data(); + run(dipoles, positions, fields, potentials); + update_particle_forces(); + } + +private: + /** Inputs */ + std::vector positions, dipoles; + /** Outputs */ + std::vector fields, potentials; +}; + +#endif // SCAFACOS_DIPOLES +#endif diff --git a/src/core/nonbonded_interactions/nonbonded_interaction_data.cpp b/src/core/nonbonded_interactions/nonbonded_interaction_data.cpp index 90e09ae7cc1..3891b3cd527 100644 --- a/src/core/nonbonded_interactions/nonbonded_interaction_data.cpp +++ b/src/core/nonbonded_interactions/nonbonded_interaction_data.cpp @@ -24,7 +24,7 @@ #include "nonbonded_interactions/nonbonded_interaction_data.hpp" #include "communication.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" +#include "electrostatics/coulomb.hpp" #include "event.hpp" #include "grid.hpp" #include "serialization/IA_parameters.hpp" diff --git a/src/core/nonbonded_interactions/thole.hpp b/src/core/nonbonded_interactions/thole.hpp index 2e6ff05c2f4..e120fca0e53 100644 --- a/src/core/nonbonded_interactions/thole.hpp +++ b/src/core/nonbonded_interactions/thole.hpp @@ -33,8 +33,8 @@ #include "Particle.hpp" #include "bonded_interactions/bonded_interaction_data.hpp" #include "bonded_interactions/bonded_interaction_utils.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/coulomb_inline.hpp" +#include "electrostatics/coulomb.hpp" +#include "electrostatics/coulomb_inline.hpp" #include "grid.hpp" #include "nonbonded_interactions/nonbonded_interaction_data.hpp" @@ -46,13 +46,15 @@ int thole_set_params(int part_type_a, int part_type_b, double scaling_coeff, double q1q2); /** Calculate Thole force */ -inline Utils::Vector3d thole_pair_force(Particle const &p1, Particle const &p2, - IA_parameters const &ia_params, - Utils::Vector3d const &d, double dist) { +inline Utils::Vector3d +thole_pair_force(Particle const &p1, Particle const &p2, + IA_parameters const &ia_params, Utils::Vector3d const &d, + double dist, + Coulomb::ShortRangeForceKernel::kernel_type const *kernel) { auto const thole_q1q2 = ia_params.thole.q1q2; auto const thole_s = ia_params.thole.scaling_coeff; - if (thole_s != 0 && thole_q1q2 != 0 && + if (thole_s != 0. and thole_q1q2 != 0. and kernel != nullptr and !(pair_bond_enum_exists_between(p1, p2))) { // Calc damping function (see @cite thole81a) // S(r) = 1.0 - (1.0 + thole_s*r/2.0) * exp(-thole_s*r); @@ -61,26 +63,28 @@ inline Utils::Vector3d thole_pair_force(Particle const &p1, Particle const &p2, // q1q2/r^2 can be used as a factor for the Coulomb::central_force method auto const sr = thole_s * dist; auto const dS_r = 0.5 * (2.0 - (exp(-sr) * (sr * (sr + 2.0) + 2.0))); - return Coulomb::central_force(thole_q1q2 * (-1. + dS_r), d, dist); + return (*kernel)(thole_q1q2 * (-1. + dS_r), d, dist); } return {}; } /** Calculate Thole energy */ -inline double thole_pair_energy(Particle const &p1, Particle const &p2, - IA_parameters const &ia_params, - Utils::Vector3d const &d, double dist) { +inline double +thole_pair_energy(Particle const &p1, Particle const &p2, + IA_parameters const &ia_params, Utils::Vector3d const &d, + double dist, + Coulomb::ShortRangeEnergyKernel::kernel_type const *kernel) { auto const thole_s = ia_params.thole.scaling_coeff; auto const thole_q1q2 = ia_params.thole.q1q2; - if (thole_s != 0 && thole_q1q2 != 0 && - dist < Coulomb::cutoff(box_geo.length()) && + if (thole_s != 0. and thole_q1q2 != 0. and kernel != nullptr and + dist < Coulomb::cutoff(box_geo.length()) and !(pair_bond_enum_exists_between(p1, p2))) { auto const sd = thole_s * dist; auto const S_r = 1.0 - (1.0 + sd / 2.0) * exp(-sd); - // Subtract p3m shortrange energy and add thole energy - return Coulomb::pair_energy(p1, p2, thole_q1q2 * (-1.0 + S_r), d, dist); + // Subtract p3m short-range energy and add thole energy + return (*kernel)(p1, p2, thole_q1q2 * (-1.0 + S_r), d, dist); } return 0.0; } diff --git a/src/core/npt.cpp b/src/core/npt.cpp index fbcc3930c2c..53b7bb5b39e 100644 --- a/src/core/npt.cpp +++ b/src/core/npt.cpp @@ -22,11 +22,11 @@ #include "communication.hpp" #include "config.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" +#include "electrostatics/coulomb.hpp" #include "errorhandling.hpp" #include "event.hpp" #include "integrate.hpp" +#include "magnetostatics/dipoles.hpp" #include @@ -76,24 +76,18 @@ void mpi_bcast_nptiso_geom_barostat() { void integrator_npt_coulomb_dipole_sanity_checks( NptIsoParameters const ¶ms) { #ifdef ELECTROSTATICS - if (params.dimension < 3 && !params.cubic_box && coulomb.prefactor > 0) { + if (params.dimension < 3 && !params.cubic_box && electrostatics_actor) { throw std::runtime_error("If electrostatics is being used you must " "use the cubic box NpT."); } #endif #ifdef DIPOLES - if (params.dimension < 3 && !params.cubic_box && dipole.prefactor > 0) { + if (params.dimension < 3 && !params.cubic_box && magnetostatics_actor) { throw std::runtime_error("If magnetostatics is being used you must " "use the cubic box NpT."); } #endif - -#if defined(ELECTROSTATICS) && defined(CUDA) - if (coulomb.method == COULOMB_P3M_GPU) { - throw std::runtime_error("NpT virial cannot be calculated on P3M GPU"); - } -#endif } void nptiso_init(double ext_pressure, double piston, bool xdir_rescale, diff --git a/src/python/espressomd/actors.pxd b/src/core/p3m/CMakeLists.txt similarity index 69% rename from src/python/espressomd/actors.pxd rename to src/core/p3m/CMakeLists.txt index 0bfa37be4fa..e140b214599 100644 --- a/src/python/espressomd/actors.pxd +++ b/src/core/p3m/CMakeLists.txt @@ -1,4 +1,5 @@ -# Copyright (C) 2010-2019 The ESPResSo project +# +# Copyright (C) 2018-2022 The ESPResSo project # # This file is part of ESPResSo. # @@ -14,7 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -cdef class Actor: - cdef public _isactive - cdef public _params - cdef public system +# +target_sources( + EspressoCore + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/common.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/TuningAlgorithm.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/send_mesh.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/fft.cpp) diff --git a/src/core/p3m/TuningAlgorithm.cpp b/src/core/p3m/TuningAlgorithm.cpp new file mode 100644 index 00000000000..80845109b4f --- /dev/null +++ b/src/core/p3m/TuningAlgorithm.cpp @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#if defined(P3M) || defined(DP3M) + +#include "p3m/TuningAlgorithm.hpp" +#include "p3m/common.hpp" + +#include "tuning.hpp" + +#include "communication.hpp" +#include "grid.hpp" +#include "integrate.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +/** @name Error codes for tuning. */ +/**@{*/ +/** charge assignment order too large for mesh size */ +static auto constexpr P3M_TUNE_CAO_TOO_LARGE = 1.; +/** conflict with ELC gap size */ +static auto constexpr P3M_TUNE_ELC_GAP_SIZE = 2.; +/** could not achieve target accuracy */ +static auto constexpr P3M_TUNE_ACCURACY_TOO_LARGE = 3.; +/**@}*/ + +/** @brief Precision threshold for a non-zero real-space cutoff. */ +static auto constexpr P3M_RCUT_PREC = 1e-3; + +void TuningAlgorithm::determine_r_cut_limits() { + auto const r_cut_iL = get_params().r_cut_iL; + if (r_cut_iL == 0.) { + auto const min_box_l = *boost::min_element(box_geo.length()); + auto const min_local_box_l = *boost::min_element(local_geo.length()); + m_r_cut_iL_min = 0.; + m_r_cut_iL_max = std::min(min_local_box_l, min_box_l / 2.) - skin; + m_r_cut_iL_min *= box_geo.length_inv()[0]; + m_r_cut_iL_max *= box_geo.length_inv()[0]; + } else { + m_r_cut_iL_min = m_r_cut_iL_max = r_cut_iL; + m_logger->report_fixed_r_cut_iL(r_cut_iL); + } +} + +void TuningAlgorithm::determine_cao_limits(int initial_cao) { + assert(initial_cao >= 1 and initial_cao <= 7); + auto const cao = get_params().cao; + if (cao == -1) { + cao_min = 1; + cao_max = 7; + cao_best = initial_cao; + } else { + cao_min = cao_max = cao_best = cao; + m_logger->report_fixed_cao(cao); + } +} + +void TuningAlgorithm::commit(Utils::Vector3i const &mesh, int cao, + double r_cut_iL, double alpha_L) { + auto &p3m_params = get_params(); + p3m_params.r_cut = r_cut_iL * box_geo.length()[0]; + p3m_params.r_cut_iL = r_cut_iL; + p3m_params.cao = cao; + p3m_params.alpha_L = alpha_L; + p3m_params.alpha = alpha_L * box_geo.length_inv()[0]; + p3m_params.mesh = mesh; +} + +/** + * @brief Get the optimal alpha and the corresponding computation time + * for a fixed @p mesh and @p cao. + * + * The @p tuned_r_cut_iL is determined via a simple bisection. + * + * @param[in] mesh @copybrief P3MParameters::mesh + * @param[in] cao @copybrief P3MParameters::cao + * @param[in,out] tuned_r_cut_iL @copybrief P3MParameters::r_cut_iL + * @param[in,out] tuned_alpha_L @copybrief P3MParameters::alpha_L + * @param[in,out] tuned_accuracy @copybrief P3MParameters::accuracy + * + * @returns The integration time in case of success, otherwise + * -@ref P3M_TUNE_ACCURACY_TOO_LARGE, + * -@ref P3M_TUNE_CAO_TOO_LARGE, or -@ref P3M_TUNE_ELC_GAP_SIZE + */ +double TuningAlgorithm::get_mc_time(Utils::Vector3i const &mesh, int cao, + double &tuned_r_cut_iL, + double &tuned_alpha_L, + double &tuned_accuracy) { + auto const target_accuracy = get_params().accuracy; + double rs_err, ks_err; + double r_cut_iL_min = m_r_cut_iL_min; + double r_cut_iL_max = m_r_cut_iL_max; + + /* initial checks. */ + auto const k_cut_per_dir = (static_cast(cao) / 2.) * + Utils::hadamard_division(box_geo.length(), mesh); + auto const k_cut = *boost::min_element(k_cut_per_dir); + auto const min_box_l = *boost::min_element(box_geo.length()); + auto const min_local_box_l = *boost::min_element(local_geo.length()); + auto const k_cut_max = std::min(min_box_l, min_local_box_l) - skin; + + if (cao >= *boost::min_element(mesh) or k_cut >= k_cut_max) { + m_logger->log_cao_too_large(mesh[0], cao); + return -P3M_TUNE_CAO_TOO_LARGE; + } + + std::tie(tuned_accuracy, rs_err, ks_err, tuned_alpha_L) = + calculate_accuracy(mesh, cao, r_cut_iL_max); + + /* Either low and high boundary are equal (for fixed cut), or the low border + is initially 0 and therefore + has infinite error estimate, as required. Therefore if the high boundary + fails, there is no possible r_cut */ + if (tuned_accuracy > target_accuracy) { + m_logger->log_skip("accuracy not achieved", mesh[0], cao, r_cut_iL_max, + tuned_alpha_L, tuned_accuracy, rs_err, ks_err); + return -P3M_TUNE_ACCURACY_TOO_LARGE; + } + + double r_cut_iL, accuracy; + for (;;) { + r_cut_iL = 0.5 * (r_cut_iL_min + r_cut_iL_max); + + if (r_cut_iL_max - r_cut_iL_min < P3M_RCUT_PREC) + break; + + /* bisection */ + std::tie(accuracy, rs_err, ks_err, tuned_alpha_L) = + calculate_accuracy(mesh, cao, r_cut_iL); + if (accuracy > target_accuracy) + r_cut_iL_min = r_cut_iL; + else + r_cut_iL_max = r_cut_iL; + } + + /* final result is always the upper interval boundary, since only there + * we know that the desired minimal accuracy is obtained */ + tuned_r_cut_iL = r_cut_iL = r_cut_iL_max; + + /* if we are running P3M+ELC, check that r_cut is compatible */ + auto const r_cut = r_cut_iL * box_geo.length()[0]; + auto const veto = layer_correction_veto_r_cut(r_cut); + if (veto) { + m_logger->log_skip(*veto, mesh[0], cao, r_cut_iL, tuned_alpha_L, + tuned_accuracy, rs_err, ks_err); + return -P3M_TUNE_ELC_GAP_SIZE; + } + + commit(mesh, cao, r_cut_iL, tuned_alpha_L); + on_solver_change(); + auto const int_time = benchmark_integration_step(m_timings); + + std::tie(tuned_accuracy, rs_err, ks_err, tuned_alpha_L) = + calculate_accuracy(mesh, cao, r_cut_iL); + + m_logger->log_success(int_time, mesh[0], cao, r_cut_iL, tuned_alpha_L, + tuned_accuracy, rs_err, ks_err); + increment_n_trials(); + return int_time; +} + +/** + * @brief Get the optimal alpha and the corresponding computation time + * for a fixed @p mesh. + * + * @p _cao should contain an initial guess, which is then adapted by stepping + * up and down. + * + * @param[in] mesh @copybrief P3MParameters::mesh + * @param[in,out] tuned_cao initial guess for the + * @copybrief P3MParameters::cao + * @param[out] tuned_r_cut_iL @copybrief P3MParameters::r_cut_iL + * @param[out] tuned_alpha_L @copybrief P3MParameters::alpha_L + * @param[out] tuned_accuracy @copybrief P3MParameters::accuracy + * + * @returns The integration time in case of success, otherwise + * -@ref P3M_TUNE_CAO_TOO_LARGE + */ +double TuningAlgorithm::get_m_time(Utils::Vector3i const &mesh, int &tuned_cao, + double &tuned_r_cut_iL, + double &tuned_alpha_L, + double &tuned_accuracy) { + double best_time = -1., tmp_r_cut_iL = 0., tmp_alpha_L = 0., + tmp_accuracy = 0.; + /* in which direction improvement is possible. Initially, we don't know it + * yet. */ + int final_dir = 0; + int cao = tuned_cao; + + /* the initial step sets a timing mark. If there is no valid r_cut, we can + * only try to increase cao to increase the obtainable precision of the far + * formula. */ + double tmp_time; + do { + tmp_time = get_mc_time(mesh, cao, tmp_r_cut_iL, tmp_alpha_L, tmp_accuracy); + /* cao is too large for this grid, but still the accuracy cannot be + * achieved, give up */ + if (tmp_time == -P3M_TUNE_CAO_TOO_LARGE) { + return tmp_time; + } + /* we have a valid time, start optimising from there */ + if (tmp_time >= 0.) { + best_time = tmp_time; + tuned_r_cut_iL = tmp_r_cut_iL; + tuned_alpha_L = tmp_alpha_L; + tuned_accuracy = tmp_accuracy; + tuned_cao = cao; + break; + } + /* the required accuracy could not be obtained, try higher caos */ + cao++; + final_dir = 1; + } while (cao <= cao_max); + /* with this mesh, the required accuracy cannot be obtained. */ + if (cao > cao_max) + return -P3M_TUNE_CAO_TOO_LARGE; + + /* at the boundaries, only the opposite direction can be used for + * optimisation + */ + if (cao == cao_min) + final_dir = 1; + else if (cao == cao_max) + final_dir = -1; + + if (final_dir == 0) { + /* check in which direction we can optimise. Both directions are possible */ + double dir_times[3]; + for (final_dir = -1; final_dir <= 1; final_dir += 2) { + dir_times[final_dir + 1] = tmp_time = get_mc_time( + mesh, cao + final_dir, tmp_r_cut_iL, tmp_alpha_L, tmp_accuracy); + /* in this direction, we cannot optimise, since we get into precision + * trouble */ + if (tmp_time < 0.) + continue; + + if (tmp_time < best_time) { + best_time = tmp_time; + tuned_r_cut_iL = tmp_r_cut_iL; + tuned_alpha_L = tmp_alpha_L; + tuned_accuracy = tmp_accuracy; + tuned_cao = cao + final_dir; + } + } + /* choose the direction which was optimal, if any of the two */ + if (dir_times[0] == best_time) { + final_dir = -1; + } else if (dir_times[2] == best_time) { + final_dir = 1; + } else { + /* no improvement in either direction, however if one is only marginally + * worse, we can still try; down is possible and not much worse, while + * up is either illegal or even worse */ + if ((dir_times[0] >= 0 && dir_times[0] < best_time + time_granularity) && + (dir_times[2] < 0 || dir_times[2] > dir_times[0])) + final_dir = -1; + /* same for up */ + else if ((dir_times[2] >= 0 && + dir_times[2] < best_time + time_granularity) && + (dir_times[0] < 0 || dir_times[0] > dir_times[2])) + final_dir = 1; + else { + /* really no chance for optimisation */ + return best_time; + } + } + /* we already checked the initial cao and its neighbor */ + cao += 2 * final_dir; + } else { + /* here some constraint is active, and we only checked the initial cao + * itself */ + cao += final_dir; + } + + /* move cao into the optimisation direction until we do not gain anymore. */ + for (; cao >= cao_min && cao <= cao_max; cao += final_dir) { + tmp_time = get_mc_time(mesh, cao, tmp_r_cut_iL, tmp_alpha_L, tmp_accuracy); + /* if we cannot meet the precision anymore, give up */ + if (tmp_time < 0.) + break; + + if (tmp_time < best_time) { + best_time = tmp_time; + tuned_r_cut_iL = tmp_r_cut_iL; + tuned_alpha_L = tmp_alpha_L; + tuned_accuracy = tmp_accuracy; + tuned_cao = cao; + } else if (tmp_time > best_time + time_granularity) { + /* no hope of further optimisation */ + break; + } + } + return best_time; +} + +#endif // P3M or DP3M diff --git a/src/core/p3m/TuningAlgorithm.hpp b/src/core/p3m/TuningAlgorithm.hpp new file mode 100644 index 00000000000..dae20fc22a4 --- /dev/null +++ b/src/core/p3m/TuningAlgorithm.hpp @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_P3M_TUNING_ALGORITHM_HPP +#define ESPRESSO_SRC_CORE_P3M_TUNING_ALGORITHM_HPP + +#include "config.hpp" + +#if defined(P3M) || defined(DP3M) + +#include "p3m/TuningLogger.hpp" +#include "p3m/common.hpp" + +#include + +#include + +#include +#include +#include +#include +#include + +/** + * @brief Tuning algorithm for P3M. + * + * The algorithm basically determines the mesh, cao + * and then the real-space cutoff, in this order. + * + * For each mesh, the optimal cao for the previous mesh is re-used as an + * initial guess, and the algorithm checks whether increasing or decreasing + * it leads to a better solution. This is efficient, since the optimal cao + * only changes little with the meshes in general. + * + * The real-space cutoff for a given mesh and cao is determined via a + * bisection on the error estimate, which determines where the error + * estimate equals the required accuracy. Therefore the smallest possible, + * i.e. fastest real-space cutoff is determined. + * + * Both the search over mesh and cao stop to search in a specific + * direction once the computation time is significantly higher + * than the currently known optimum. + */ +class TuningAlgorithm { + int m_timings; + std::size_t m_n_trials; + +protected: + double m_prefactor; + std::unique_ptr m_logger = nullptr; + int cao_min = -1, cao_max = -1, cao_best = -1; + double m_r_cut_iL_min = -1., m_r_cut_iL_max = -1.; + + /** + * @brief Granularity of the time measurement (milliseconds). + * Tuning halts when the runtime is larger than the best time plus this value. + */ + static auto constexpr time_granularity = 2.; + + /** + * @brief Maximal number of consecutive trials that don't improve runtime. + * Tuning halts when this threshold is reached. + */ + static auto constexpr max_n_consecutive_trials = 20; + + /** @brief Value for invalid time measurements. */ + static auto constexpr time_sentinel = std::numeric_limits::max(); + +public: + TuningAlgorithm(double prefactor, int timings) + : m_timings{timings}, m_n_trials{0ul}, m_prefactor{prefactor} {} + virtual ~TuningAlgorithm() = default; + + struct Parameters { + Utils::Vector3i mesh = {}; + int cao = -1; + double alpha_L = -1.; + double r_cut_iL = -1.; + double accuracy = -1.; + double time = std::numeric_limits::max(); + }; + + /** @brief Get the P3M parameters. */ + virtual P3MParameters &get_params() = 0; + + /** @brief Re-initialize the currently active solver. */ + virtual void on_solver_change() const = 0; + + /** @brief Tuning loop entry point. */ + virtual TuningAlgorithm::Parameters get_time() = 0; + + /** @brief Configure the logger. */ + virtual void setup_logger(bool verbose) = 0; + + /** @brief Determine a sensible range for the mesh. */ + virtual void determine_mesh_limits() = 0; + + /** @brief Determine a sensible range for the real-space cutoff. */ + void determine_r_cut_limits(); + + /** @brief Determine a sensible range for the charge assignment order. */ + void determine_cao_limits(int initial_cao); + + /** + * @brief Get the minimal error for this combination of parameters. + * + * The real-space error is tuned such that it contributes half of the + * total error, and then the k-space error is calculated. + * If an optimal alpha is not found, the value 0.1 is used as fallback. + * @param[in] mesh @copybrief P3MParameters::mesh + * @param[in] cao @copybrief P3MParameters::cao + * @param[in] r_cut_iL @copybrief P3MParameters::r_cut_iL + * @returns Error magnitude, real-space error, k-space error, + * @copybrief P3MParameters::alpha_L + */ + virtual std::tuple + calculate_accuracy(Utils::Vector3i const &mesh, int cao, + double r_cut_iL) const = 0; + + /** @brief Veto real-space cutoffs larger than the layer correction gap. */ + virtual boost::optional + layer_correction_veto_r_cut(double r_cut) const = 0; + + /** @brief Write tuned parameters to the P3M parameter struct. */ + void commit(Utils::Vector3i const &mesh, int cao, double r_cut_iL, + double alpha_L); + + void tune() { + // activate tuning mode + get_params().tuning = true; + + auto const tuned_params = get_time(); + + // deactivate tuning mode + get_params().tuning = false; + + if (tuned_params.time == time_sentinel) { + throw std::runtime_error(m_logger->get_name() + + ": failed to reach requested accuracy"); + } + // set tuned parameters + get_params().accuracy = tuned_params.accuracy; + commit(tuned_params.mesh, tuned_params.cao, tuned_params.r_cut_iL, + tuned_params.alpha_L); + + m_logger->tuning_results(tuned_params.mesh, tuned_params.cao, + tuned_params.r_cut_iL, tuned_params.alpha_L, + tuned_params.accuracy, tuned_params.time); + } + +protected: + auto get_n_trials() { return m_n_trials; } + void increment_n_trials() { ++m_n_trials; } + void reset_n_trials() { m_n_trials = 0ul; } + double get_m_time(Utils::Vector3i const &mesh, int &tuned_cao, + double &tuned_r_cut_iL, double &tuned_alpha_L, + double &tuned_accuracy); + double get_mc_time(Utils::Vector3i const &mesh, int cao, + double &tuned_r_cut_iL, double &tuned_alpha_L, + double &tuned_accuracy); +}; + +#endif // P3M or DP3M + +#endif diff --git a/src/core/p3m/TuningLogger.hpp b/src/core/p3m/TuningLogger.hpp new file mode 100644 index 00000000000..b2b91da1d0b --- /dev/null +++ b/src/core/p3m/TuningLogger.hpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_P3M_TUNING_LOGGER_HPP +#define ESPRESSO_SRC_CORE_P3M_TUNING_LOGGER_HPP + +#include "config.hpp" + +#if defined(P3M) || defined(DP3M) + +#include + +#include +#include + +class TuningLogger { +public: + enum class Mode { Coulomb, Dipolar }; + TuningLogger(bool verbose, std::string name, Mode mode) + : m_verbose{verbose}, m_name{std::move(name)}, m_mode{mode} {} + + void log_tuning_start() const { + if (m_verbose) { + std::printf("mesh cao r_cut_iL alpha_L err " + "rs_err ks_err time [ms]\n"); + } + } + + template + void log_success(double time, Types... parameter_set) const { + if (m_verbose) { + row(parameter_set...); + std::printf(" %-8.2f\n", time); + } + } + + template + void log_skip(std::string reason, Types... parameter_set) const { + if (m_verbose) { + row(parameter_set...); + std::printf(" %s\n", reason.c_str()); + } + } + + void log_cao_too_large(int mesh, int cao) const { + if (m_verbose) { + std::printf("%-4d %-3d cao too large for this mesh\n", mesh, cao); + } + } + + void tuning_goals(double accuracy, double prefactor, double box_l, + int n_particles, double sum_prop) const { + if (m_verbose) { + std::string particle_trait; + std::string particle_property; + switch (m_mode) { + case Mode::Coulomb: + particle_trait = "charged"; + particle_property = "Sum[q_i^2]"; + break; + case Mode::Dipolar: + particle_trait = "magnetic"; + particle_property = "Sum[mu_i^2]"; + break; + } + std::printf("%s tune parameters: Accuracy goal = %.5e prefactor = %.5e\n" + "System: box_l = %.5e # %s part = %d %s = %.5e\n", + m_name.c_str(), accuracy, prefactor, box_l, + particle_trait.c_str(), n_particles, + particle_property.c_str(), sum_prop); + } + } + + void tuning_results(Utils::Vector3i const &mesh, int cao, double r_cut_iL, + double alpha_L, double accuracy, double time) const { + if (m_verbose) { + std::printf( + "\nresulting parameters: mesh: (%d, %d, %d), cao: %d, r_cut_iL: %.4e," + "\n alpha_L: %.4e, accuracy: %.4e, time: %.2f\n", + mesh[0], mesh[1], mesh[2], cao, r_cut_iL, alpha_L, accuracy, time); + } + } + + void report_fixed_cao(int cao) const { + if (m_verbose) { + std::printf("fixed cao %d\n", cao); + } + } + + void report_fixed_r_cut_iL(double r_cut_iL) const { + if (m_verbose) { + std::printf("fixed r_cut_iL %f\n", r_cut_iL); + } + } + + void report_fixed_mesh(Utils::Vector3i const &mesh) const { + if (m_verbose) { + std::printf("fixed mesh (%d, %d, %d)\n", mesh[0], mesh[1], mesh[2]); + } + } + + auto get_name() const { return m_name; } + +private: + bool m_verbose; + std::string m_name; + Mode m_mode; + + void row(int mesh, int cao, double r_cut_iL, double alpha_L, double accuracy, + double rs_err, double ks_err) const { + std::printf("%-4d %-3d %.5e %.5e %.3e %.3e %.3e", mesh, cao, r_cut_iL, + alpha_L, accuracy, rs_err, ks_err); + } +}; + +#endif // P3M or DP3M + +#endif diff --git a/src/core/p3m/common.cpp b/src/core/p3m/common.cpp new file mode 100644 index 00000000000..df928f5c8b7 --- /dev/null +++ b/src/core/p3m/common.cpp @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#if defined(P3M) || defined(DP3M) + +#include "common.hpp" + +#include "LocalBox.hpp" +#include "communication.hpp" +#include "errorhandling.hpp" + +#include +#include +#include + +#include +#include + +double p3m_analytic_cotangent_sum(int n, double mesh_i, int cao) { + auto const c = Utils::sqr(std::cos(Utils::pi() * mesh_i * (double)n)); + auto res = 0.0; + + switch (cao) { + case 1: { + res = 1.0; + break; + } + case 2: { + res = (1.0 + c * 2.0) / 3.0; + break; + } + case 3: { + res = (2.0 + c * (11.0 + c * 2.0)) / 15.0; + break; + } + case 4: { + res = (17.0 + c * (180.0 + c * (114.0 + c * 4.0))) / 315.0; + break; + } + case 5: { + res = (62.0 + c * (1072.0 + c * (1452.0 + c * (247.0 + c * 2.0)))) / 2835.0; + break; + } + case 6: { + res = (1382.0 + + c * (35396.0 + + c * (83021.0 + c * (34096.0 + c * (2026.0 + c * 4.0))))) / + 155925.0; + break; + } + case 7: { + res = + (21844.0 + + c * (776661.0 + c * (2801040.0 + + c * (2123860.0 + + c * (349500.0 + c * (8166.0 + c * 4.0)))))) / + 6081075.0; + break; + } + default: { + fprintf(stderr, + "%d: INTERNAL_ERROR: The value %d for the interpolation order " + "should not occur!\n", + this_node, cao); + errexit(); + } + } + + return res; +} + +void P3MLocalMesh::calc_local_ca_mesh(P3MParameters const ¶ms, + LocalBox const &local_geo, + double skin, double space_layer) { + int i; + int ind[3]; + // total skin size + auto const full_skin = Utils::Vector3d{params.cao_cut} + + Utils::Vector3d::broadcast(skin) + + Utils::Vector3d{0., 0., space_layer}; + // inner left down corner + auto const &inner_ld_pos = local_geo.my_left(); + // inner up right corner + auto const &inner_ur_pos = local_geo.my_right(); + // outer up right corner + auto const outer_ur_pos = inner_ur_pos + full_skin; + // outer left down corner + auto const outer_ld_pos = inner_ld_pos - full_skin; + // convert spatial positions to grid indices + auto const calc_grid_pos = [¶ms](Utils::Vector3d const &pos, int i) { + return pos[i] * params.ai[i] - params.mesh_off[i]; + }; + + /* inner left down grid point (global index) */ + for (i = 0; i < 3; i++) + in_ld[i] = static_cast(std::ceil(calc_grid_pos(inner_ld_pos, i))); + /* inner up right grid point (global index) */ + for (i = 0; i < 3; i++) + in_ur[i] = static_cast(std::floor(calc_grid_pos(inner_ur_pos, i))); + + /* correct roundoff errors at boundary */ + for (i = 0; i < 3; i++) { + if (calc_grid_pos(inner_ur_pos, i) - in_ur[i] < ROUND_ERROR_PREC) + in_ur[i]--; + if (calc_grid_pos(inner_ld_pos, i) - in_ld[i] + 1. < ROUND_ERROR_PREC) + in_ld[i]--; + } + /* inner grid dimensions */ + for (i = 0; i < 3; i++) + inner[i] = in_ur[i] - in_ld[i] + 1; + /* index of left down grid point in global mesh */ + for (i = 0; i < 3; i++) + ld_ind[i] = static_cast(std::ceil(calc_grid_pos(outer_ld_pos, i))); + /* left down margin */ + for (i = 0; i < 3; i++) + margin[i * 2] = in_ld[i] - ld_ind[i]; + /* up right grid point */ + for (i = 0; i < 3; i++) + ind[i] = static_cast(std::floor(calc_grid_pos(outer_ur_pos, i))); + /* correct roundoff errors at up right boundary */ + for (i = 0; i < 3; i++) + if (calc_grid_pos(outer_ur_pos, i) - ind[i] == 0.) + ind[i]--; + /* up right margin */ + for (i = 0; i < 3; i++) + margin[(i * 2) + 1] = ind[i] - in_ur[i]; + + /* grid dimension */ + size = 1; + for (i = 0; i < 3; i++) { + dim[i] = ind[i] - ld_ind[i] + 1; + size *= dim[i]; + } + + /* reduce inner grid indices from global to local */ + for (i = 0; i < 3; i++) + in_ld[i] = margin[i * 2]; + for (i = 0; i < 3; i++) + in_ur[i] = margin[i * 2] + inner[i]; + + q_2_off = dim[2] - params.cao; + q_21_off = dim[2] * (dim[1] - params.cao); +} + +#endif /* defined(P3M) || defined(DP3M) */ diff --git a/src/core/electrostatics_magnetostatics/p3m-common.hpp b/src/core/p3m/common.hpp similarity index 58% rename from src/core/electrostatics_magnetostatics/p3m-common.hpp rename to src/core/p3m/common.hpp index c5642389010..536dd52f3ab 100644 --- a/src/core/electrostatics_magnetostatics/p3m-common.hpp +++ b/src/core/p3m/common.hpp @@ -18,8 +18,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_COMMON_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_COMMON_HPP + /** \file * Common functions for dipolar and charge P3M. * @@ -32,32 +31,25 @@ * @cite deserno98b, @cite deserno00e, @cite deserno00b, @cite cerda08d * */ + +#ifndef ESPRESSO_SRC_CORE_P3M_COMMON_HPP +#define ESPRESSO_SRC_CORE_P3M_COMMON_HPP + #include "config.hpp" -#include -#include -#include +#include + +/** This value indicates metallic boundary conditions. */ +auto constexpr P3M_EPSILON_METALLIC = 0.0; #if defined(P3M) || defined(DP3M) #include "LocalBox.hpp" -#include - -/** Error Codes for p3m tuning (version 2) */ -enum P3M_TUNE_ERROR { - /** force evaluation failed */ - P3M_TUNE_FAIL = 1, - /** could not find a valid realspace cutoff radius */ - P3M_TUNE_NOCUTOFF = 2, - /** charge assignment order too large for mesh size */ - P3M_TUNE_CAO_TOO_LARGE = 4, - /** conflict with ELC gap size */ - P3M_TUNE_ELCTEST = 8, - P3M_TUNE_CUTOFF_TOO_LARGE = 16, - /** could not achieve target accuracy */ - P3M_TUNE_ACCURACY_TOO_LARGE = 32 -}; +#include +#include +#include +#include namespace detail { /** @brief Index helpers for direct and reciprocal space. @@ -70,17 +62,114 @@ enum FFT_WAVE_VECTOR : int { KY = 0, KZ = 1, KX = 2 }; } // namespace FFT_indexing } // namespace detail -/** This value indicates metallic boundary conditions. */ -#define P3M_EPSILON_METALLIC 0.0 +/** Structure to hold P3M parameters and some dependent variables. */ +struct P3MParameters { + /** tuning or production? */ + bool tuning; + /** Ewald splitting parameter (00), rescaled to + * @p r_cut_iL = @p r_cut * @p box_l_i. */ + double r_cut_iL; + /** number of mesh points per coordinate direction (>0). */ + Utils::Vector3i mesh = {}; + /** offset of the first mesh point (lower left corner) from the + * coordinate origin ([0,1[). */ + Utils::Vector3d mesh_off; + /** charge assignment order ([0,7]). */ + int cao; + /** accuracy of the actual parameter set. */ + double accuracy; -/** precision limit for the r_cut zero */ -#define P3M_RCUT_PREC 1e-3 -/** granularity of the time measurement */ -#define P3M_TIME_GRAN 2 + /** epsilon of the "surrounding dielectric". */ + double epsilon; + /** cutoff for charge assignment. */ + Utils::Vector3d cao_cut; + /** mesh constant. */ + Utils::Vector3d a; + /** inverse mesh constant. */ + Utils::Vector3d ai; + /** unscaled @ref P3MParameters::alpha_L "alpha_L" for use with fast + * inline functions only */ + double alpha; + /** unscaled @ref P3MParameters::r_cut_iL "r_cut_iL" for use with fast + * inline functions only */ + double r_cut; + /** number of points unto which a single charge is interpolated, i.e. + * @ref P3MParameters::cao "cao" cubed */ + int cao3; + + P3MParameters(bool tuning, double epsilon, double r_cut, + Utils::Vector3i const &mesh, Utils::Vector3d const &mesh_off, + int cao, double alpha, double accuracy) + : tuning{tuning}, alpha_L{0.}, r_cut_iL{0.}, mesh{mesh}, + mesh_off{mesh_off}, cao{cao}, accuracy{accuracy}, epsilon{epsilon}, + cao_cut{}, a{}, ai{}, alpha{alpha}, r_cut{r_cut}, cao3{-1} { + + auto constexpr value_to_tune = -1.; + + if (epsilon < 0.) { + throw std::domain_error("Parameter 'epsilon' must be >= 0"); + } -/************************************************ - * data types - ************************************************/ + if (accuracy <= 0.) { + throw std::domain_error("Parameter 'accuracy' must be > 0"); + } + + if (r_cut <= 0.) { + if (tuning and r_cut == value_to_tune) { + this->r_cut = 0.; + } else { + throw std::domain_error("Parameter 'r_cut' must be > 0"); + } + } + + if (alpha <= 0.) { + if (tuning and alpha == value_to_tune) { + this->alpha = 0.; + } else { + throw std::domain_error("Parameter 'alpha' must be > 0"); + } + } + + if (not(mesh >= Utils::Vector3i::broadcast(1) or + ((mesh[0] >= 1) and + (mesh == Utils::Vector3i{{mesh[0], -1, -1}}))) and + not(tuning and mesh == Utils::Vector3i::broadcast(-1))) { + throw std::domain_error("Parameter 'mesh' must be > 0"); + } + + if (not(mesh_off >= Utils::Vector3d::broadcast(0.) and + mesh_off <= Utils::Vector3d::broadcast(1.))) { + if (mesh_off == Utils::Vector3d::broadcast(-1.)) { + this->mesh_off = Utils::Vector3d::broadcast(P3M_MESHOFF); + } else { + throw std::domain_error("Parameter 'mesh_off' must be >= 0 and <= 1"); + } + } + + if ((cao < 1 or cao > 7) and not(tuning and cao == -1)) { + throw std::domain_error("Parameter 'cao' must be >= 1 and <= 7"); + } + + if (not tuning and (Utils::Vector3i::broadcast(cao) > mesh)) { + throw std::domain_error("Parameter 'cao' cannot be larger than 'mesh'"); + } + } + + /** + * @brief Recalculate quantities derived from the mesh and box length: + * @ref P3MParameters::a "a", + * @ref P3MParameters::ai "ai" and + * @ref P3MParameters::cao_cut "cao_cut". + */ + void recalc_a_ai_cao_cut(Utils::Vector3d const &box_l) { + ai = Utils::hadamard_division(mesh, box_l); + a = Utils::hadamard_division(Utils::Vector3d::broadcast(1.), ai); + cao_cut = (static_cast(cao) / 2.) * a; + } +}; /** Structure for local mesh parameters. */ struct P3MLocalMesh { @@ -108,66 +197,28 @@ struct P3MLocalMesh { int q_2_off; /** offset between mesh lines of the two last dimensions */ int q_21_off; -}; -/** Structure to hold P3M parameters and some dependent variables. */ -struct P3MParameters { - /** tuning or production? */ - bool tuning = false; - /** Ewald splitting parameter (00), rescaled to - * @p r_cut_iL = @p r_cut * @p box_l_i. */ - double r_cut_iL = 0.0; - /** number of mesh points per coordinate direction (>0). */ - int mesh[3] = {}; - /** offset of the first mesh point (lower left corner) from the - * coordinate origin ([0,1[). */ - double mesh_off[3] = {P3M_MESHOFF, P3M_MESHOFF, P3M_MESHOFF}; - /** charge assignment order ([0,7]). */ - int cao = 0; - /** accuracy of the actual parameter set. */ - double accuracy = 0.0; - - /** epsilon of the "surrounding dielectric". */ - double epsilon = P3M_EPSILON_METALLIC; - /** cutoff for charge assignment. */ - double cao_cut[3] = {}; - /** mesh constant. */ - double a[3] = {}; - /** inverse mesh constant. */ - Utils::Vector3d ai = {}; - /** unscaled @ref P3MParameters::alpha_L "alpha_L" for use with fast - * inline functions only */ - double alpha = 0.0; - /** unscaled @ref P3MParameters::r_cut_iL "r_cut_iL" for use with fast - * inline functions only */ - double r_cut = -1.; - /** number of points unto which a single charge is interpolated, i.e. - * p3m.cao^3 */ - int cao3 = 0; - - template void serialize(Archive &ar, long int) { - ar &tuning &alpha_L &r_cut_iL &mesh; - ar &mesh_off &cao &accuracy &epsilon &cao_cut; - ar &a &ai &alpha &r_cut &cao3; + /** + * @brief Recalculate quantities derived from the mesh and box length: + * @ref P3MLocalMesh::ld_pos "ld_pos" (position of the left down mesh). + */ + void recalc_ld_pos(P3MParameters const ¶ms) { + // spatial position of left down mesh point + for (int i = 0; i < 3; i++) { + ld_pos[i] = (ld_ind[i] + params.mesh_off[i]) * params.a[i]; + } } -}; -/** Add values of a 3d-grid input block (size[3]) to values of 3d-grid - * output array with dimension dim[3] at start position start[3]. - * - * \param in Pointer to first element of input block data. - * \param out Pointer to first element of output grid. - * \param start Start position of block in output grid. - * \param size Dimensions of the block - * \param dim Dimensions of the output grid. - */ -void p3m_add_block(double const *in, double *out, int const start[3], - int const size[3], int const dim[3]); + /** + * @brief Calculate properties of the local FFT mesh + * for the charge assignment process. + */ + void calc_local_ca_mesh(P3MParameters const ¶ms, + LocalBox const &local_geo, double skin, + double space_layer); +}; -/** One of the aliasing sums used by \ref p3m_k_space_error. +/** One of the aliasing sums used to compute k-space errors. * Fortunately the one which is most important (because it converges * most slowly, since it is not damped exponentially) can be * calculated analytically. The result (which depends on the order of @@ -177,23 +228,6 @@ void p3m_add_block(double const *in, double *out, int const start[3], */ double p3m_analytic_cotangent_sum(int n, double mesh_i, int cao); -/** Calculate properties of the local FFT mesh for the - * charge assignment process. - */ -void p3m_calc_local_ca_mesh(P3MLocalMesh &local_mesh, - const P3MParameters ¶ms, - const LocalBox &local_geo, double skin, - double space_layer); - -/** Calculate the spatial position of the left down mesh - * point of the local mesh, to be stored in - * @ref P3MLocalMesh::ld_pos "ld_pos". - * - * Function called by @ref p3m_calc_local_ca_mesh() once and by - * @ref p3m_scaleby_box_l() whenever the box size changes. - */ -void p3m_calc_lm_ld_pos(P3MLocalMesh &local_mesh, const P3MParameters ¶ms); - #endif /* P3M || DP3M */ namespace detail { @@ -205,7 +239,7 @@ namespace detail { * \ldots, -1\right) @f$. */ std::array, 3> inline calc_meshift( - int const mesh_size[3], bool zero_out_midpoint = false) { + Utils::Vector3i const &mesh_size, bool zero_out_midpoint = false) { std::array, 3> ret{}; for (std::size_t i = 0; i < 3; i++) { @@ -223,4 +257,4 @@ std::array, 3> inline calc_meshift( } } // namespace detail -#endif /* CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_COMMON_HPP */ +#endif diff --git a/src/core/electrostatics_magnetostatics/p3m-data_struct.hpp b/src/core/p3m/data_struct.hpp similarity index 89% rename from src/core/electrostatics_magnetostatics/p3m-data_struct.hpp rename to src/core/p3m/data_struct.hpp index 1840660a2de..5038a4483ed 100644 --- a/src/core/electrostatics_magnetostatics/p3m-data_struct.hpp +++ b/src/core/p3m/data_struct.hpp @@ -18,19 +18,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef P3M_DATA_STRUCT_HPP -#define P3M_DATA_STRUCT_HPP +#ifndef CORE_P3M_DATA_STRUCT_HPP +#define CORE_P3M_DATA_STRUCT_HPP #include "config.hpp" #if defined(P3M) || defined(DP3M) -#include "p3m-common.hpp" +#include "common.hpp" #include #include struct p3m_data_struct_base { + explicit p3m_data_struct_base(P3MParameters &¶meters) + : params{std::move(parameters)}, ks_pnum{0} {} + P3MParameters params; /** Spatial differential operator in k-space. We use an i*k differentiation. diff --git a/src/core/electrostatics_magnetostatics/fft.cpp b/src/core/p3m/fft.cpp similarity index 97% rename from src/core/electrostatics_magnetostatics/fft.cpp rename to src/core/p3m/fft.cpp index 9d386502113..ceabc7736b9 100644 --- a/src/core/electrostatics_magnetostatics/fft.cpp +++ b/src/core/p3m/fft.cpp @@ -29,7 +29,7 @@ #if defined(P3M) || defined(DP3M) -#include "fft.hpp" +#include "p3m/fft.hpp" #include #include @@ -480,10 +480,11 @@ void calc_2d_grid(int n, int grid[3]) { } } // namespace -int fft_init(const Utils::Vector3i &ca_mesh_dim, int const *ca_mesh_margin, - int const *global_mesh_dim, double const *global_mesh_off, - int &ks_pnum, fft_data_struct &fft, const Utils::Vector3i &grid, - const boost::mpi::communicator &comm) { +int fft_init(Utils::Vector3i const &ca_mesh_dim, int const *ca_mesh_margin, + Utils::Vector3i const &global_mesh_dim, + Utils::Vector3d const &global_mesh_off, int &ks_pnum, + fft_data_struct &fft, Utils::Vector3i const &grid, + boost::mpi::communicator const &comm) { int n_grid[4][3]; /* The four node grids. */ int my_pos[4][3]; /* The position of comm.rank() in the node grids. */ @@ -561,9 +562,9 @@ int fft_init(const Utils::Vector3i &ca_mesh_dim, int const *ca_mesh_margin, fft.plan[i].recv_block.resize(6 * fft.plan[i].group.size()); fft.plan[i].recv_size.resize(fft.plan[i].group.size()); - fft.plan[i].new_size = - calc_local_mesh(my_pos[i], n_grid[i], global_mesh_dim, global_mesh_off, - fft.plan[i].new_mesh, fft.plan[i].start); + fft.plan[i].new_size = calc_local_mesh( + my_pos[i], n_grid[i], global_mesh_dim.data(), global_mesh_off.data(), + fft.plan[i].new_mesh, fft.plan[i].start); permute_ifield(fft.plan[i].new_mesh, 3, -(fft.plan[i].n_permute)); permute_ifield(fft.plan[i].start, 3, -(fft.plan[i].n_permute)); fft.plan[i].n_ffts = fft.plan[i].new_mesh[0] * fft.plan[i].new_mesh[1]; @@ -574,7 +575,8 @@ int fft_init(const Utils::Vector3i &ca_mesh_dim, int const *ca_mesh_margin, int node = fft.plan[i].group[j]; fft.plan[i].send_size[j] = calc_send_block( my_pos[i - 1], n_grid[i - 1], &(n_pos[i][3 * node]), n_grid[i], - global_mesh_dim, global_mesh_off, &(fft.plan[i].send_block[6 * j])); + global_mesh_dim.data(), global_mesh_off.data(), + &(fft.plan[i].send_block[6 * j])); permute_ifield(&(fft.plan[i].send_block[6 * j]), 3, -(fft.plan[i - 1].n_permute)); permute_ifield(&(fft.plan[i].send_block[6 * j + 3]), 3, @@ -591,7 +593,8 @@ int fft_init(const Utils::Vector3i &ca_mesh_dim, int const *ca_mesh_margin, /* recv block: comm.rank() from comm-group-node i (identity: node) */ fft.plan[i].recv_size[j] = calc_send_block( my_pos[i], n_grid[i], &(n_pos[i - 1][3 * node]), n_grid[i - 1], - global_mesh_dim, global_mesh_off, &(fft.plan[i].recv_block[6 * j])); + global_mesh_dim.data(), global_mesh_off.data(), + &(fft.plan[i].recv_block[6 * j])); permute_ifield(&(fft.plan[i].recv_block[6 * j]), 3, -(fft.plan[i].n_permute)); permute_ifield(&(fft.plan[i].recv_block[6 * j + 3]), 3, diff --git a/src/core/electrostatics_magnetostatics/fft.hpp b/src/core/p3m/fft.hpp similarity index 95% rename from src/core/electrostatics_magnetostatics/fft.hpp rename to src/core/p3m/fft.hpp index 86d7cc7e1a5..ede0cb4ef8c 100644 --- a/src/core/electrostatics_magnetostatics/fft.hpp +++ b/src/core/p3m/fft.hpp @@ -18,12 +18,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_FFT_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_FFT_HPP +#ifndef CORE_P3M_FFT_HPP +#define CORE_P3M_FFT_HPP /** \file * * Routines, row decomposition, data structures and communication for the - * 3D-FFT. + * 3D-FFT. * * The 3D-FFT is split into 3 ond dimensional FFTs. The data is * distributed in such a way, that for the actual direction of the @@ -43,14 +43,15 @@ */ #include "config.hpp" + #if defined(P3M) || defined(DP3M) #include #include + #include -#include #include #include #include @@ -148,7 +149,7 @@ struct fft_back_plan { * node grids, the index 0 is used for the real space charge assignment * grid). */ -struct fft_data_struct { +struct fft_data_struct { // NOLINT(bugprone-reserved-identifier) /** Information for forward FFTs. */ fft_forw_plan plan[4]; /** Information for backward FFTs. */ @@ -183,10 +184,11 @@ struct fft_data_struct { * \param[in] comm MPI communicator. * \return Maximal size of local fft mesh (needed for allocation of ca_mesh). */ -int fft_init(const Utils::Vector3i &ca_mesh_dim, int const *ca_mesh_margin, - int const *global_mesh_dim, double const *global_mesh_off, - int &ks_pnum, fft_data_struct &fft, const Utils::Vector3i &grid, - const boost::mpi::communicator &comm); +int fft_init(Utils::Vector3i const &ca_mesh_dim, int const *ca_mesh_margin, + Utils::Vector3i const &global_mesh_dim, + Utils::Vector3d const &global_mesh_off, int &ks_pnum, + fft_data_struct &fft, Utils::Vector3i const &grid, + boost::mpi::communicator const &comm); /** Perform an in-place forward 3D FFT. * \warning The content of \a data is overwritten. diff --git a/src/core/electrostatics_magnetostatics/p3m_influence_function.hpp b/src/core/p3m/influence_function.hpp similarity index 76% rename from src/core/electrostatics_magnetostatics/p3m_influence_function.hpp rename to src/core/p3m/influence_function.hpp index da1eb763d6a..30a22df1434 100644 --- a/src/core/electrostatics_magnetostatics/p3m_influence_function.hpp +++ b/src/core/p3m/influence_function.hpp @@ -19,7 +19,7 @@ #ifndef ESPRESSO_P3M_INFLUENCE_FUNCTION_HPP #define ESPRESSO_P3M_INFLUENCE_FUNCTION_HPP -#include "electrostatics_magnetostatics/p3m-common.hpp" +#include "p3m/common.hpp" #include #include @@ -36,25 +36,36 @@ #include #include -namespace detail { -template T g_ewald(T alpha, T k2) { - auto constexpr limit = T{30}; - auto const exponent = Utils::sqr(1. / (2. * alpha)) * k2; - return (exponent < limit) ? std::exp(-exponent) * (4. * Utils::pi() / k2) - : 0.0; -} - +/** + * @brief Hockney/Eastwood/Ballenegger optimal influence function. + * + * This implements Eq. 30 of @cite cerda08d, which can be used + * for monopole and dipole P3M by choosing the appropriate S factor. + * + * @tparam S Order of the differential operator, e.g. 0 for potential, + * 1 for electric field, ... + * @tparam m Number of aliasing terms to take into account. + * + * @param cao Charge assignment order. + * @param alpha Ewald splitting parameter. + * @param k k Vector to evaluate the function for. + * @param h Grid spacing. + */ template -std::pair aliasing_sums_ik(std::size_t cao, double alpha, - const Utils::Vector3d &k, - const Utils::Vector3d &h) { +double G_opt(int cao, double alpha, Utils::Vector3d const &k, + Utils::Vector3d const &h) { using namespace detail::FFT_indexing; using Utils::int_pow; using Utils::sinc; - using Utils::Vector3d; - constexpr double two_pi = 2 * Utils::pi(); - constexpr double two_pi_i = 1 / two_pi; + auto constexpr two_pi = 2. * Utils::pi(); + auto constexpr two_pi_i = 1. / two_pi; + auto constexpr limit = 30.; + + auto const k2 = k.norm2(); + if (k2 == 0.0) { + return 0.0; + } double numerator = 0.0; double denominator = 0.0; @@ -63,50 +74,24 @@ std::pair aliasing_sums_ik(std::size_t cao, double alpha, for (int my = -m; my <= m; my++) { for (int mz = -m; mz <= m; mz++) { auto const km = - k + two_pi * Vector3d{mx / h[RX], my / h[RY], mz / h[RZ]}; + k + two_pi * Utils::Vector3d{mx / h[RX], my / h[RY], mz / h[RZ]}; auto const U2 = std::pow(sinc(km[RX] * h[RX] * two_pi_i) * sinc(km[RY] * h[RY] * two_pi_i) * sinc(km[RZ] * h[RZ] * two_pi_i), 2 * cao); - numerator += U2 * g_ewald(alpha, km.norm2()) * int_pow(k * km); + auto const km2 = km.norm2(); + auto const exponent = Utils::sqr(1. / (2. * alpha)) * km2; + if (exponent < limit) { + auto const f3 = std::exp(-exponent) * (4. * Utils::pi() / km2); + numerator += U2 * f3 * int_pow(k * km); + } denominator += U2; } } } - return {numerator, denominator}; -} -} // namespace detail - -/** - * @brief Hockney/Eastwood/Ballenegger optimal influence function. - * - * This implements Eq. 30 of @cite cerda08d, which can be used - * for monopole and dipole P3M by choosing the appropriate S factor. - * - * @tparam S Order of the differential operator, e.g. 0 for potential, - * 1 for electric field, ... - * @tparam m Number of aliasing terms to take into account. - * @tparam T Floating-point type. - * - * @param cao Charge assignment order. - * @param alpha Ewald splitting parameter. - * @param k k Vector to evaluate the function for. - * @param h Grid spacing. - */ -template -double G_opt(std::size_t cao, T alpha, const Utils::Vector3 &k, - const Utils::Vector3 &h) { - using Utils::int_pow; - using Utils::sqr; - - auto const k2 = k.norm2(); - if (k2 == 0.0) { - return 0.0; - } - const auto as = detail::aliasing_sums_ik(cao, alpha, k, h); - return as.first / (int_pow(k2) * sqr(as.second)); + return numerator / (int_pow(k2) * Utils::sqr(denominator)); } /** @@ -173,4 +158,4 @@ std::vector grid_influence_function(const P3MParameters ¶ms, return g; } -#endif // ESPRESSO_P3M_INFLUENCE_FUNCTION_HPP +#endif diff --git a/src/core/electrostatics_magnetostatics/dp3m_influence_function.hpp b/src/core/p3m/influence_function_dipolar.hpp similarity index 92% rename from src/core/electrostatics_magnetostatics/dp3m_influence_function.hpp rename to src/core/p3m/influence_function_dipolar.hpp index 9407f27924c..41b9ee198a1 100644 --- a/src/core/electrostatics_magnetostatics/dp3m_influence_function.hpp +++ b/src/core/p3m/influence_function_dipolar.hpp @@ -18,10 +18,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef ESPRESSO_DP3M_INFLUENCE_FUNCTION_HPP -#define ESPRESSO_DP3M_INFLUENCE_FUNCTION_HPP +#ifndef ESPRESSO_CORE_P3M_INFLUENCE_FUNCTION_DIPOLAR_HPP +#define ESPRESSO_CORE_P3M_INFLUENCE_FUNCTION_DIPOLAR_HPP -#include "electrostatics_magnetostatics/p3m-common.hpp" +#include "config.hpp" + +#if defined(DP3M) + +#include "p3m/common.hpp" #include #include @@ -37,8 +41,6 @@ #include #include -#if defined(DP3M) - /** Calculate the aliasing sums for the optimal influence function. * * Calculates the aliasing sums in the numerator and denominator of @@ -56,10 +58,10 @@ double G_opt_dipolar(P3MParameters const ¶ms, Utils::Vector3i const &shift, Utils::Vector3i const &d_op) { using Utils::int_pow; using Utils::sinc; - constexpr double limit = 30; + auto constexpr limit = 30.; - double numerator = 0.0; - double denominator = 0.0; + auto numerator = 0.0; + auto denominator = 0.0; auto const f1 = 1.0 / static_cast(params.mesh[0]); auto const f2 = Utils::sqr(Utils::pi() / params.alpha_L); @@ -151,23 +153,24 @@ std::vector grid_influence_function(P3MParameters const ¶ms, return g; } -double G_opt_dipolar_self_energy(P3MParameters const ¶ms, - Utils::Vector3i const &shift) { +inline double G_opt_dipolar_self_energy(P3MParameters const ¶ms, + Utils::Vector3i const &shift) { using Utils::sinc; double u_sum = 0.0; - constexpr int limit = P3M_BRILLOUIN + 5; + constexpr int limit = P3M_BRILLOUIN + 1; + auto const exponent = 2. * params.cao; auto const f1 = 1.0 / static_cast(params.mesh[0]); for (double mx = -limit; mx <= limit; mx++) { auto const nmx = shift[0] + params.mesh[0] * mx; - auto const sx = std::pow(sinc(f1 * nmx), 2.0 * params.cao); + auto const sx = std::pow(sinc(f1 * nmx), exponent); for (double my = -limit; my <= limit; my++) { auto const nmy = shift[1] + params.mesh[0] * my; - auto const sy = sx * std::pow(sinc(f1 * nmy), 2.0 * params.cao); + auto const sy = sx * std::pow(sinc(f1 * nmy), exponent); for (double mz = -limit; mz <= limit; mz++) { auto const nmz = shift[2] + params.mesh[0] * mz; - auto const sz = sy * std::pow(sinc(f1 * nmz), 2.0 * params.cao); + auto const sz = sy * std::pow(sinc(f1 * nmz), exponent); u_sum += sz; } } diff --git a/src/core/electrostatics_magnetostatics/p3m_interpolation.hpp b/src/core/p3m/interpolation.hpp similarity index 98% rename from src/core/electrostatics_magnetostatics/p3m_interpolation.hpp rename to src/core/p3m/interpolation.hpp index f444a9cc58d..21d48c50a67 100644 --- a/src/core/electrostatics_magnetostatics/p3m_interpolation.hpp +++ b/src/core/p3m/interpolation.hpp @@ -18,8 +18,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef ESPRESSO_P3M_INTERPOLATION_HPP -#define ESPRESSO_P3M_INTERPOLATION_HPP +#ifndef ESPRESSO_CORE_P3M_INTERPOLATION_HPP +#define ESPRESSO_CORE_P3M_INTERPOLATION_HPP #include #include @@ -207,4 +207,4 @@ void p3m_interpolate(P3MLocalMesh const &local_mesh, } } -#endif // ESPRESSO_P3M_INTERPOLATION_HPP +#endif diff --git a/src/core/electrostatics_magnetostatics/p3m_send_mesh.cpp b/src/core/p3m/send_mesh.cpp similarity index 82% rename from src/core/electrostatics_magnetostatics/p3m_send_mesh.cpp rename to src/core/p3m/send_mesh.cpp index 495b3b1f59d..7b9d50dc870 100644 --- a/src/core/electrostatics_magnetostatics/p3m_send_mesh.cpp +++ b/src/core/p3m/send_mesh.cpp @@ -18,20 +18,57 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#include "p3m_send_mesh.hpp" +#include "config.hpp" #if defined(P3M) || defined(DP3M) -#include "fft.hpp" -#include "p3m-common.hpp" +#include "p3m/common.hpp" +#include "p3m/fft.hpp" +#include "p3m/send_mesh.hpp" #include #include #include +#include + #include #include +#include + +/** Add values of a 3d-grid input block (size[3]) to values of 3d-grid + * output array with dimension dim[3] at start position start[3]. + * + * \param in Pointer to first element of input block data. + * \param out Pointer to first element of output grid. + * \param start Start position of block in output grid. + * \param size Dimensions of the block + * \param dim Dimensions of the output grid. + */ +static void p3m_add_block(double const *in, double *out, int const start[3], + int const size[3], int const dim[3]) { + /* fast, mid and slow changing indices */ + int f, m, s; + /* linear index of in grid, linear index of out grid */ + int li_in = 0, li_out = 0; + /* offsets for indices in output grid */ + int m_out_offset, s_out_offset; + + li_out = start[2] + (dim[2] * (start[1] + (dim[1] * start[0]))); + m_out_offset = dim[2] - size[2]; + s_out_offset = (dim[2] * (dim[1] - size[1])); + + for (s = 0; s < size[0]; s++) { + for (m = 0; m < size[1]; m++) { + for (f = 0; f < size[2]; f++) { + out[li_out++] += in[li_in++]; + } + li_out += m_out_offset; + } + li_out += s_out_offset; + } +} void p3m_send_mesh::resize(const boost::mpi::communicator &comm, const P3MLocalMesh &local_mesh) { diff --git a/src/core/electrostatics_magnetostatics/p3m_send_mesh.hpp b/src/core/p3m/send_mesh.hpp similarity index 95% rename from src/core/electrostatics_magnetostatics/p3m_send_mesh.hpp rename to src/core/p3m/send_mesh.hpp index bf100e93d86..6620a7bd389 100644 --- a/src/core/electrostatics_magnetostatics/p3m_send_mesh.hpp +++ b/src/core/p3m/send_mesh.hpp @@ -18,14 +18,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef ESPRESSO_P3M_SEND_MESH_HPP -#define ESPRESSO_P3M_SEND_MESH_HPP +#ifndef ESPRESSO_CORE_P3M_SEND_MESH_HPP +#define ESPRESSO_CORE_P3M_SEND_MESH_HPP #include "config.hpp" #if defined(P3M) || defined(DP3M) -#include "p3m-common.hpp" +#include "p3m/common.hpp" #include #include @@ -84,4 +84,4 @@ class p3m_send_mesh { } }; #endif -#endif // ESPRESSO_P3M_SEND_MESH_HPP +#endif diff --git a/src/core/electrostatics_magnetostatics/p3m_gpu.cpp b/src/core/pair_criteria/BondCriterion.hpp similarity index 50% rename from src/core/electrostatics_magnetostatics/p3m_gpu.cpp rename to src/core/pair_criteria/BondCriterion.hpp index 763189c2833..aca262ea3ab 100644 --- a/src/core/electrostatics_magnetostatics/p3m_gpu.cpp +++ b/src/core/pair_criteria/BondCriterion.hpp @@ -16,5 +16,27 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#include "electrostatics_magnetostatics/p3m_gpu.hpp" -#include "config.hpp" +#ifndef ESPRESSO_SRC_CORE_PAIR_CRITERIA_BOND_CRITERION_HPP +#define ESPRESSO_SRC_CORE_PAIR_CRITERIA_BOND_CRITERION_HPP + +#include "pair_criteria/PairCriterion.hpp" + +#include "BondList.hpp" + +namespace PairCriteria { +/** @brief True if a bond of given type exists between two particles. */ +class BondCriterion : public PairCriterion { +public: + bool decide(Particle const &p1, Particle const &p2) const override { + return pair_bond_exists_on(p1.bonds(), p2.identity(), m_bond_type) || + pair_bond_exists_on(p2.bonds(), p1.identity(), m_bond_type); + } + int get_bond_type() { return m_bond_type; } + void set_bond_type(int t) { m_bond_type = t; } + +private: + int m_bond_type; +}; +} // namespace PairCriteria + +#endif diff --git a/src/core/electrostatics_magnetostatics/debye_hueckel.cpp b/src/core/pair_criteria/DistanceCriterion.hpp similarity index 52% rename from src/core/electrostatics_magnetostatics/debye_hueckel.cpp rename to src/core/pair_criteria/DistanceCriterion.hpp index 793da55e898..399c533fa6c 100644 --- a/src/core/electrostatics_magnetostatics/debye_hueckel.cpp +++ b/src/core/pair_criteria/DistanceCriterion.hpp @@ -1,7 +1,5 @@ /* * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group * * This file is part of ESPResSo. * @@ -18,29 +16,30 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file - * - * Implementation of \ref debye_hueckel.hpp - */ - -#include "debye_hueckel.hpp" - -#ifdef ELECTROSTATICS -#include "electrostatics_magnetostatics/common.hpp" +#ifndef ESPRESSO_SRC_CORE_PAIR_CRITERIA_DISTANCE_CRITERION_HPP +#define ESPRESSO_SRC_CORE_PAIR_CRITERIA_DISTANCE_CRITERION_HPP -#include +#include "pair_criteria/PairCriterion.hpp" -DebyeHueckelParameters dh_params{}; +#include "grid.hpp" -void dh_set_params(double kappa, double r_cut) { - if (kappa < 0.0) - throw std::domain_error("kappa should be a non-negative number"); - if (r_cut < 0.0) - throw std::domain_error("r_cut should be a non-negative number"); - - dh_params = {r_cut, kappa}; - - mpi_bcast_coulomb_params(); -} +namespace PairCriteria { +/** + * @brief True if two particles are closer than a cut off distance, + * respecting minimum image convention. + */ +class DistanceCriterion : public PairCriterion { +public: + bool decide(const Particle &p1, const Particle &p2) const override { + return box_geo.get_mi_vector(p1.pos(), p2.pos()).norm() <= m_cut_off; + } + double get_cut_off() { return m_cut_off; } + void set_cut_off(double c) { m_cut_off = c; } + +private: + double m_cut_off; +}; + +} // namespace PairCriteria #endif diff --git a/src/core/pair_criteria/EnergyCriterion.hpp b/src/core/pair_criteria/EnergyCriterion.hpp new file mode 100644 index 00000000000..b4506db3c9c --- /dev/null +++ b/src/core/pair_criteria/EnergyCriterion.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_CORE_PAIR_CRITERIA_ENERGY_CRITERION_HPP +#define ESPRESSO_SRC_CORE_PAIR_CRITERIA_ENERGY_CRITERION_HPP + +#include "pair_criteria/PairCriterion.hpp" + +#include "energy_inline.hpp" + +namespace PairCriteria { +/** + * @brief True if the short-range energy is larger than a cutoff value. + */ +class EnergyCriterion : public PairCriterion { +public: + bool decide(Particle const &p1, Particle const &p2) const override { + // Distance between particles + auto const d = box_geo.get_mi_vector(p1.pos(), p2.pos()); + + // Interaction parameters for particle types + auto const &ia_params = *get_ia_param(p1.type(), p2.type()); + auto const coulomb_kernel = Coulomb::pair_energy_kernel(); + + auto const energy = calc_non_bonded_pair_energy( + p1, p2, ia_params, d, d.norm(), coulomb_kernel.get_ptr()); + + return energy >= m_cut_off; + } + double get_cut_off() { return m_cut_off; } + void set_cut_off(double c) { m_cut_off = c; } + +private: + double m_cut_off; +}; +} // namespace PairCriteria + +#endif diff --git a/src/core/pair_criteria/PairCriterion.hpp b/src/core/pair_criteria/PairCriterion.hpp new file mode 100644 index 00000000000..9db8a65f643 --- /dev/null +++ b/src/core/pair_criteria/PairCriterion.hpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_CORE_PAIR_CRITERIA_PAIR_CRITERION_HPP +#define ESPRESSO_SRC_CORE_PAIR_CRITERIA_PAIR_CRITERION_HPP + +#include "Particle.hpp" +#include "particle_node.hpp" + +namespace PairCriteria { +/** + * @brief Criterion which retuns a true/false value for a pair of particles. + */ +class PairCriterion { +public: + /** @brief Make a decision based on two particles */ + virtual bool decide(Particle const &p1, Particle const &p2) const = 0; + /** + * @brief Make a decision based on particle ids. + * This can only run on the head node outside of the integration loop. + */ + bool decide(int id1, int id2) const { + // Retrieve particle data + auto const &p1 = get_particle_data(id1); + auto const &p2 = get_particle_data(id2); + const bool res = decide(p1, p2); + return res; + } + virtual ~PairCriterion() = default; +}; +} // namespace PairCriteria + +#endif diff --git a/src/core/pair_criteria/pair_criteria.hpp b/src/core/pair_criteria/pair_criteria.hpp deleted file mode 100644 index d8b8a22db42..00000000000 --- a/src/core/pair_criteria/pair_criteria.hpp +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef PAIR_CRITERIA_HPP -#define PAIR_CRITERIA_HPP - -#include "Particle.hpp" -#include "energy_inline.hpp" -#include "grid.hpp" -#include "particle_node.hpp" - -namespace PairCriteria { -/** @brief Criterion which provides a true/false for a pair of particles */ -class PairCriterion { -public: - /** @brief Make a decision based on two Particle objects */ - virtual bool decide(const Particle &p1, const Particle &p2) const = 0; - /** - * @brief Make a decision based on particle ids. - * This can only run on the head node outside the integration loop. - */ - bool decide(int id1, int id2) const { - // Retrieve particle data - auto const &p1 = get_particle_data(id1); - auto const &p2 = get_particle_data(id2); - const bool res = decide(p1, p2); - return res; - } - virtual ~PairCriterion() = default; -}; - -/** @brief True if two particles are closer than a cut off distance, respecting - * minimum image convention */ -class DistanceCriterion : public PairCriterion { -public: - bool decide(const Particle &p1, const Particle &p2) const override { - return box_geo.get_mi_vector(p1.pos(), p2.pos()).norm() <= m_cut_off; - } - double get_cut_off() { return m_cut_off; } - void set_cut_off(double c) { m_cut_off = c; } - -private: - double m_cut_off; -}; - -/** True if the short range energy is larger than a cut_off */ -class EnergyCriterion : public PairCriterion { -public: - bool decide(const Particle &p1, const Particle &p2) const override { - // Distance between particles - auto const vec21 = box_geo.get_mi_vector(p1.pos(), p2.pos()); - const double dist_betw_part = vec21.norm(); - - // Interaction parameters for particle types - IA_parameters const &ia_params = *get_ia_param(p1.type(), p2.type()); - - return (calc_non_bonded_pair_energy(p1, p2, ia_params, vec21, - dist_betw_part)) >= m_cut_off; - } - double get_cut_off() { return m_cut_off; } - void set_cut_off(double c) { m_cut_off = c; } - -private: - double m_cut_off; -}; - -/** True if a bond of given type exists between the two particles */ -class BondCriterion : public PairCriterion { -public: - bool decide(const Particle &p1, const Particle &p2) const override { - return pair_bond_exists_on(p1.bonds(), p2.identity(), m_bond_type) || - pair_bond_exists_on(p2.bonds(), p1.identity(), m_bond_type); - } - int get_bond_type() { return m_bond_type; } - void set_bond_type(int t) { m_bond_type = t; } - -private: - int m_bond_type; -}; -} // namespace PairCriteria - -#endif diff --git a/src/core/pressure.cpp b/src/core/pressure.cpp index ca5e95520d0..4a20f927f24 100644 --- a/src/core/pressure.cpp +++ b/src/core/pressure.cpp @@ -39,8 +39,8 @@ #include "short_range_loop.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/dipole.hpp" +#include "electrostatics/coulomb.hpp" +#include "magnetostatics/dipoles.hpp" #include #include @@ -57,8 +57,9 @@ static std::shared_ptr calculate_pressure_local() { auto obs_pressure_ptr = std::make_shared(9); - if (long_range_interactions_sanity_checks()) + if (long_range_interactions_sanity_checks()) { return obs_pressure_ptr; + } auto &obs_pressure = *obs_pressure_ptr; @@ -71,11 +72,16 @@ static std::shared_ptr calculate_pressure_local() { add_kinetic_virials(p, obs_pressure); } + auto const coulomb_force_kernel = Coulomb::pair_force_kernel(); + auto const coulomb_pressure_kernel = Coulomb::pair_pressure_kernel(); + short_range_loop( - [&obs_pressure](Particle const &p1, int bond_id, - Utils::Span partners) { + [&obs_pressure, + coulomb_force_kernel_ptr = coulomb_force_kernel.get_ptr()]( + Particle const &p1, int bond_id, Utils::Span partners) { auto const &iaparams = *bonded_ia_params.at(bond_id); - auto const result = calc_bonded_pressure_tensor(iaparams, p1, partners); + auto const result = calc_bonded_pressure_tensor( + iaparams, p1, partners, coulomb_force_kernel_ptr); if (result) { auto const &tensor = result.get(); /* pressure tensor part */ @@ -88,10 +94,12 @@ static std::shared_ptr calculate_pressure_local() { } return true; }, - [&obs_pressure](Particle const &p1, Particle const &p2, - Distance const &d) { + [&obs_pressure, coulomb_force_kernel_ptr = coulomb_force_kernel.get_ptr(), + coulomb_pressure_kernel_ptr = coulomb_pressure_kernel.get_ptr()]( + Particle const &p1, Particle const &p2, Distance const &d) { add_non_bonded_pair_virials(p1, p2, d.vec21, sqrt(d.dist2), - obs_pressure); + obs_pressure, coulomb_force_kernel_ptr, + coulomb_pressure_kernel_ptr); }, maximal_cutoff(n_nodes), maximal_cutoff_bonded()); @@ -102,7 +110,7 @@ static std::shared_ptr calculate_pressure_local() { #endif #ifdef DIPOLES /* calculate k-space part of magnetostatic interaction. */ - Dipole::calc_pressure_long_range(); + Dipoles::calc_pressure_long_range(); #endif #ifdef VIRTUAL_SITES diff --git a/src/core/pressure_inline.hpp b/src/core/pressure_inline.hpp index 5b5f18302c8..b07603ccb5a 100644 --- a/src/core/pressure_inline.hpp +++ b/src/core/pressure_inline.hpp @@ -20,9 +20,6 @@ */ #ifndef CORE_PRESSURE_INLINE_HPP #define CORE_PRESSURE_INLINE_HPP -/** \file - * Pressure calculation. - */ #include "config.hpp" @@ -52,18 +49,23 @@ * @param p2 pointer to particle 2. * @param d vector between p1 and p2. * @param dist distance between p1 and p2. + * @param kernel_forces %Coulomb force kernel. + * @param kernel_pressure %Coulomb pressure kernel. * @param[in,out] obs_pressure pressure observable. */ -inline void add_non_bonded_pair_virials(Particle const &p1, Particle const &p2, - Utils::Vector3d const &d, double dist, - Observable_stat &obs_pressure) { +inline void add_non_bonded_pair_virials( + Particle const &p1, Particle const &p2, Utils::Vector3d const &d, + double dist, Observable_stat &obs_pressure, + Coulomb::ShortRangeForceKernel::kernel_type const *kernel_forces, + Coulomb::ShortRangePressureKernel::kernel_type const *kernel_pressure) { #ifdef EXCLUSIONS if (do_nonbonded(p1, p2)) #endif { IA_parameters const &ia_params = *get_ia_param(p1.type(), p2.type()); - auto const force = calc_non_bonded_pair_force(p1, p2, ia_params, d, dist).f; - auto const stress = tensor_product(d, force); + auto const force = + calc_non_bonded_pair_force(p1, p2, ia_params, d, dist, kernel_forces).f; + auto const stress = Utils::tensor_product(d, force); auto const type1 = p1.mol_id(); auto const type2 = p2.mol_id(); @@ -71,9 +73,9 @@ inline void add_non_bonded_pair_virials(Particle const &p1, Particle const &p2, } #ifdef ELECTROSTATICS - if (!obs_pressure.coulomb.empty()) { + if (!obs_pressure.coulomb.empty() and kernel_pressure != nullptr) { /* real space Coulomb */ - auto const p_coulomb = Coulomb::pair_pressure(p1, p2, d, dist); + auto const p_coulomb = (*kernel_pressure)(p1.q() * p2.q(), d, dist); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { @@ -81,26 +83,27 @@ inline void add_non_bonded_pair_virials(Particle const &p1, Particle const &p2, } } } -#endif /*ifdef ELECTROSTATICS */ +#endif // ELECTROSTATICS #ifdef DIPOLES /* real space magnetic dipole-dipole */ - if (dipole.method != DIPOLAR_NONE) { + if (magnetostatics_actor) { fprintf(stderr, "calculating pressure for magnetostatics which doesn't " "have it implemented\n"); } -#endif /*ifdef DIPOLES */ +#endif // DIPOLES } -boost::optional> -calc_bonded_virial_pressure_tensor(Bonded_IA_Parameters const &iaparams, - Particle const &p1, Particle const &p2) { +boost::optional> calc_bonded_virial_pressure_tensor( + Bonded_IA_Parameters const &iaparams, Particle const &p1, + Particle const &p2, + Coulomb::ShortRangeForceKernel::kernel_type const *kernel) { auto const dx = box_geo.get_mi_vector(p1.pos(), p2.pos()); - auto const result = calc_bond_pair_force(p1, p2, iaparams, dx); + auto const result = calc_bond_pair_force(p1, p2, iaparams, dx, kernel); if (result) { auto const &force = result.get(); - return tensor_product(force, dx); + return Utils::tensor_product(force, dx); } return {}; @@ -124,7 +127,8 @@ calc_bonded_three_body_pressure_tensor(Bonded_IA_Parameters const &iaparams, Utils::Vector3d force2, force3; std::tie(std::ignore, force2, force3) = result.get(); - return tensor_product(force2, dx21) + tensor_product(force3, dx31); + return Utils::tensor_product(force2, dx21) + + Utils::tensor_product(force3, dx31); } } else { runtimeWarningMsg() << "Unsupported bond type " + @@ -136,13 +140,14 @@ calc_bonded_three_body_pressure_tensor(Bonded_IA_Parameters const &iaparams, return {}; } -inline boost::optional> -calc_bonded_pressure_tensor(Bonded_IA_Parameters const &iaparams, - Particle const &p1, - Utils::Span partners) { +inline boost::optional> calc_bonded_pressure_tensor( + Bonded_IA_Parameters const &iaparams, Particle const &p1, + Utils::Span partners, + Coulomb::ShortRangeForceKernel::kernel_type const *kernel) { switch (number_of_partners(iaparams)) { case 1: - return calc_bonded_virial_pressure_tensor(iaparams, p1, *partners[0]); + return calc_bonded_virial_pressure_tensor(iaparams, p1, *partners[0], + kernel); case 2: return calc_bonded_three_body_pressure_tensor(iaparams, p1, *partners[0], *partners[1]); @@ -155,8 +160,8 @@ calc_bonded_pressure_tensor(Bonded_IA_Parameters const &iaparams, } /** Calculate kinetic pressure (aka energy) for one particle. - * @param[in] p1 particle for which to calculate pressure - * @param[out] obs_pressure pressure observable + * @param[in] p1 particle for which to calculate pressure + * @param[out] obs_pressure pressure observable */ inline void add_kinetic_virials(Particle const &p1, Observable_stat &obs_pressure) { diff --git a/src/core/scafacos/CMakeLists.txt b/src/core/scafacos/CMakeLists.txt new file mode 100644 index 00000000000..868f1f66adc --- /dev/null +++ b/src/core/scafacos/CMakeLists.txt @@ -0,0 +1,3 @@ +target_sources( + EspressoCore PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ScafacosContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ScafacosContextBase.cpp) diff --git a/src/core/scafacos/ScafacosContext.cpp b/src/core/scafacos/ScafacosContext.cpp new file mode 100644 index 00000000000..c870c593df3 --- /dev/null +++ b/src/core/scafacos/ScafacosContext.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#if defined(SCAFACOS) or defined(SCAFACOS_DIPOLES) + +#include "scafacos/ScafacosContext.hpp" + +#include "cells.hpp" +#include "communication.hpp" +#include "grid.hpp" + +#include + +#include + +#include +#include + +namespace detail { +std::tuple +get_system_params() { + auto periodicity = Utils::Vector3i{static_cast(box_geo.periodic(0)), + static_cast(box_geo.periodic(1)), + static_cast(box_geo.periodic(2))}; + auto const n_part = boost::mpi::all_reduce( + comm_cart, cell_structure.local_particles().size(), std::plus<>()); + return {box_geo.length(), periodicity, n_part}; +} +} // namespace detail + +#endif // SCAFACOS or SCAFACOS_DIPOLES diff --git a/src/core/scafacos/ScafacosContext.hpp b/src/core/scafacos/ScafacosContext.hpp new file mode 100644 index 00000000000..fc612b14488 --- /dev/null +++ b/src/core/scafacos/ScafacosContext.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_CORE_SCAFACOS_SCAFACOS_CONTEXT_HPP +#define ESPRESSO_SRC_CORE_SCAFACOS_SCAFACOS_CONTEXT_HPP + +/** + * @file + * @ref ScafacosContext implements the interface of the ScaFaCoS bridge. + * It is further derived for the coulombic and dipolar versions of ScaFaCoS. + */ + +#include "config.hpp" + +#if defined(SCAFACOS) or defined(SCAFACOS_DIPOLES) + +#include "scafacos/ScafacosContextBase.hpp" + +#include +#include +#include + +namespace detail { +std::tuple +get_system_params(); +} + +template +struct ScafacosContext : virtual public ScafacosContextBase, + public ScafacosInterface { + using ScafacosContextBase::ScafacosContextBase; + using ScafacosInterface::ScafacosInterface; + + void update_system_params() override { + auto params = detail::get_system_params(); + ScafacosInterface::set_runtime_parameters( + std::get<0>(params).data(), std::get<1>(params).data(), + static_cast(std::get<2>(params))); + } + + std::string get_method() const override { + return ScafacosInterface::get_method(); + } + + std::string get_parameters() const override { + return ScafacosInterface::get_parameters(); + } +}; + +#endif // SCAFACOS or SCAFACOS_DIPOLES +#endif diff --git a/src/core/scafacos/ScafacosContextBase.cpp b/src/core/scafacos/ScafacosContextBase.cpp new file mode 100644 index 00000000000..991adedf4e0 --- /dev/null +++ b/src/core/scafacos/ScafacosContextBase.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#if defined(SCAFACOS) or defined(SCAFACOS_DIPOLAR) + +#include "scafacos/ScafacosContextBase.hpp" + +#include + +#include + +#include +#include +#include +#include + +std::vector ScafacosContextBase::available_methods() { + return ::Scafacos::Scafacos::available_methods(); +} + +void ScafacosContextBase::sanity_check_method(std::string const &method_name) { + auto const all = ScafacosContextBase::available_methods(); + auto const valid_methods = std::set(all.begin(), all.end()); + if (valid_methods.count(method_name) == 0) { + auto const method_names = "'" + boost::algorithm::join(all, "', '") + "'"; + throw std::invalid_argument("Method '" + method_name + + "' is unknown or not compiled in ScaFaCoS; " + "currently available methods are " + + method_names); + } +} + +#endif // SCAFACOS or SCAFACOS_DIPOLAR diff --git a/src/core/electrostatics_magnetostatics/ScafacosContextBase.hpp b/src/core/scafacos/ScafacosContextBase.hpp similarity index 59% rename from src/core/electrostatics_magnetostatics/ScafacosContextBase.hpp rename to src/core/scafacos/ScafacosContextBase.hpp index b3f6b32a219..e153a930b70 100644 --- a/src/core/electrostatics_magnetostatics/ScafacosContextBase.hpp +++ b/src/core/scafacos/ScafacosContextBase.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2020 The ESPResSo project + * Copyright (C) 2010-2022 The ESPResSo project * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 * Max-Planck-Institute for Polymer Research, Theory Group * @@ -18,25 +18,26 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef SRC_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOSCONTEXTBASE_HPP -#define SRC_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOSCONTEXTBASE_HPP + /** * @file - * @ref Scafacos::ScafacosContextBase provides the public interface + * @ref ScafacosContextBase provides the public interface * of the ScaFaCoS bridge. It relies on type erasure to hide the * ScaFaCoS implementation details from the ESPResSo core. It is - * implemented by @ref Scafacos::ScafacosContext. + * implemented by @ref ScafacosContext. */ +#ifndef ESPRESSO_SRC_CORE_SCAFACOS_SCAFACOS_CONTEXT_BASE_HPP +#define ESPRESSO_SRC_CORE_SCAFACOS_SCAFACOS_CONTEXT_BASE_HPP + #include "config.hpp" -#if defined(SCAFACOS) +#if defined(SCAFACOS) or defined(SCAFACOS_DIPOLAR) #include #include - -namespace Scafacos { +#include /** * @brief Public interface to the ScaFaCoS context. @@ -45,6 +46,7 @@ namespace Scafacos { * as the output arrays. */ struct ScafacosContextBase { + ScafacosContextBase() = default; virtual ~ScafacosContextBase() = default; /** @brief Collect particle data in continuous arrays. */ virtual void update_particle_data() = 0; @@ -53,32 +55,16 @@ struct ScafacosContextBase { /** @brief Calculate long-range part of the energy. */ virtual double long_range_energy() = 0; /** @brief Add long-range part of the forces to particles. */ - virtual void add_long_range_force() = 0; - /** @brief Add near-field pair force. */ - inline void add_pair_force(double q1q2, Utils::Vector3d const &d, double dist, - Utils::Vector3d &force) { - if (dist > get_r_cut()) - return; - - auto const field = get_pair_force(dist); - auto const fak = q1q2 * field / dist; - force -= fak * d; - } - /** @brief Calculate near-field pair energy. */ - inline double pair_energy(double q1q2, double dist) { - if (dist > get_r_cut()) - return 0.; - - return q1q2 * get_pair_energy(dist); - } + virtual void add_long_range_forces() = 0; /** @brief Reinitialize number of particles, box shape and periodicity. */ virtual void update_system_params() = 0; - virtual double get_r_cut() const = 0; - virtual double get_pair_force(double dist) const = 0; - virtual double get_pair_energy(double dist) const = 0; - virtual std::string get_method_and_parameters() = 0; + virtual std::string get_method() const = 0; + virtual std::string get_parameters() const = 0; + virtual void sanity_checks() const = 0; + + static std::vector available_methods(); + static void sanity_check_method(std::string const &method_name); }; -} // namespace Scafacos -#endif // SCAFACOS -#endif // SRC_CORE_ELECTROSTATICS_MAGNETOSTATICS_SCAFACOSCONTEXTBASE_HPP +#endif // SCAFACOS or SCAFACOS_DIPOLAR +#endif diff --git a/src/core/tuning.cpp b/src/core/tuning.cpp index 6f6d66ae04e..bbd29d9a41b 100644 --- a/src/core/tuning.cpp +++ b/src/core/tuning.cpp @@ -18,9 +18,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file - * Implementation of tuning.hpp. - */ + #include "tuning.hpp" #include "cells.hpp" @@ -39,36 +37,63 @@ #include #include -#include +#include +#include + +std::string TuningFailed::get_first_error() const { + using namespace ErrorHandling; + auto const queued_warnings = mpi_gather_runtime_errors_all(this_node == 0); + auto message = std::string("tuning failed: an exception was thrown while " + "benchmarking the integration loop"); + for (auto const &warning : queued_warnings) { + if (warning.level() == RuntimeError::ErrorLevel::ERROR) { + message += " (" + warning.what() + ")"; + break; + } + } + return message; +} -double time_force_calc(int int_steps) { +static void check_statistics(Utils::Statistics::RunningAverage &acc) { + if (acc.avg() <= 5 * MPI_Wtick()) { + runtimeWarningMsg() + << "Clock resolution is too low to reliably time integration."; + } + if (acc.sig() >= 0.1 * acc.avg()) { + runtimeWarningMsg() << "Statistics of tuning samples is very bad."; + } +} + +static void throw_on_error() { + auto const n_errors = check_runtime_errors_local(); + if (boost::mpi::all_reduce(comm_cart, n_errors, std::plus<>()) != 0) { + throw TuningFailed{}; + } +} + +double benchmark_integration_step(int int_steps) { Utils::Statistics::RunningAverage running_average; - if (mpi_integrate(0, 0)) - return -1; + integrate(0, 0); + throw_on_error(); /* perform force calculation test */ for (int i = 0; i < int_steps; i++) { - const double tick = MPI_Wtime(); - - if (mpi_integrate(0, -1)) - return -1; - - const double tock = MPI_Wtime(); + auto const tick = MPI_Wtime(); + integrate(0, -1); + auto const tock = MPI_Wtime(); running_average.add_sample((tock - tick)); + throw_on_error(); } - if (running_average.avg() <= 5 * MPI_Wtick()) { - runtimeWarningMsg() - << "Clock resolution is too low to reliably time integration."; - } - - if (running_average.sig() >= 0.1 * running_average.avg()) { - runtimeWarningMsg() << "Statistics of tuning samples is very bad."; + if (this_node == 0) { + check_statistics(running_average); } - /* MPI returns s, return value should be in ms. */ - return 1000. * running_average.avg(); + /* MPI returns in seconds, returned value should be in ms. */ + auto retval = 1000. * running_average.avg(); + boost::mpi::broadcast(comm_cart, retval, 0); + return retval; } /** diff --git a/src/core/tuning.hpp b/src/core/tuning.hpp index 9b40b513877..6c66916ff6a 100644 --- a/src/core/tuning.hpp +++ b/src/core/tuning.hpp @@ -18,24 +18,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** \file - * This contains a timing loop for the force calculation. - * - * Implementation in tuning.cpp. - */ -#ifndef TUNING_H -#define TUNING_H +#ifndef ESPRESSO_SRC_CORE_TUNING_HPP +#define ESPRESSO_SRC_CORE_TUNING_HPP + +#include +#include + +class TuningFailed : public std::runtime_error { + std::string get_first_error() const; + +public: + TuningFailed() : std::runtime_error{get_first_error()} {} +}; -/** Measure the time for some force calculations. - * Actually performs \ref mpi_integrate (0). - * This times the force calculation without - * propagating the system. It therefore does - * not include e.g. Verlet list updates. - * @param int_steps Number of integration steps. - * @return Time per integration in milliseconds. +/** + * @brief Benchmark the integration loop. + * Call @ref integrate() several times and measure the elapsed time + * without propagating the system. It therefore doesn't include e.g. + * Verlet list updates. + * @param int_steps Number of integration steps. + * @return Average time per integration loop in milliseconds. */ -double time_force_calc(int int_steps); +double benchmark_integration_step(int int_steps); /** Set the optimal @ref skin between @p min_skin and @p max_skin * by bisection to tolerance @p tol. diff --git a/src/core/unit_tests/EspressoSystemStandAlone_test.cpp b/src/core/unit_tests/EspressoSystemStandAlone_test.cpp index e1d34113c9c..e205730304f 100644 --- a/src/core/unit_tests/EspressoSystemStandAlone_test.cpp +++ b/src/core/unit_tests/EspressoSystemStandAlone_test.cpp @@ -34,8 +34,8 @@ namespace utf = boost::unit_test; #include "bonded_interactions/fene.hpp" #include "bonded_interactions/harmonic.hpp" #include "communication.hpp" -#include "electrostatics_magnetostatics/coulomb.hpp" -#include "electrostatics_magnetostatics/p3m.hpp" +#include "electrostatics/p3m.hpp" +#include "electrostatics/registration.hpp" #include "energy.hpp" #include "galilei.hpp" #include "integrate.hpp" @@ -59,6 +59,7 @@ namespace utf = boost::unit_test; #include #include #include +#include #include namespace espresso { @@ -76,7 +77,7 @@ struct if_head_node { boost::mpi::communicator world; }; -void create_bonds_local(int harm_bond_id, int fene_bond_id) { +static void mpi_create_bonds_local(int harm_bond_id, int fene_bond_id) { // set up a harmonic bond auto const harm_bond = HarmonicBond(200.0, 0.3, 1.0); auto const harm_bond_ia = std::make_shared(harm_bond); @@ -87,12 +88,34 @@ void create_bonds_local(int harm_bond_id, int fene_bond_id) { bonded_ia_params.insert(fene_bond_id, fene_bond_ia); } -REGISTER_CALLBACK(create_bonds_local) +REGISTER_CALLBACK(mpi_create_bonds_local) -void create_bonds(int harm_bond_id, int fene_bond_id) { - mpi_call_all(create_bonds_local, harm_bond_id, fene_bond_id); +static void mpi_create_bonds(int harm_bond_id, int fene_bond_id) { + mpi_call_all(mpi_create_bonds_local, harm_bond_id, fene_bond_id); } +#ifdef P3M +static void mpi_set_tuned_p3m_local(double prefactor) { + auto p3m = P3MParameters{false, + 0.0, + 3.5, + Utils::Vector3i::broadcast(8), + Utils::Vector3d::broadcast(0.5), + 5, + 0.654, + 1e-3}; + auto solver = + std::make_shared(std::move(p3m), prefactor, 1, false); + ::Coulomb::add_actor(solver); +} + +REGISTER_CALLBACK(mpi_set_tuned_p3m_local) + +static void mpi_set_tuned_p3m(double prefactor) { + mpi_call_all(mpi_set_tuned_p3m_local, prefactor); +} +#endif // P3M + BOOST_FIXTURE_TEST_CASE(espresso_system_stand_alone, ParticleFactory, *utf::precondition(if_head_node())) { constexpr auto tol = 100. * std::numeric_limits::epsilon(); @@ -217,7 +240,7 @@ BOOST_FIXTURE_TEST_CASE(espresso_system_stand_alone, ParticleFactory, auto const harm_bond_id = 0; auto const none_bond_id = 1; auto const fene_bond_id = 2; - create_bonds(harm_bond_id, fene_bond_id); + mpi_create_bonds(harm_bond_id, fene_bond_id); auto const &harm_bond = *boost::get(bonded_ia_params.at(harm_bond_id).get()); auto const &fene_bond = @@ -245,12 +268,8 @@ BOOST_FIXTURE_TEST_CASE(espresso_system_stand_alone, ParticleFactory, set_particle_q(pid2, -1.); // set up P3M - auto const prefactor = 2.0; - auto const mesh = std::vector{8, 8, 8}; - Coulomb::set_prefactor(prefactor); - p3m_set_eps(0.0); - p3m_set_params(3.5, mesh.data(), 5, 0.654, 1e-3); - p3m_set_mesh_offset(0.5, 0.5, 0.5); + auto const prefactor = 2.; + mpi_set_tuned_p3m(prefactor); // measure energies auto const step = 0.02; diff --git a/src/core/unit_tests/p3m_test.cpp b/src/core/unit_tests/p3m_test.cpp index d62e86deaae..60ae2db1058 100644 --- a/src/core/unit_tests/p3m_test.cpp +++ b/src/core/unit_tests/p3m_test.cpp @@ -21,7 +21,9 @@ #define BOOST_TEST_DYN_LINK #include -#include "electrostatics_magnetostatics/p3m-common.hpp" +#include "p3m/common.hpp" + +#include #include #include @@ -32,7 +34,7 @@ BOOST_AUTO_TEST_CASE(calc_meshift_false) { {std::vector{0}, std::vector{0, 1, -2, -1}, std::vector{0, 1, 2, 3, -3, -2, -1}}}; - int const mesh[3] = {1, 4, 7}; + auto const mesh = Utils::Vector3i{{1, 4, 7}}; auto const val = detail::calc_meshift(mesh, false); for (std::size_t i = 0; i < 3; ++i) { @@ -47,7 +49,7 @@ BOOST_AUTO_TEST_CASE(calc_meshift_true) { {std::vector{0}, std::vector{0, 1, 0, -1}, std::vector{0, 1, 2, 0, -3, -2, -1}}}; - int const mesh[3] = {1, 4, 7}; + auto const mesh = Utils::Vector3i{{1, 4, 7}}; auto const val = detail::calc_meshift(mesh, true); for (std::size_t i = 0; i < 3; ++i) { diff --git a/src/python/espressomd/actors.py b/src/python/espressomd/actors.py new file mode 100644 index 00000000000..dd32ca2f375 --- /dev/null +++ b/src/python/espressomd/actors.py @@ -0,0 +1,99 @@ +# Copyright (C) 2010-2019 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from . import highlander +from . import script_interface + + +class Actors: + + """ + Container for actor objects. + """ + + active_actors = [] + + def __del__(self): + self.clear() + + def __getstate__(self): + return self.active_actors + + def __setstate__(self, active_actors): + self.active_actors[:] = [] + for actor in active_actors: + self.active_actors.append(actor) + actor._activate() + + def add(self, actor): + """ + Parameters + ---------- + actor : + Actor to add to this container. + + """ + if actor in Actors.active_actors: + raise highlander.ThereCanOnlyBeOne(actor) + + if isinstance(actor, script_interface.ScriptInterfaceHelper): + actor._activate() + + self.active_actors.append(actor) + + if not isinstance(actor, script_interface.ScriptInterfaceHelper): + actor._activate() + + def remove(self, actor): + """ + Parameters + ---------- + actor : + Actor to remove from this container. + + """ + if actor not in self.active_actors: + raise Exception("Actor is not active") + actor._deactivate() + self.active_actors.remove(actor) + + def clear(self): + """Remove all actors.""" + # The order in which actors are removed matters. Some actors set up + # global bitfields that activate sanity checks on the MD cellsystem, + # and reset these bitfields when removed. Actors need to be removed + # in the reverse order they were inserted to guarantee pre-conditions + # and post-conditions are always met. + while len(self.active_actors): + self.remove(self.active_actors[-1]) + + def __str__(self): + return str(self.active_actors) + + def __getitem__(self, key): + return self.active_actors[key] + + def __len__(self): + return len(self.active_actors) + + def __iter__(self): + for actor in self.active_actors: + yield actor + + def __delitem__(self, idx): + actor = self[idx] + self.remove(actor) diff --git a/src/python/espressomd/actors.pyx b/src/python/espressomd/actors.pyx deleted file mode 100644 index 13f34a636b3..00000000000 --- a/src/python/espressomd/actors.pyx +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright (C) 2010-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -include "myconfig.pxi" -from .highlander import ThereCanOnlyBeOne -from .utils import handle_errors -from . import utils - - -cdef class Actor: - - """ - Abstract base class for interactions affecting particles in the system, - such as electrostatics, magnetostatics or LB fluids. Derived classes must - implement the interface to the relevant core objects and global variables. - """ - - # Keys in active_list have to match the method name. - active_list = dict(ElectrostaticInteraction=False, - MagnetostaticInteraction=False, - MagnetostaticExtension=False, - HydrodynamicInteraction=False, - Scafacos=False) - - # __getstate__ and __setstate__ define the pickle interaction - def __getstate__(self): - odict = self._params.copy() - return odict - - def __setstate__(self, params): - self._params = params - self._set_params_in_es_core() - - def __init__(self, *args, **kwargs): - self._isactive = False - utils.check_valid_keys(self.valid_keys(), kwargs.keys()) - utils.check_required_keys(self.required_keys(), kwargs.keys()) - self._params = self.default_params() - self._params.update(kwargs) - - def _activate(self): - inter = self._get_interaction_type() - if inter in Actor.active_list: - if Actor.active_list[inter]: - raise ThereCanOnlyBeOne(self.__class__.__bases__[0]) - Actor.active_list[inter] = True - - self.validate_params() - self._activate_method() - handle_errors("Activation of an actor") - self._isactive = True - - def _deactivate(self): - self._deactivate_method() - handle_errors("Deactivation of an actor") - self._isactive = False - inter = self._get_interaction_type() - if inter in Actor.active_list: - if not Actor.active_list[inter]: - raise Exception( - "Class not registered in Actor.active_list: " + self.__class__.__bases__[0].__name__) - Actor.active_list[inter] = False - - def is_valid(self): - """ - Check if the data stored in this instance still matches the - corresponding data in the core. - """ - temp_params = self._get_params_from_es_core() - if self._params != temp_params: - return False - - # If we're still here, the instance is valid - return True - - def get_params(self): - """Get interaction parameters""" - # If this instance refers to an actual interaction defined in the es - # core, load current parameters from there - if self.is_active(): - update = self._get_params_from_es_core() - self._params.update(update) - return self._params - - def set_params(self, **p): - """Update the given parameters.""" - # Check if keys are valid - utils.check_valid_keys(self.valid_keys(), p.keys()) - - # When an interaction is newly activated, all required keys must be - # given - if not self.is_active(): - utils.check_required_keys(self.required_keys(), p.keys()) - - self._params.update(p) - # validate updated parameters - self.validate_params() - # Put in values given by the user - if self.is_active(): - self._set_params_in_es_core() - - def __str__(self): - return f"{self.__class__.__name__}({self.get_params()})" - - def _get_interaction_type(self): - bases = self.class_lookup(self.__class__) - for i in range(len(bases)): - if bases[i].__name__ in Actor.active_list: - return bases[i].__name__ - - def class_lookup(self, cls): - c = list(cls.__bases__) - for base in c: - c.extend(self.class_lookup(base)) - return c - - def is_active(self): - return self._isactive - - def valid_keys(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the valid_keys() method." % self._get_interaction_type()) - - def required_keys(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the required_keys() method." % self._get_interaction_type()) - - def validate_params(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the validate_params() method." % self._get_interaction_type()) - - def _get_params_from_es_core(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the _get_params_from_es_core() method." % self._get_interaction_type()) - - def _set_params_in_es_core(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the _set_params_in_es_core() method." % self._get_interaction_type()) - - def default_params(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the default_params() method." % self._get_interaction_type()) - - def _activate_method(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the _activate_method() method." % self._get_interaction_type()) - - def _deactivate_method(self): - """Virtual method.""" - raise Exception( - "Subclasses of %s must define the _deactivate_method() method." % self._get_interaction_type()) - - -class Actors: - - """ - Container for :class:`Actor` objects. - """ - - active_actors = [] - - def __del__(self): - self.clear() - - def __getstate__(self): - return self.active_actors - - def __setstate__(self, active_actors): - self.active_actors[:] = [] - for a in active_actors: - self.active_actors.append(a) - a._activate() - - def add(self, actor): - """ - Parameters - ---------- - actor : :class:`Actor` - Actor to add to this container. - - """ - if actor not in Actors.active_actors: - self.active_actors.append(actor) - actor._activate() - else: - raise ThereCanOnlyBeOne(actor) - - def remove(self, actor): - """ - Parameters - ---------- - actor : :class:`Actor` - Actor to remove from this container. - - """ - if actor not in self.active_actors: - raise Exception("Actor is not active") - actor._deactivate() - self.active_actors.remove(actor) - - def clear(self): - """Remove all actors.""" - # The order in which actors are removed matters. Some actors set up - # global bitfields that activate sanity checks on the MD cellsystem, - # and reset these bitfields when removed. Actors need to be removed - # in the reverse order they were inserted to guarantee pre-conditions - # and post-conditions are always met. - while len(self.active_actors): - self.remove(self.active_actors[-1]) - - def __str__(self): - return str(self.active_actors) - - def __getitem__(self, key): - return self.active_actors[key] - - def __len__(self): - return len(self.active_actors) - - def __iter__(self): - for a in self.active_actors: - yield a - - def __delitem__(self, idx): - actor = self[idx] - self.remove(actor) diff --git a/src/python/espressomd/analyze.pxd b/src/python/espressomd/analyze.pxd index 4806e7dfeb6..dffe1a12808 100644 --- a/src/python/espressomd/analyze.pxd +++ b/src/python/espressomd/analyze.pxd @@ -120,7 +120,8 @@ cdef inline get_obs_contribs(Span[double] contributions, """ cdef np.ndarray value value = create_nparray_from_double_span(contributions) - if contributions.size() == 9: + if contributions.size() >= 9: + assert contributions.size() % 3 == 0 if calc_scalar_pressure: return np.einsum('...ii', value.reshape((-1, 3, 3))) / 3 else: @@ -149,8 +150,8 @@ cdef inline get_obs_contrib(Span[double] contribution, return value[0] return value -cdef inline observable_stat_matrix(size_t size, cbool calc_sp): - if size == 9 and not calc_sp: +cdef inline observable_stat_matrix(size_t size, cbool calc_scalar_pressure): + if size == 9 and not calc_scalar_pressure: return np.zeros((3, 3), dtype=float) else: return 0.0 @@ -196,11 +197,15 @@ cdef inline Observable_stat_to_dict(Observable_stat * obs, cbool calc_sp): cdef size_t n_nonbonded = max_seen_particle_type cdef double[9] total + # numpy array shape + shape = (1,) + if obs_dim == 9 and not calc_sp: + shape = (3, 3) + # Dict to store the results p = {} # Total contribution - for i in range(obs_dim): total[i] = obs.accumulate(0.0, i) p["total"] = get_obs_contrib(Span[double](total, obs_dim), calc_sp) @@ -244,6 +249,7 @@ cdef inline Observable_stat_to_dict(Observable_stat * obs, cbool calc_sp): IF ELECTROSTATICS == 1: cdef np.ndarray coulomb coulomb = get_obs_contribs(obs.coulomb, calc_sp) + coulomb = coulomb.reshape((-1, *shape)).squeeze() for i in range(coulomb.shape[0]): p["coulomb", i] = coulomb[i] p["coulomb"] = np.sum(coulomb, axis=0) @@ -252,6 +258,7 @@ cdef inline Observable_stat_to_dict(Observable_stat * obs, cbool calc_sp): IF DIPOLES == 1: cdef np.ndarray dipolar dipolar = get_obs_contribs(obs.dipolar, calc_sp) + dipolar = dipolar.reshape((-1, *shape)).squeeze() for i in range(dipolar.shape[0]): p["dipolar", i] = dipolar[i] p["dipolar"] = np.sum(dipolar, axis=0) diff --git a/src/python/espressomd/electrostatic_extensions.pxd b/src/python/espressomd/electrostatic_extensions.pxd deleted file mode 100644 index 8d9a8162cc5..00000000000 --- a/src/python/espressomd/electrostatic_extensions.pxd +++ /dev/null @@ -1,52 +0,0 @@ -# -# Copyright (C) 2013-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -include "myconfig.pxi" -from libcpp.vector cimport vector -from .utils cimport Vector3d - -IF ELECTROSTATICS: - - cdef extern from "electrostatics_magnetostatics/icc.hpp": - ctypedef struct icc_struct: - int n_icc - int num_iteration - double eout - vector[double] areas - vector[double] ein - vector[double] sigma - double convergence - vector[Vector3d] normals - Vector3d ext_field - double relax - int citeration - int first_id - - # links intern C-struct with python object - cdef extern icc_struct icc_cfg - - void icc_set_params(int n_icc, double convergence, double relaxation, - const Vector3d & ext_field, int max_iterations, - int first_id, double eps_out, - vector[double] & areas, - vector[double] & e_in, - vector[double] & sigma, - vector[Vector3d] & normals) except + - - void icc_deactivate() diff --git a/src/python/espressomd/electrostatic_extensions.py b/src/python/espressomd/electrostatic_extensions.py new file mode 100644 index 00000000000..69d8ed8a8ae --- /dev/null +++ b/src/python/espressomd/electrostatic_extensions.py @@ -0,0 +1,170 @@ +# +# Copyright (C) 2013-2019 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from . import utils +from .script_interface import ScriptInterfaceHelper, script_interface_register +from .__init__ import has_features +import numpy as np + + +class ElectrostaticExtensions(ScriptInterfaceHelper): + + _so_creation_policy = "GLOBAL" + + def __init__(self, **kwargs): + self._check_required_features() + + if 'sip' not in kwargs: + params = self.default_params() + params.update(kwargs) + self.validate_params(params) + super().__init__(**params) + else: + super().__init__(**kwargs) + + def _check_required_features(self): + if not has_features("ELECTROSTATICS"): + raise NotImplementedError("Feature ELECTROSTATICS not compiled in") + + def validate_params(self, params): + raise NotImplementedError("Derived classes must implement this method") + + def default_params(self): + raise NotImplementedError("Derived classes must implement this method") + + def valid_keys(self): + raise NotImplementedError("Derived classes must implement this method") + + def required_keys(self): + raise NotImplementedError("Derived classes must implement this method") + + def _activate(self): + self.call_method("check_charge_neutrality") + self.call_method("activate") + utils.handle_errors("Coulomb extension activation failed") + + def _deactivate(self): + self.call_method("deactivate") + utils.handle_errors("Coulomb extension deactivation failed") + + +@script_interface_register +class ICC(ElectrostaticExtensions): + """ + Interface to the induced charge calculation scheme for dielectric + interfaces. See :ref:`Dielectric interfaces with the ICC algorithm` + for more details. + + Parameters + ---------- + n_icc : :obj:`int` + Total number of ICC Particles. + first_id : :obj:`int`, optional + ID of the first ICC Particle. + convergence : :obj:`float`, optional + Abort criteria of the iteration. It corresponds to the maximum relative + change of any of the interface particle's charge. + relaxation : :obj:`float`, optional + SOR relaxation parameter. + ext_field : :obj:`float`, optional + Homogeneous electric field added to the calculation of dielectric boundary forces. + max_iterations : :obj:`int`, optional + Maximal number of iterations. + eps_out : :obj:`float`, optional + Relative permittivity of the outer region (where the particles are). + normals : (``n_icc``, 3) array_like :obj:`float` + Normal vectors pointing into the outer region. + areas : (``n_icc``, ) array_like :obj:`float` + Areas of the discretized surface. + sigmas : (``n_icc``, ) array_like :obj:`float`, optional + Additional surface charge density in the absence of any charge + induction. + epsilons : (``n_icc``, ) array_like :obj:`float` + Dielectric constant associated to the areas. + + """ + _so_name = "Coulomb::ICCStar" + _so_creation_policy = "GLOBAL" + + def validate_params(self, params): + utils.check_type_or_throw_except( + params["n_icc"], 1, int, "Invalid parameter 'n_icc'") + utils.check_type_or_throw_except( + params["first_id"], 1, int, "Invalid parameter 'first_id'") + utils.check_type_or_throw_except( + params["convergence"], 1, float, "Invalid parameter 'convergence'") + utils.check_type_or_throw_except( + params["relaxation"], 1, float, "Invalid parameter 'relaxation'") + utils.check_type_or_throw_except( + params["ext_field"], 3, float, "Invalid parameter 'ext_field'") + utils.check_type_or_throw_except( + params["max_iterations"], 1, int, "Invalid parameter 'max_iterations'") + utils.check_type_or_throw_except( + params["eps_out"], 1, float, "Invalid parameter 'eps_out'") + + n_icc = params["n_icc"] + if n_icc <= 0: + raise ValueError("Parameter 'n_icc' must be >= 1") + + if n_icc: + if np.shape(params["normals"]) != (n_icc, 3): + raise ValueError("Parameter 'normals' has incorrect shape") + utils.check_array_type_or_throw_except( + np.reshape(params["normals"], (-1,)), 3 * n_icc, float, + "Parameter 'normals' has incorrect type") + + if "sigmas" not in params: + params["sigmas"] = np.zeros(n_icc) + + for key in ("areas", "sigmas", "epsilons"): + if np.shape(params[key]) != (n_icc,): + raise ValueError(f"Parameter '{key}' has incorrect shape") + utils.check_array_type_or_throw_except( + np.reshape(params[key], (-1,)), n_icc, float, + f"Parameter '{key}' has incorrect type") + + def valid_keys(self): + return {"n_icc", "convergence", "relaxation", "ext_field", + "max_iterations", "first_id", "eps_out", "normals", + "areas", "sigmas", "epsilons", "check_neutrality"} + + def required_keys(self): + return {"n_icc", "normals", "areas", "epsilons"} + + def default_params(self): + return {"convergence": 1e-3, + "relaxation": 0.7, + "ext_field": [0., 0., 0.], + "max_iterations": 100, + "first_id": 0, + "eps_out": 1, + "check_neutrality": True} + + def last_iterations(self): + """ + Number of iterations needed in last relaxation to + reach the convergence criterion. + + Returns + ------- + iterations : :obj:`int` + Number of iterations + + """ + return self.citeration diff --git a/src/python/espressomd/electrostatic_extensions.pyx b/src/python/espressomd/electrostatic_extensions.pyx deleted file mode 100644 index 2fc4a5bf211..00000000000 --- a/src/python/espressomd/electrostatic_extensions.pyx +++ /dev/null @@ -1,193 +0,0 @@ -# -# Copyright (C) 2013-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from . cimport utils -include "myconfig.pxi" -from . cimport actors -from . import actors -import numpy as np -from .utils import array_locked -from .utils cimport check_type_or_throw_except, Vector3d, make_Vector3d, make_array_locked, make_array_locked_vector -from libcpp.vector cimport vector - -IF ELECTROSTATICS: - from espressomd.electrostatics import check_neutrality - - cdef class ElectrostaticExtensions(actors.Actor): - pass - - cdef class ICC(ElectrostaticExtensions): - """ - Interface to the induced charge calculation scheme for dielectric - interfaces. See :ref:`Dielectric interfaces with the ICC algorithm` - for more details. - - Parameters - ---------- - n_icc : :obj:`int` - Total number of ICC Particles. - first_id : :obj:`int`, optional - ID of the first ICC Particle. - convergence : :obj:`float`, optional - Abort criteria of the iteration. It corresponds to the maximum relative - change of any of the interface particle's charge. - relaxation : :obj:`float`, optional - SOR relaxation parameter. - ext_field : :obj:`float`, optional - Homogeneous electric field added to the calculation of dielectric boundary forces. - max_iterations : :obj:`int`, optional - Maximal number of iterations. - eps_out : :obj:`float`, optional - Relative permittivity of the outer region (where the particles are). - normals : (``n_icc``, 3) array_like :obj:`float` - Normal vectors pointing into the outer region. - areas : (``n_icc``, ) array_like :obj:`float` - Areas of the discretized surface. - sigmas : (``n_icc``, ) array_like :obj:`float`, optional - Additional surface charge density in the absence of any charge - induction. - epsilons : (``n_icc``, ) array_like :obj:`float` - Dielectric constant associated to the areas. - - """ - - def validate_params(self): - check_type_or_throw_except(self._params["n_icc"], 1, int, "") - - check_type_or_throw_except( - self._params["first_id"], 1, int, "") - - check_type_or_throw_except( - self._params["convergence"], 1, float, "") - - check_type_or_throw_except( - self._params["relaxation"], 1, float, "") - - check_type_or_throw_except( - self._params["ext_field"], 3, float, "") - - check_type_or_throw_except( - self._params["max_iterations"], 1, int, "") - - check_type_or_throw_except( - self._params["eps_out"], 1, float, "") - - n_icc = self._params["n_icc"] - assert n_icc >= 0, "ICC: invalid number of particles" - - self._params["normals"] = np.array(self._params["normals"]) - if self._params["normals"].size != n_icc * 3: - raise ValueError( - f"Expecting normal list with {n_icc * 3} entries.") - check_type_or_throw_except(self._params["normals"], n_icc, - np.ndarray, "Error in normal list.") - - check_type_or_throw_except( - self._params["areas"], n_icc, float, "Error in area list.") - - if "sigmas" in self._params.keys(): - check_type_or_throw_except( - self._params["sigmas"], n_icc, float, "Error in sigma list.") - else: - self._params["sigmas"] = np.zeros(n_icc) - - check_type_or_throw_except( - self._params["epsilons"], n_icc, float, "Error in epsilon list.") - - def valid_keys(self): - return ["n_icc", "convergence", "relaxation", "ext_field", - "max_iterations", "first_id", "eps_out", "normals", - "areas", "sigmas", "epsilons", "check_neutrality"] - - def required_keys(self): - return ["n_icc", "normals", "areas", "epsilons"] - - def default_params(self): - return {"convergence": 1e-3, - "relaxation": 0.7, - "ext_field": [0, 0, 0], - "max_iterations": 100, - "first_id": 0, - "eps_out": 1, - "check_neutrality": True} - - def _get_params_from_es_core(self): - params = {} - params["n_icc"] = icc_cfg.n_icc - params["first_id"] = icc_cfg.first_id - params["max_iterations"] = icc_cfg.num_iteration - params["convergence"] = icc_cfg.convergence - params["relaxation"] = icc_cfg.relax - params["eps_out"] = icc_cfg.eout - params["normals"] = make_array_locked_vector(icc_cfg.normals) - params["areas"] = array_locked(icc_cfg.areas) - params["epsilons"] = array_locked(icc_cfg.ein) - params["sigmas"] = array_locked(icc_cfg.sigma) - params["ext_field"] = make_array_locked(icc_cfg.ext_field) - - return params - - def _set_params_in_es_core(self): - cdef Vector3d ext_field = make_Vector3d(self._params["ext_field"]) - cdef vector[double] areas, e_in, sigma - cdef vector[Vector3d] normals - areas.resize(self._params["n_icc"]) - e_in.resize(self._params["n_icc"]) - sigma.resize(self._params["n_icc"]) - normals.resize(self._params["n_icc"]) - - for i in range(self._params["n_icc"]): - areas[i] = self._params["areas"][i] - e_in[i] = self._params["epsilons"][i] - sigma[i] = self._params["sigmas"][i] - - for j in range(3): - normals[i][j] = self._params["normals"][i][j] - - icc_set_params(self._params["n_icc"], - self._params["convergence"], - self._params["relaxation"], - ext_field, - self._params["max_iterations"], - self._params["first_id"], - self._params["eps_out"], - areas, - e_in, - sigma, - normals) - - def _activate_method(self): - check_neutrality(self._params) - self._set_params_in_es_core() - - def _deactivate_method(self): - icc_deactivate() - - def last_iterations(self): - """ - Number of iterations needed in last relaxation to - reach the convergence criterion. - - Returns - ------- - iterations : :obj:`int` - Number of iterations - - """ - return icc_cfg.citeration diff --git a/src/python/espressomd/electrostatics.pxd b/src/python/espressomd/electrostatics.pxd deleted file mode 100644 index 17124972bac..00000000000 --- a/src/python/espressomd/electrostatics.pxd +++ /dev/null @@ -1,149 +0,0 @@ -# -# Copyright (C) 2013-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -include "myconfig.pxi" -from libcpp cimport bool -from .analyze cimport PartCfg, partCfg - -cdef extern from "SystemInterface.hpp": - cdef cppclass SystemInterface: - pass -cdef extern from "EspressoSystemInterface.hpp": - cdef cppclass EspressoSystemInterface(SystemInterface): - @staticmethod - EspressoSystemInterface & Instance() - - -IF ELECTROSTATICS: - cdef extern from "electrostatics_magnetostatics/common.hpp": - void mpi_bcast_coulomb_params() - - IF P3M: - from p3m_common cimport P3MParameters - - cdef extern from "electrostatics_magnetostatics/coulomb.hpp": - - cdef enum CoulombMethod: - COULOMB_NONE, \ - COULOMB_DH, \ - COULOMB_P3M, \ - COULOMB_MMM1D, \ - COULOMB_ELC_P3M, \ - COULOMB_RF, \ - COULOMB_P3M_GPU, \ - COULOMB_MMM1D_GPU, \ - COULOMB_SCAFACOS - - ctypedef struct Coulomb_parameters: - double prefactor - CoulombMethod method - - cdef extern Coulomb_parameters coulomb - - cdef extern from "electrostatics_magnetostatics/coulomb.hpp" namespace "Coulomb": - - int set_prefactor(double prefactor) except + - void deactivate_method() - - IF P3M: - from p3m_common cimport P3MParameters - - cdef extern from "electrostatics_magnetostatics/p3m.hpp": - void p3m_set_params(double r_cut, int * mesh, int cao, double alpha, double accuracy) except + - void p3m_set_tune_params(double r_cut, int mesh[3], int cao, double accuracy) - void p3m_set_mesh_offset(double x, double y, double z) except + - void p3m_set_eps(double eps) - int p3m_adaptive_tune(int timings, bool verbose) - - ctypedef struct p3m_data_struct: - P3MParameters params - - # links intern C-struct with python object - cdef extern p3m_data_struct p3m - - IF CUDA: - cdef extern from "electrostatics_magnetostatics/p3m_gpu.hpp": - void p3m_gpu_init(int cao, int * mesh, double alpha) except + - - cdef extern from "electrostatics_magnetostatics/elc.hpp": - ctypedef struct ELCParameters: - double maxPWerror - double gap_size - double far_cut - bool neutralize - double delta_mid_top - double delta_mid_bot - bool const_pot - double pot_diff - - void ELC_set_params(double maxPWerror, double min_dist, double far_cut, - bool neutralize, double delta_mid_top, - double delta_mid_bot, bool const_pot, double pot_diff) except + - - # links intern C-struct with python object - ELCParameters elc_params - - cdef extern from "electrostatics_magnetostatics/debye_hueckel.hpp": - ctypedef struct DebyeHueckelParameters: - double r_cut - double kappa - - cdef extern DebyeHueckelParameters dh_params - - void dh_set_params(double kappa, double r_cut) except + - - cdef extern from "electrostatics_magnetostatics/reaction_field.hpp": - ctypedef struct ReactionFieldParameters: - double kappa - double epsilon1 - double epsilon2 - double r_cut - - cdef extern ReactionFieldParameters rf_params - - void rf_set_params(double kappa, double epsilon1, double epsilon2, - double r_cut) except + - -IF ELECTROSTATICS: - cdef extern from "electrostatics_magnetostatics/mmm1d.hpp": - ctypedef struct MMM1DParameters: - double far_switch_radius_2 - double maxPWerror - int bessel_cutoff - - cdef extern MMM1DParameters mmm1d_params - - void MMM1D_set_params(double switch_rad, double maxPWerror) - int MMM1D_init() - int mmm1d_tune(int timings, bool verbose) - -IF ELECTROSTATICS and MMM1D_GPU: - - cdef extern from "actor/Mmm1dgpuForce.hpp": - cdef cppclass Mmm1dgpuForce: - Mmm1dgpuForce(SystemInterface & s) except+ - void setup(SystemInterface & s) - void tune(SystemInterface & s, float _maxPWerror, float _far_switch_radius, int _bessel_cutoff) - void set_params(float _boxz, float _coulomb_prefactor, float _maxPWerror, float _far_switch_radius, int _bessel_cutoff) - - void activate() - void deactivate() - -cdef extern from "utils/checks/charge_neutrality.hpp" namespace "Utils": - bool check_charge_neutrality[ParticleRange](ParticleRange & partCfg) diff --git a/src/python/espressomd/electrostatics.py b/src/python/espressomd/electrostatics.py new file mode 100644 index 00000000000..940b600282d --- /dev/null +++ b/src/python/espressomd/electrostatics.py @@ -0,0 +1,549 @@ +# +# Copyright (C) 2013-2019 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from . import utils +from .script_interface import ScriptInterfaceHelper, script_interface_register +from .__init__ import has_features + + +class ElectrostaticInteraction(ScriptInterfaceHelper): + """ + Common interface for electrostatics solvers. + + Parameters + ---------- + prefactor : :obj:`float` + Electrostatics prefactor :math:`\\frac{1}{4\\pi\\varepsilon_0\\varepsilon_r}` + + """ + _so_creation_policy = "GLOBAL" + + def __init__(self, **kwargs): + self._check_required_features() + + if 'sip' not in kwargs: + params = self.default_params() + params.update(kwargs) + self.validate_params(params) + super().__init__(**params) + else: + super().__init__(**kwargs) + + def _check_required_features(self): + if not has_features("ELECTROSTATICS"): + raise NotImplementedError("Feature ELECTROSTATICS not compiled in") + + def validate_params(self, params): + """Check validity of given parameters. + """ + utils.check_type_or_throw_except( + params["prefactor"], 1, float, "prefactor should be a double") + + def default_params(self): + raise NotImplementedError("Derived classes must implement this method") + + def valid_keys(self): + raise NotImplementedError("Derived classes must implement this method") + + def required_keys(self): + raise NotImplementedError("Derived classes must implement this method") + + def _activate(self): + self.call_method("check_charge_neutrality") + self.call_method("activate") + utils.handle_errors("Coulomb actor activation failed") + + def _deactivate(self): + self.call_method("deactivate") + utils.handle_errors("Coulomb actor deactivation failed") + + +@script_interface_register +class DH(ElectrostaticInteraction): + """ + Electrostatics solver based on the Debye-Hueckel framework. + See :ref:`Debye-Hückel potential` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Electrostatics prefactor (see :eq:`coulomb_prefactor`). + kappa : :obj:`float` + Inverse Debye screening length. + r_cut : :obj:`float` + Cutoff radius for this interaction. + + """ + _so_name = "Coulomb::DebyeHueckel" + _so_creation_policy = "GLOBAL" + + def valid_keys(self): + return {"prefactor", "kappa", "r_cut", "check_neutrality"} + + def required_keys(self): + return {"prefactor", "kappa", "r_cut"} + + def default_params(self): + return {"check_neutrality": True} + + +@script_interface_register +class ReactionField(ElectrostaticInteraction): + """ + Electrostatics solver based on the Reaction Field framework. + See :ref:`Reaction Field method` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Electrostatics prefactor (see :eq:`coulomb_prefactor`). + kappa : :obj:`float` + Inverse Debye screening length. + epsilon1 : :obj:`float` + interior dielectric constant + epsilon2 : :obj:`float` + exterior dielectric constant + r_cut : :obj:`float` + Cutoff radius for this interaction. + + """ + _so_name = "Coulomb::ReactionField" + _so_creation_policy = "GLOBAL" + + def valid_keys(self): + return {"prefactor", "kappa", "epsilon1", "epsilon2", "r_cut", + "check_neutrality"} + + def required_keys(self): + return {"prefactor", "kappa", "epsilon1", "epsilon2", "r_cut"} + + def default_params(self): + return {"check_neutrality": True} + + +class _P3MBase(ElectrostaticInteraction): + def valid_keys(self): + return {"mesh", "cao", "accuracy", "epsilon", "alpha", "r_cut", + "prefactor", "tune", "check_neutrality", "timings", + "verbose", "mesh_off"} + + def required_keys(self): + return {"prefactor", "accuracy"} + + def default_params(self): + return {"cao": -1, + "r_cut": -1., + "alpha": -1., + "mesh": [-1, -1, -1], + "epsilon": 0., + "mesh_off": [-1., -1., -1.], + "prefactor": 0., + "check_neutrality": True, + "tune": True, + "timings": 10, + "verbose": True} + + def validate_params(self, params): + super().validate_params(params) + + if utils.is_valid_type(params["mesh"], int): + params["mesh"] = 3 * [params["mesh"]] + utils.check_type_or_throw_except(params["mesh"], 3, int, + "P3M mesh has to be an integer or integer list of length 3") + if (params["mesh"][0] % 2 != 0 and params["mesh"][0] != -1) or \ + (params["mesh"][1] % 2 != 0 and params["mesh"][1] != -1) or \ + (params["mesh"][2] % 2 != 0 and params["mesh"][2] != -1): + raise ValueError( + "P3M requires an even number of mesh points in all directions") + + if params["epsilon"] == "metallic": + params["epsilon"] = 0.0 + + utils.check_type_or_throw_except( + params["epsilon"], 1, float, + "epsilon should be a double or 'metallic'") + + utils.check_type_or_throw_except( + params["mesh_off"], 3, float, + "mesh_off should be a (3,) array_like of values between 0 and 1") + + if not utils.is_valid_type(params["timings"], int): + raise TypeError("P3M timings has to be an integer") + if params["timings"] <= 0: + raise ValueError("P3M timings must be > 0") + if not utils.is_valid_type(params["tune"], bool): + raise TypeError("P3M tune has to be a boolean") + + +@script_interface_register +class P3M(_P3MBase): + """ + P3M electrostatics solver. + + Particle--Particle--Particle--Mesh (P3M) is a Fourier-based Ewald + summation method to calculate potentials in N-body simulation. + See :ref:`Coulomb P3M` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Electrostatics prefactor (see :eq:`coulomb_prefactor`). + accuracy : :obj:`float` + P3M tunes its parameters to provide this target accuracy. + alpha : :obj:`float`, optional + The Ewald parameter. + cao : :obj:`float`, optional + The charge-assignment order, an integer between 1 and 7. + epsilon : :obj:`float` or :obj:`str`, optional + A positive number for the dielectric constant of the + surrounding medium. Use ``'metallic'`` to set the dielectric + constant of the surrounding medium to infinity (default). + mesh : :obj:`int` or (3,) array_like of :obj:`int`, optional + The number of mesh points in x, y and z direction. Use a single + value for cubic boxes. + mesh_off : (3,) array_like of :obj:`float`, optional + Mesh offset. + r_cut : :obj:`float`, optional + The real space cutoff. + tune : :obj:`bool`, optional + Used to activate/deactivate the tuning method on activation. + Defaults to ``True``. + timings : :obj:`int` + Number of force calculations during tuning. + verbose : :obj:`bool`, optional + If ``False``, disable log output during tuning. + check_neutrality : :obj:`bool`, optional + Raise a warning if the system is not electrically neutral when + set to ``True`` (default). + + """ + _so_name = "Coulomb::CoulombP3M" + _so_creation_policy = "GLOBAL" + + def _check_required_features(self): + if not has_features("P3M"): + raise NotImplementedError("Feature P3M not compiled in") + + +@script_interface_register +class P3MGPU(_P3MBase): + """ + P3M electrostatics solver with GPU support. + + Particle--Particle--Particle--Mesh (P3M) is a Fourier-based Ewald + summation method to calculate potentials in N-body simulation. + See :ref:`Coulomb P3M on GPU` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Electrostatics prefactor (see :eq:`coulomb_prefactor`). + accuracy : :obj:`float` + P3M tunes its parameters to provide this target accuracy. + alpha : :obj:`float`, optional + The Ewald parameter. + cao : :obj:`float`, optional + The charge-assignment order, an integer between 0 and 7. + epsilon : :obj:`float` or :obj:`str`, optional + A positive number for the dielectric constant of the + surrounding medium. Use ``'metallic'`` to set the dielectric + constant of the surrounding medium to infinity (default). + mesh : :obj:`int` or (3,) array_like of :obj:`int`, optional + The number of mesh points in x, y and z direction. Use a single + value for cubic boxes. + mesh_off : (3,) array_like of :obj:`float`, optional + Mesh offset. + r_cut : :obj:`float`, optional + The real space cutoff + tune : :obj:`bool`, optional + Used to activate/deactivate the tuning method on activation. + Defaults to ``True``. + timings : :obj:`int` + Number of force calculations during tuning. + verbose : :obj:`bool`, optional + If ``False``, disable log output during tuning. + check_neutrality : :obj:`bool`, optional + Raise a warning if the system is not electrically neutral when + set to ``True`` (default). + + """ + _so_name = "Coulomb::CoulombP3MGPU" + _so_creation_policy = "GLOBAL" + + def _check_required_features(self): + if not has_features("P3M"): + raise NotImplementedError("Feature P3M not compiled in") + if not has_features("CUDA"): + raise NotImplementedError("Feature CUDA not compiled in") + + +@script_interface_register +class ELC(ElectrostaticInteraction): + """ + Electrostatics solver for systems with two periodic dimensions. + See :ref:`Electrostatic Layer Correction (ELC)` for more details. + + Parameters + ---------- + actor : :obj:`P3M`, required + Base P3M actor. + gap_size : :obj:`float`, required + The gap size gives the height :math:`h` of the empty region between + the system box and the neighboring artificial images. |es| checks + that the gap is empty and will throw an error if it isn't. Therefore + you should really make sure that the gap region is empty (e.g. + with wall constraints). + maxPWerror : :obj:`float`, required + The maximal pairwise error sets the least upper bound (LUB) error + of the force between any two charges without prefactors (see the + papers). The algorithm tries to find parameters to meet this LUB + requirements or will throw an error if there are none. + delta_mid_top : :obj:`float`, optional + Dielectric contrast :math:`\\Delta_t` between the upper boundary + and the simulation box. Value between -1 and +1 (inclusive). + delta_mid_bottom : :obj:`float`, optional + Dielectric contrast :math:`\\Delta_b` between the lower boundary + and the simulation box. Value between -1 and +1 (inclusive). + const_pot : :obj:`bool`, optional + Activate a constant electric potential between the top and bottom + of the simulation box. + pot_diff : :obj:`float`, optional + If ``const_pot`` is enabled, this parameter controls the applied + voltage between the boundaries of the simulation box in the + *z*-direction (at :math:`z = 0` and :math:`z = L_z - h`). + neutralize : :obj:`bool`, optional + By default, *ELC* just as P3M adds a homogeneous neutralizing + background to the system in case of a net charge. However, unlike + in three dimensions, this background adds a parabolic potential + across the slab :cite:`ballenegger09a`. Therefore, under normal + circumstances, you will probably want to disable the neutralization + for non-neutral systems. This corresponds then to a formal + regularization of the forces and energies :cite:`ballenegger09a`. + Also, if you add neutralizing walls explicitly as constraints, you + have to disable the neutralization. When using a dielectric + contrast or full metallic walls (``delta_mid_top != 0`` or + ``delta_mid_bot != 0`` or ``const_pot=True``), ``neutralize`` is + overwritten and switched off internally. Note that the special + case of non-neutral systems with a *non-metallic* dielectric jump + (e.g. ``delta_mid_top`` or ``delta_mid_bot`` in ``]-1,1[``) is not + covered by the algorithm and will throw an error. + far_cut : :obj:`float`, optional + Cutoff radius, use with care, intended for testing purposes. When + setting the cutoff directly, the maximal pairwise error is ignored. + """ + _so_name = "Coulomb::ElectrostaticLayerCorrection" + _so_creation_policy = "GLOBAL" + + def _check_required_features(self): + if not has_features("P3M"): + raise NotImplementedError("Feature P3M not compiled in") + + def validate_params(self, params): + utils.check_type_or_throw_except( + params["maxPWerror"], 1, float, "maxPWerror has to be a float") + utils.check_type_or_throw_except( + params["gap_size"], 1, float, "gap_size has to be a float") + utils.check_type_or_throw_except( + params["far_cut"], 1, float, "far_cut has to be a float") + utils.check_type_or_throw_except( + params["neutralize"], 1, bool, "neutralize has to be a bool") + + def valid_keys(self): + return {"actor", "maxPWerror", "gap_size", "far_cut", + "neutralize", "delta_mid_top", "delta_mid_bot", + "const_pot", "pot_diff", "check_neutrality"} + + def required_keys(self): + return {"actor", "maxPWerror", "gap_size"} + + def default_params(self): + return {"far_cut": -1., + "delta_mid_top": 0., + "delta_mid_bot": 0., + "const_pot": False, + "pot_diff": 0., + "neutralize": True, + "check_neutrality": True} + + +@script_interface_register +class MMM1D(ElectrostaticInteraction): + """ + Electrostatics solver for systems with one periodic direction. + See :ref:`MMM1D` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Electrostatics prefactor (see :eq:`coulomb_prefactor`). + maxWPerror : :obj:`float` + Maximal pairwise error. + far_switch_radius : :obj:`float`, optional + Radius where near-field and far-field calculation are switched. + bessel_cutoff : :obj:`int`, optional + tune : :obj:`bool`, optional + Specify whether to automatically tune or not. Defaults to ``True``. + timings : :obj:`int` + Number of force calculations during tuning. + + """ + _so_name = "Coulomb::CoulombMMM1D" + _so_creation_policy = "GLOBAL" + + def validate_params(self, params): + default_params = self.default_params() + if params["prefactor"] <= 0: + raise ValueError("prefactor should be a positive float") + if params["maxPWerror"] < 0 and params["maxPWerror"] != default_params["maxPWerror"]: + raise ValueError("maxPWerror should be a positive double") + if params["far_switch_radius"] < 0 and params["far_switch_radius"] != default_params["far_switch_radius"]: + raise ValueError("switch radius should be a positive double") + + def default_params(self): + return {"far_switch_radius": -1., + "verbose": True, + "timings": 15, + "tune": True, + "check_neutrality": True} + + def valid_keys(self): + return {"prefactor", "maxPWerror", "far_switch_radius", + "verbose", "timings", "tune", "check_neutrality"} + + def required_keys(self): + return {"prefactor", "maxPWerror"} + + +@script_interface_register +class MMM1DGPU(ElectrostaticInteraction): + """ + Electrostatics solver with GPU support for systems with one periodic + direction. See :ref:`MMM1D on GPU` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Electrostatics prefactor (see :eq:`coulomb_prefactor`). + maxWPerror : :obj:`float` + Maximal pairwise error. + far_switch_radius : :obj:`float`, optional + Radius where near-field and far-field calculation are switched + bessel_cutoff : :obj:`int`, optional + tune : :obj:`bool`, optional + Specify whether to automatically tune or not. Defaults to ``True``. + """ + _so_name = "Coulomb::CoulombMMM1DGpu" + _so_creation_policy = "GLOBAL" + + def _check_required_features(self): + if not has_features("MMM1D_GPU"): + raise NotImplementedError("Feature MMM1D_GPU not compiled in") + + def validate_params(self, params): + default_params = self.default_params() + if params["prefactor"] <= 0: + raise ValueError("prefactor should be a positive float") + if params["maxPWerror"] < 0 and params["maxPWerror"] != default_params["maxPWerror"]: + raise ValueError("maxPWerror should be a positive double") + if params["far_switch_radius"] < 0 and params["far_switch_radius"] != default_params["far_switch_radius"]: + raise ValueError("switch radius should be a positive double") + if params["bessel_cutoff"] < 0 and params["bessel_cutoff"] != default_params["bessel_cutoff"]: + raise ValueError("bessel_cutoff should be a positive integer") + + def default_params(self): + return {"far_switch_radius": -1., + "bessel_cutoff": -1, + "tune": True, + "check_neutrality": True} + + def valid_keys(self): + return {"prefactor", "maxPWerror", "far_switch_radius", + "bessel_cutoff", "tune", "check_neutrality"} + + def required_keys(self): + return {"prefactor", "maxPWerror"} + + +@script_interface_register +class Scafacos(ElectrostaticInteraction): + + """ + Calculate the Coulomb interaction using the ScaFaCoS library. + See :ref:`ScaFaCoS electrostatics` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Coulomb prefactor as defined in :eq:`coulomb_prefactor`. + method_name : :obj:`str` + Name of the ScaFaCoS method to use. + method_params : :obj:`dict` + Dictionary containing the method-specific parameters. + + Methods + ------- + get_available_methods() + List long-range methods available in the ScaFaCoS library. + + set_near_field_delegation() + Choose whether to delegate short-range calculation to ESPResSo + (this is the default when the method supports it) or ScaFaCos. + + Parameters + ---------- + delegate : :obj:`bool` + Delegate to ESPResSo if ``True`` and the method supports it. + + get_near_field_delegation() + Find whether the short-range calculation is delegated to ESPResSo + (this is the default when the method supports it) or ScaFaCos. + + Returns + ------- + delegate : :obj:`bool` + Delegate to ESPResSo if ``True`` and the method supports it, + ``False`` if delegated to ScaFaCoS or the method doesn't have a + short-range kernel. + + """ + _so_name = "Coulomb::CoulombScafacos" + _so_creation_policy = "GLOBAL" + _so_bind_methods = ElectrostaticInteraction._so_bind_methods + \ + ("get_available_methods", + "get_near_field_delegation", + "set_near_field_delegation") + + def _check_required_features(self): + if not has_features("ELECTROSTATICS"): + raise NotImplementedError("Feature ELECTROSTATICS not compiled in") + if not has_features("SCAFACOS"): + raise NotImplementedError("Feature SCAFACOS not compiled in") + + def validate_params(self, params): + pass + + def default_params(self): + return {"check_neutrality": True} + + def valid_keys(self): + return {"method_name", "method_params", + "prefactor", "check_neutrality"} + + def required_keys(self): + return {"method_name", "method_params", "prefactor"} diff --git a/src/python/espressomd/electrostatics.pyx b/src/python/espressomd/electrostatics.pyx deleted file mode 100644 index 50e4e80ccf0..00000000000 --- a/src/python/espressomd/electrostatics.pyx +++ /dev/null @@ -1,737 +0,0 @@ -# -# Copyright (C) 2013-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -from libcpp.memory cimport shared_ptr, make_shared -from cython.operator cimport dereference -include "myconfig.pxi" -from .actors cimport Actor -from .grid cimport box_geo -import numpy as np -IF SCAFACOS == 1: - from .scafacos import ScafacosConnector - from . cimport scafacos -from . import utils -from .utils import is_valid_type, check_type_or_throw_except, handle_errors -from .analyze cimport partCfg, PartCfg -from .particle_data cimport particle -import sys - - -IF ELECTROSTATICS == 1: - def check_neutrality(_params): - if _params.get("check_neutrality", False): - if not check_charge_neutrality[PartCfg](partCfg()): - raise Exception(' '.join(""" - The system is not charge neutral. Please - neutralize the system before adding a new actor by adding - the corresponding counterions to the system. Alternatively - you can turn off the electroneutrality check by supplying - check_neutrality=False when creating the actor. In this - case you may be simulating a non-neutral system which will - affect physical observables like e.g. the pressure, the - chemical potentials of charged species or potential - energies of the system. Since simulations of non charge - neutral systems are special please make sure you know what - you are doing. - """.split())) - - cdef class ElectrostaticInteraction(Actor): - def _tune(self): - raise Exception( - "Subclasses of ElectrostaticInteraction must define the " - "_tune() method or chosen method does not support tuning.") - - def _set_params_in_es_core(self): - raise Exception( - "Subclasses of ElectrostaticInteraction must define the " - "_set_params_in_es_core() method.") - - def _deactivate_method(self): - deactivate_method() - handle_errors("Coulomb method deactivation") - - def tune(self, **tune_params): - utils.check_valid_keys(self.valid_keys(), tune_params.keys()) - self._params.update(tune_params) - self._tune() - - -IF ELECTROSTATICS: - cdef class DH(ElectrostaticInteraction): - """ - Electrostatics solver based on the Debye-Hueckel framework. See - :ref:`Debye-Hückel potential` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Electrostatics prefactor (see :eq:`coulomb_prefactor`). - kappa : :obj:`float` - Inverse Debye screening length. - r_cut : :obj:`float` - Cutoff radius for this interaction. - - """ - - def validate_params(self): - pass - - def valid_keys(self): - return {"prefactor", "kappa", "r_cut", "check_neutrality"} - - def required_keys(self): - return {"prefactor", "kappa", "r_cut"} - - def _set_params_in_es_core(self): - set_prefactor(self._params["prefactor"]) - dh_set_params(self._params["kappa"], self._params["r_cut"]) - - def _get_params_from_es_core(self): - params = {} - params.update(dh_params) - return params - - def _activate_method(self): - check_neutrality(self._params) - coulomb.method = COULOMB_DH - self._set_params_in_es_core() - - def default_params(self): - return {"prefactor": -1, - "kappa": -1, - "r_cut": -1, - "check_neutrality": True} - - cdef class ReactionField(ElectrostaticInteraction): - """ - Electrostatics solver based on the Reaction Field framework. - See :ref:`Reaction Field method` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Electrostatics prefactor (see :eq:`coulomb_prefactor`). - kappa : :obj:`float` - Inverse Debye screening length. - epsilon1 : :obj:`float` - interior dielectric constant - epsilon2 : :obj:`float` - exterior dielectric constant - r_cut : :obj:`float` - Cutoff radius for this interaction. - - """ - - def validate_params(self): - pass - - def valid_keys(self): - return {"prefactor", "kappa", "epsilon1", "epsilon2", "r_cut", - "check_neutrality"} - - def required_keys(self): - return {"prefactor", "kappa", "epsilon1", "epsilon2", "r_cut"} - - def _set_params_in_es_core(self): - set_prefactor(self._params["prefactor"]) - rf_set_params( - self._params["kappa"], - self._params["epsilon1"], - self._params["epsilon2"], - self._params["r_cut"]) - - def _get_params_from_es_core(self): - params = {} - params.update(rf_params) - return params - - def _activate_method(self): - check_neutrality(self._params) - coulomb.method = COULOMB_RF - self._set_params_in_es_core() - - def default_params(self): - return {"prefactor": -1, - "kappa": -1, - "epsilon1": -1, - "epsilon2": -1, - "r_cut": -1, - "check_neutrality": True} - - -IF P3M == 1: - cdef class _P3MBase(ElectrostaticInteraction): - - cdef _check_and_copy_mesh_size(self, int mesh[3], pmesh): - if is_valid_type(pmesh, int): - pmesh = 3 * [pmesh] - else: - check_type_or_throw_except( - pmesh, 3, int, "mesh size must be 3 ints") - for i in range(3): - mesh[i] = pmesh[i] - - def valid_keys(self): - return {"mesh", "cao", "accuracy", "epsilon", "alpha", "r_cut", - "prefactor", "tune", "check_neutrality", "timings", - "verbose", "mesh_off"} - - def required_keys(self): - return {"prefactor", "accuracy"} - - def default_params(self): - return {"cao": 0, - "r_cut": -1, - "alpha": 0, - "accuracy": 0, - "mesh": [0, 0, 0], - "epsilon": 0.0, - "mesh_off": [-1, -1, -1], - "tune": True, - "timings": 10, - "check_neutrality": True, - "verbose": True} - - def _get_params_from_es_core(self): - params = {} - params.update(p3m.params) - params["prefactor"] = coulomb.prefactor - params["tune"] = self._params["tune"] - params["timings"] = self._params["timings"] - return params - - def _tune(self): - cdef int mesh[3] - self._check_and_copy_mesh_size(mesh, self._params["mesh"]) - - set_prefactor(self._params["prefactor"]) - p3m_set_eps(self._params["epsilon"]) - p3m_set_tune_params(self._params["r_cut"], mesh, - self._params["cao"], self._params["accuracy"]) - tuning_error = p3m_adaptive_tune( - self._params["timings"], self._params["verbose"]) - if tuning_error: - handle_errors("P3M: tuning failed") - self._params.update(self._get_params_from_es_core()) - - def tune(self, **tune_params_subset): - # update the three necessary parameters if not provided by the user - default_params = self.default_params() - for key in ["r_cut", "mesh", "cao"]: - if key not in tune_params_subset: - tune_params_subset[key] = default_params[key] - - super().tune(**tune_params_subset) - - def _set_params_in_es_core(self): - cdef int mesh[3] - self._check_and_copy_mesh_size(mesh, self._params["mesh"]) - - set_prefactor(self._params["prefactor"]) - # Sets p3m parameters - # p3m_set_params() -> set parameters and bcasts - # Careful: calls on_coulomb_change(), which calls p3m_init(), - # which resets r_cut if prefactor=0 - p3m_set_params(self._params["r_cut"], mesh, self._params["cao"], - self._params["alpha"], self._params["accuracy"]) - # Sets eps, bcast - p3m_set_eps(self._params["epsilon"]) - p3m_set_mesh_offset(self._params["mesh_off"][0], - self._params["mesh_off"][1], - self._params["mesh_off"][2]) - - def validate_params(self): - default_params = self.default_params() - if not (self._params["prefactor"] > 0.0): - raise ValueError("prefactor should be a positive float") - - if is_valid_type(self._params["mesh"], int): - if self._params["mesh"] % 2 != 0 and self._params["mesh"] != -1: - raise ValueError( - "P3M requires an even number of mesh points in all directions") - else: - check_type_or_throw_except(self._params["mesh"], 3, int, - "P3M mesh has to be an integer or integer list of length 3") - if (self._params["mesh"][0] % 2 != 0 and self._params["mesh"][0] != -1) or \ - (self._params["mesh"][1] % 2 != 0 and self._params["mesh"][1] != -1) or \ - (self._params["mesh"][2] % 2 != 0 and self._params["mesh"][2] != -1): - raise ValueError( - "P3M requires an even number of mesh points in all directions") - - if self._params["epsilon"] == "metallic": - self._params["epsilon"] = 0.0 - - check_type_or_throw_except( - self._params["epsilon"], 1, float, - "epsilon should be a double or 'metallic'") - - if self._params["mesh_off"] != default_params["mesh_off"]: - check_type_or_throw_except(self._params["mesh_off"], 3, float, - "mesh_off should be a (3,) array_like of values between 0.0 and 1.0") - - if not is_valid_type(self._params["timings"], int): - raise TypeError("DipolarP3M timings has to be an integer") - if self._params["timings"] <= 0: - raise ValueError("DipolarP3M timings must be > 0") - - cdef class P3M(_P3MBase): - """ - P3M electrostatics solver. - - Particle--Particle--Particle--Mesh (P3M) is a Fourier-based Ewald - summation method to calculate potentials in N-body simulation. - See :ref:`Coulomb P3M` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Electrostatics prefactor (see :eq:`coulomb_prefactor`). - accuracy : :obj:`float` - P3M tunes its parameters to provide this target accuracy. - alpha : :obj:`float`, optional - The Ewald parameter. - cao : :obj:`float`, optional - The charge-assignment order, an integer between 0 and 7. - epsilon : :obj:`float` or :obj:`str`, optional - A positive number for the dielectric constant of the - surrounding medium. Use ``'metallic'`` to set the dielectric - constant of the surrounding medium to infinity (default). - mesh : :obj:`int` or (3,) array_like of :obj:`int`, optional - The number of mesh points in x, y and z direction. Use a single - value for cubic boxes. - r_cut : :obj:`float`, optional - The real space cutoff. - tune : :obj:`bool`, optional - Used to activate/deactivate the tuning method on activation. - Defaults to ``True``. - timings : :obj:`int` - Number of force calculations during tuning. - verbose : :obj:`bool`, optional - If ``False``, disable log output during tuning. - check_neutrality : :obj:`bool`, optional - Raise a warning if the system is not electrically neutral when - set to ``True`` (default). - - """ - - def _activate_method(self): - check_neutrality(self._params) - if self._params["tune"]: - self._tune() - self._set_params_in_es_core() - handle_errors("P3M: initialization failed") - - IF CUDA: - cdef class P3MGPU(_P3MBase): - """ - P3M electrostatics solver with GPU support. - - Particle--Particle--Particle--Mesh (P3M) is a Fourier-based Ewald - summation method to calculate potentials in N-body simulation. - See :ref:`Coulomb P3M on GPU` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Electrostatics prefactor (see :eq:`coulomb_prefactor`). - accuracy : :obj:`float` - P3M tunes its parameters to provide this target accuracy. - alpha : :obj:`float`, optional - The Ewald parameter. - cao : :obj:`float`, optional - The charge-assignment order, an integer between 0 and 7. - epsilon : :obj:`float` or :obj:`str`, optional - A positive number for the dielectric constant of the - surrounding medium. Use ``'metallic'`` to set the dielectric - constant of the surrounding medium to infinity (default). - mesh : :obj:`int` or (3,) array_like of :obj:`int`, optional - The number of mesh points in x, y and z direction. Use a single - value for cubic boxes. - r_cut : :obj:`float`, optional - The real space cutoff - tune : :obj:`bool`, optional - Used to activate/deactivate the tuning method on activation. - Defaults to ``True``. - timings : :obj:`int` - Number of force calculations during tuning. - verbose : :obj:`bool`, optional - If ``False``, disable log output during tuning. - check_neutrality : :obj:`bool`, optional - Raise a warning if the system is not electrically neutral when - set to ``True`` (default). - - """ - - def _activate_method(self): - cdef int mesh[3] - self._check_and_copy_mesh_size(mesh, self._params["mesh"]) - - check_neutrality(self._params) - p3m_gpu_init(self._params["cao"], mesh, self._params["alpha"]) - handle_errors("P3M: tuning failed") - coulomb.method = COULOMB_P3M_GPU - if self._params["tune"]: - self._tune() - p3m_gpu_init(self._params["cao"], mesh, self._params["alpha"]) - handle_errors("P3M: tuning failed") - self._set_params_in_es_core() - - def _set_params_in_es_core(self): - super()._set_params_in_es_core() - handle_errors("P3M: initialization failed") - - cdef class ELC(ElectrostaticInteraction): - """ - Electrostatics solver for systems with two periodic dimensions. - See :ref:`Electrostatic Layer Correction (ELC)` for more details. - - Parameters - ---------- - p3m_actor : :obj:`P3M`, required - Base P3M actor. - gap_size : :obj:`float`, required - The gap size gives the height :math:`h` of the empty region between - the system box and the neighboring artificial images. |es| checks - that the gap is empty and will throw an error if it isn't. Therefore - you should really make sure that the gap region is empty (e.g. - with wall constraints). - maxPWerror : :obj:`float`, required - The maximal pairwise error sets the least upper bound (LUB) error - of the force between any two charges without prefactors (see the - papers). The algorithm tries to find parameters to meet this LUB - requirements or will throw an error if there are none. - delta_mid_top : :obj:`float`, optional - Dielectric contrast :math:`\\Delta_t` between the upper boundary - and the simulation box. - delta_mid_bottom : :obj:`float`, optional - Dielectric contrast :math:`\\Delta_b` between the lower boundary - and the simulation box. - const_pot : :obj:`bool`, optional - Activate a constant electric potential between the top and bottom - of the simulation box. - pot_diff : :obj:`float`, optional - If ``const_pot`` is enabled, this parameter controls the applied - voltage between the boundaries of the simulation box in the - *z*-direction (at :math:`z = 0` and :math:`z = L_z - h`). - neutralize : :obj:`bool`, optional - By default, *ELC* just as P3M adds a homogeneous neutralizing - background to the system in case of a net charge. However, unlike - in three dimensions, this background adds a parabolic potential - across the slab :cite:`ballenegger09a`. Therefore, under normal - circumstances, you will probably want to disable the neutralization - for non-neutral systems. This corresponds then to a formal - regularization of the forces and energies :cite:`ballenegger09a`. - Also, if you add neutralizing walls explicitly as constraints, you - have to disable the neutralization. When using a dielectric - contrast or full metallic walls (``delta_mid_top != 0`` or - ``delta_mid_bot != 0`` or ``const_pot=True``), ``neutralize`` is - overwritten and switched off internally. Note that the special - case of non-neutral systems with a *non-metallic* dielectric jump - (e.g. ``delta_mid_top`` or ``delta_mid_bot`` in ``]-1,1[``) is not - covered by the algorithm and will throw an error. - far_cut : :obj:`float`, optional - Cutoff radius, use with care, intended for testing purposes. When - setting the cutoff directly, the maximal pairwise error is ignored. - """ - - def validate_params(self): - # P3M - if CUDA: - if isinstance(self._params["p3m_actor"], P3MGPU): - raise ValueError( - "ELC is not set up to work with the GPU P3M") - check_type_or_throw_except( - self._params["p3m_actor"], 1, getattr( - sys.modules[__name__], "P3M"), - "p3m_actor has to be a P3M solver") - self._params["p3m_actor"]._params["epsilon"] = 0.0 - self._params["p3m_actor"].validate_params() - # ELC - check_type_or_throw_except( - self._params["maxPWerror"], 1, float, - "maxPWerror has to be a float") - check_type_or_throw_except(self._params["gap_size"], 1, float, - "gap_size has to be a float") - check_type_or_throw_except(self._params["far_cut"], 1, float, - "far_cut has to be a float") - check_type_or_throw_except( - self._params["neutralize"], 1, type(True), - "neutralize has to be a bool") - - def valid_keys(self): - return {"p3m_actor", "maxPWerror", "gap_size", "far_cut", - "neutralize", "delta_mid_top", "delta_mid_bot", - "const_pot", "pot_diff", "check_neutrality"} - - def required_keys(self): - return {"p3m_actor", "maxPWerror", "gap_size"} - - def default_params(self): - return {"maxPWerror": -1, - "gap_size": -1, - "far_cut": -1, - "delta_mid_top": 0, - "delta_mid_bot": 0, - "const_pot": False, - "pot_diff": 0.0, - "neutralize": True, - "check_neutrality": True} - - def _get_params_from_es_core(self): - params = {} - params.update(elc_params) - params["p3m_actor"] = self._params["p3m_actor"] - return params - - def _set_params_in_es_core(self): - self._params["p3m_actor"]._set_params_in_es_core() - if coulomb.method == COULOMB_P3M_GPU: - raise Exception("ELC is not set up to work with the GPU P3M") - - if self._params["const_pot"]: - self._params["delta_mid_top"] = -1 - self._params["delta_mid_bot"] = -1 - - ELC_set_params( - self._params["maxPWerror"], - self._params["gap_size"], - self._params["far_cut"], - self._params["neutralize"], - self._params["delta_mid_top"], - self._params["delta_mid_bot"], - self._params["const_pot"], - self._params["pot_diff"]) - - def tune(self, **tune_params_subset): - self._params["p3m_actor"].tune(**tune_params_subset) - - def _activate_method(self): - self._params["p3m_actor"]._activate_method() - check_neutrality(self._params) - self._set_params_in_es_core() - -IF ELECTROSTATICS: - cdef class MMM1D(ElectrostaticInteraction): - """ - Electrostatics solver for systems with one periodic direction. - See :ref:`MMM1D` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Electrostatics prefactor (see :eq:`coulomb_prefactor`). - maxWPerror : :obj:`float` - Maximal pairwise error. - far_switch_radius : :obj:`float`, optional - Radius where near-field and far-field calculation are switched. - bessel_cutoff : :obj:`int`, optional - tune : :obj:`bool`, optional - Specify whether to automatically tune or not. Defaults to ``True``. - timings : :obj:`int` - Number of force calculations during tuning. - - """ - - def validate_params(self): - default_params = self.default_params() - if self._params["prefactor"] <= 0.: - raise ValueError("Prefactor should be a positive float") - if self._params["maxPWerror"] < 0 and self._params["maxPWerror"] != default_params["maxPWerror"]: - raise ValueError("maxPWerror should be a positive double") - if self._params["far_switch_radius"] < 0 and self._params["far_switch_radius"] != default_params["far_switch_radius"]: - raise ValueError("switch radius should be a positive double") - if self._params["bessel_cutoff"] < 0 and self._params["bessel_cutoff"] != default_params["bessel_cutoff"]: - raise ValueError("bessel_cutoff should be a positive integer") - if not is_valid_type(self._params["timings"], int): - raise TypeError("DipolarP3M timings has to be an integer") - if self._params["timings"] <= 0: - raise ValueError("DipolarP3M timings must be > 0") - - def default_params(self): - return {"prefactor": -1, - "maxPWerror": -1, - "far_switch_radius": -1, - "bessel_cutoff": -1, - "tune": True, - "timings": 1000, - "check_neutrality": True, - "verbose": True} - - def valid_keys(self): - return {"prefactor", "maxPWerror", "far_switch_radius", - "bessel_cutoff", "tune", "check_neutrality", "timings", - "verbose"} - - def required_keys(self): - return {"prefactor", "maxPWerror"} - - def _get_params_from_es_core(self): - params = {} - params.update(mmm1d_params) - params["far_switch_radius"] = np.sqrt( - params["far_switch_radius_2"]) - del params["far_switch_radius_2"] - params["prefactor"] = coulomb.prefactor - params["tune"] = self._params["tune"] - params["timings"] = self._params["timings"] - return params - - def _set_params_in_es_core(self): - set_prefactor(self._params["prefactor"]) - MMM1D_set_params( - self._params["far_switch_radius"], self._params["maxPWerror"]) - - def _tune(self): - resp = MMM1D_init() - if resp: - handle_errors("MMM1D: initialization failed") - resp = mmm1d_tune(self._params["timings"], self._params["verbose"]) - if resp: - handle_errors("MMM1D: tuning failed") - self._params.update(self._get_params_from_es_core()) - - def _activate_method(self): - check_neutrality(self._params) - coulomb.method = COULOMB_MMM1D - self._set_params_in_es_core() - if self._params["tune"]: - self._tune() - - self._set_params_in_es_core() - -IF ELECTROSTATICS and MMM1D_GPU: - cdef class MMM1DGPU(ElectrostaticInteraction): - """ - Electrostatics solver with GPU support for systems with one periodic - direction. See :ref:`MMM1D on GPU` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Electrostatics prefactor (see :eq:`coulomb_prefactor`). - maxWPerror : :obj:`float` - Maximal pairwise error. - far_switch_radius : :obj:`float`, optional - Radius where near-field and far-field calculation are switched - bessel_cutoff : :obj:`int`, optional - tune : :obj:`bool`, optional - Specify whether to automatically tune or not. Defaults to ``True``. - """ - cdef shared_ptr[Mmm1dgpuForce] sip - cdef EspressoSystemInterface * interface - - def __cinit__(self): - self.interface = &EspressoSystemInterface.Instance() - self.sip = make_shared[Mmm1dgpuForce](dereference(self.interface)) - - def validate_params(self): - default_params = self.default_params() - if self._params["prefactor"] <= 0: - raise ValueError("prefactor should be a positive float") - if self._params["maxPWerror"] < 0 and self._params["maxPWerror"] != default_params["maxPWerror"]: - raise ValueError("maxPWerror should be a positive double") - if self._params["far_switch_radius"] < 0 and self._params["far_switch_radius"] != default_params["far_switch_radius"]: - raise ValueError("switch radius should be a positive double") - if self._params["bessel_cutoff"] < 0 and self._params["bessel_cutoff"] != default_params["bessel_cutoff"]: - raise ValueError("bessel_cutoff should be a positive integer") - - def default_params(self): - return {"prefactor": -1, - "maxPWerror": -1.0, - "far_switch_radius": -1.0, - "bessel_cutoff": -1, - "tune": True, - "check_neutrality": True} - - def valid_keys(self): - return {"prefactor", "maxPWerror", "far_switch_radius", - "bessel_cutoff", "tune", "check_neutrality"} - - def required_keys(self): - return {"prefactor", "maxPWerror"} - - def _get_params_from_es_core(self): - params = {} - params.update(mmm1d_params) - params["prefactor"] = coulomb.prefactor - return params - - def _set_params_in_es_core(self): - set_prefactor(self._params["prefactor"]) - default_params = self.default_params() - - dereference(self.sip).set_params( - < float > box_geo.length()[2], < float > coulomb.prefactor, - self._params["maxPWerror"], self._params["far_switch_radius"], - self._params["bessel_cutoff"]) - - def _tune(self): - dereference(self.sip).setup(dereference(self.interface)) - dereference(self.sip).tune( - dereference(self.interface), self._params["maxPWerror"], - self._params["far_switch_radius"], self._params["bessel_cutoff"]) - - def _activate_method(self): - check_neutrality(self._params) - self._set_params_in_es_core() - dereference(self.sip).activate() - coulomb.method = COULOMB_MMM1D_GPU - if self._params["tune"]: - self._tune() - self._set_params_in_es_core() - - def _deactivate_method(self): - dereference(self.sip).deactivate() - -IF ELECTROSTATICS: - IF SCAFACOS == 1: - class Scafacos(ScafacosConnector, ElectrostaticInteraction): - - """ - Calculate the Coulomb interaction using the ScaFaCoS library. - See :ref:`ScaFaCoS electrostatics` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Coulomb prefactor as defined in :eq:`coulomb_prefactor`. - method_name : :obj:`str` - Name of the ScaFaCoS method to use. - method_params : :obj:`dict` - Dictionary containing the method-specific parameters. - """ - - dipolar = False - - # Explicit constructor needed due to multiple inheritance - def __init__(self, *args, **kwargs): - Actor.__init__(self, *args, **kwargs) - - def _activate_method(self): - check_neutrality(self._params) - set_prefactor(self._params["prefactor"]) - self._set_params_in_es_core() - mpi_bcast_coulomb_params() - - def default_params(self): - return {} - - def _deactivate_method(self): - super()._deactivate_method() - scafacos.free_handle(self.dipolar) - mpi_bcast_coulomb_params() diff --git a/src/python/espressomd/gen_code_info.py b/src/python/espressomd/gen_code_info.py index fb69a8417a6..38e02e72370 100644 --- a/src/python/espressomd/gen_code_info.py +++ b/src/python/espressomd/gen_code_info.py @@ -45,26 +45,50 @@ # DO NOT EDIT MANUALLY, CHANGES WILL BE LOST include "myconfig.pxi" +from . import utils def features(): \"\"\"Returns list of features compiled into ESPResSo core\"\"\" - f=[] + f = [] """) -template = """ - IF {0} == 1: - f.append("{0}") -""" - for feature in defs.allfeatures: - cfile.write(template.format(feature)) + cfile.write(f"\n IF {feature} == 1:\n f.append(\"{feature}\")\n") cfile.write(f""" return sorted(f) def all_features(): return {defs.allfeatures} + + +cdef extern from "version.hpp": + cdef const char * ESPRESSO_BUILD_TYPE + + +def build_type(): + \"\"\"Prints the CMake build type. + Can be e.g. Debug, Release, RelWithAssert, RelWithDebInfo, Coverage, etc. + \"\"\" + return utils.to_str(ESPRESSO_BUILD_TYPE) # pylint: disable=undefined-variable + + +from libcpp.string cimport string +from libcpp.vector cimport vector + +IF SCAFACOS: + cdef extern from "script_interface/scafacos/scafacos.hpp" namespace "ScriptInterface::Scafacos": + vector[string] available_methods() + +def scafacos_methods(): + \"\"\"Lists long-range methods available in the ScaFaCoS library.\"\"\" + scafacos_features = [] + IF SCAFACOS == 1: + method_names = available_methods() + for method_name in method_names: + scafacos_features.append(method_name.decode('ascii')) + return scafacos_features """) cfile.close() diff --git a/src/python/espressomd/lb.pxd b/src/python/espressomd/lb.pxd index 56b6b1d5beb..db0b436bbd0 100644 --- a/src/python/espressomd/lb.pxd +++ b/src/python/espressomd/lb.pxd @@ -23,14 +23,18 @@ from libcpp.vector cimport vector from libcpp.string cimport string from libc cimport stdint -from .actors cimport Actor from .utils cimport Vector3d from .utils cimport Vector3i from .utils cimport Vector6d from .utils cimport Vector19d from .utils cimport make_array_locked -cdef class HydrodynamicInteraction(Actor): +cdef class FluidActor: + cdef public _isactive + cdef public _params + cdef public system + +cdef class HydrodynamicInteraction(FluidActor): pass cdef class LBFluidRoutines: diff --git a/src/python/espressomd/lb.pyx b/src/python/espressomd/lb.pyx index 92c2704f018..031a3b00cb8 100644 --- a/src/python/espressomd/lb.pyx +++ b/src/python/espressomd/lb.pyx @@ -24,20 +24,161 @@ import functools import numpy as np cimport numpy as np from libc cimport stdint -from .actors cimport Actor +from . import highlander from . import utils from . cimport utils from .utils cimport Vector3i, Vector3d, Vector6d, Vector19d from .integrate cimport get_time_step -def _construct(cls, params): - obj = cls(**params) - obj._params = params - return obj +cdef class FluidActor: + + """ + Abstract base class for interactions affecting particles in the system, + such as LB fluids. Derived classes must implement the interface to the + relevant core objects and global variables. + """ + + # Keys in active_list have to match the method name. + active_list = dict(HydrodynamicInteraction=False) + + # __getstate__ and __setstate__ define the pickle interaction + def __getstate__(self): + odict = self._params.copy() + return odict + + def __setstate__(self, params): + self._params = params + self._set_params_in_es_core() + + def __init__(self, *args, **kwargs): + self._isactive = False + utils.check_valid_keys(self.valid_keys(), kwargs.keys()) + utils.check_required_keys(self.required_keys(), kwargs.keys()) + self._params = self.default_params() + self._params.update(kwargs) + + def _activate(self): + inter = self._get_interaction_type() + if inter in FluidActor.active_list: + if FluidActor.active_list[inter]: + raise highlander.ThereCanOnlyBeOne(self.__class__.__bases__[0]) + FluidActor.active_list[inter] = True + + self.validate_params() + self._activate_method() + utils.handle_errors("Activation of an actor") + self._isactive = True + + def _deactivate(self): + self._deactivate_method() + utils.handle_errors("Deactivation of an actor") + self._isactive = False + inter = self._get_interaction_type() + if inter in FluidActor.active_list: + if not FluidActor.active_list[inter]: + raise Exception( + f"Class not registered in Actor.active_list: {self.__class__.__bases__[0].__name__}") + FluidActor.active_list[inter] = False + + def is_valid(self): + """ + Check if the data stored in this instance still matches the + corresponding data in the core. + """ + temp_params = self._get_params_from_es_core() + if self._params != temp_params: + return False + + # If we're still here, the instance is valid + return True + + def get_params(self): + """Get interaction parameters""" + # If this instance refers to an actual interaction defined in the es + # core, load current parameters from there + if self.is_active(): + update = self._get_params_from_es_core() + self._params.update(update) + return self._params + + def set_params(self, **p): + """Update the given parameters.""" + # Check if keys are valid + utils.check_valid_keys(self.valid_keys(), p.keys()) + + # When an interaction is newly activated, all required keys must be + # given + if not self.is_active(): + utils.check_required_keys(self.required_keys(), p.keys()) + + self._params.update(p) + # validate updated parameters + self.validate_params() + # Put in values given by the user + if self.is_active(): + self._set_params_in_es_core() + + def __str__(self): + return f"{self.__class__.__name__}({self.get_params()})" + + def _get_interaction_type(self): + bases = self.class_lookup(self.__class__) + for i in range(len(bases)): + if bases[i].__name__ in FluidActor.active_list: + return bases[i].__name__ + + def class_lookup(self, cls): + c = list(cls.__bases__) + for base in c: + c.extend(self.class_lookup(base)) + return c + + def is_active(self): + return self._isactive + + def valid_keys(self): + """Virtual method.""" + raise Exception( + f"Subclasses of {self._get_interaction_type()} must define the valid_keys() method.") + + def required_keys(self): + """Virtual method.""" + raise Exception( + "Subclasses of {self._get_interaction_type()} must define the required_keys() method.") + + def validate_params(self): + """Virtual method.""" + raise Exception( + "Subclasses of {self._get_interaction_type()} must define the validate_params() method.") + + def _get_params_from_es_core(self): + """Virtual method.""" + raise Exception( + "Subclasses of {self._get_interaction_type()} must define the _get_params_from_es_core() method.") + + def _set_params_in_es_core(self): + """Virtual method.""" + raise Exception( + "Subclasses of {self._get_interaction_type()} must define the _set_params_in_es_core() method.") + + def default_params(self): + """Virtual method.""" + raise Exception( + "Subclasses of {self._get_interaction_type()} must define the default_params() method.") + + def _activate_method(self): + """Virtual method.""" + raise Exception( + "Subclasses of {self._get_interaction_type()} must define the _activate_method() method.") + + def _deactivate_method(self): + """Virtual method.""" + raise Exception( + "Subclasses of {self._get_interaction_type()} must define the _deactivate_method() method.") -cdef class HydrodynamicInteraction(Actor): +cdef class HydrodynamicInteraction(FluidActor): """ Base class for LB implementations. @@ -77,8 +218,15 @@ cdef class HydrodynamicInteraction(Actor): raise Exception( "Subclasses of HydrodynamicInteraction must define the _lb_init() method.") + @classmethod + def _restore_object(cls, derived_cls, params): + obj = derived_cls(**params) + obj._params = params + return obj + def __reduce__(self): - return _construct, (self.__class__, self._params), None + return (HydrodynamicInteraction._restore_object, + (self.__class__, self._params)) def __getitem__(self, key): cdef Vector3i shape diff --git a/src/python/espressomd/magnetostatic_extensions.pyx b/src/python/espressomd/magnetostatic_extensions.pyx deleted file mode 100644 index 0d51c441f53..00000000000 --- a/src/python/espressomd/magnetostatic_extensions.pyx +++ /dev/null @@ -1,95 +0,0 @@ -# -# Copyright (C) 2013-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from . cimport utils -include "myconfig.pxi" -from .actors import Actor -from .utils import handle_errors -from .utils cimport check_range_or_except, check_type_or_throw_except - -IF DIPOLES: - class MagnetostaticExtension(Actor): - - pass - - class DLC(MagnetostaticExtension): - - """ - Electrostatics solver for systems with two periodic dimensions. - See :ref:`Dipolar Layer Correction (DLC)` for more details. - - Notes - ----- - At present, the empty gap (volume without any particles), is assumed to be - along the z-axis. As a reference for the DLC method, see :cite:`brodka04a`. - - Parameters - ---------- - gap_size : :obj:`float` - The gap size gives the height :math:`h` of the empty region between - the system box and the neighboring artificial images. |es| checks - that the gap is empty and will throw an error if it isn't. Therefore - you should really make sure that the gap region is empty (e.g. - with wall constraints). - maxPWerror : :obj:`float` - Maximal pairwise error of the potential and force. - far_cut : :obj:`float`, optional - Cutoff of the exponential sum. - - """ - - def validate_params(self): - """Check validity of class attributes. - - """ - default_params = self.default_params() - check_type_or_throw_except( - self._params["maxPWerror"], 1, float, - "maxPWerror has to be a float") - check_type_or_throw_except( - self._params["gap_size"], 1, float, - "gap_size has to be a float") - check_type_or_throw_except( - self._params["far_cut"], 1, float, - "far_cut has to be a float") - - def valid_keys(self): - return ["maxPWerror", "gap_size", "far_cut"] - - def required_keys(self): - return ["maxPWerror", "gap_size"] - - def default_params(self): - return {"far_cut": -1} - - def _get_params_from_es_core(self): - params = {} - params.update(dlc_params) - return params - - def _set_params_in_es_core(self): - mdlc_set_params(self._params["maxPWerror"], - self._params["gap_size"], - self._params["far_cut"]) - - def _activate_method(self): - self._set_params_in_es_core() - - def _deactivate_method(self): - pass diff --git a/src/python/espressomd/magnetostatics.pxd b/src/python/espressomd/magnetostatics.pxd deleted file mode 100644 index 45df8411b2f..00000000000 --- a/src/python/espressomd/magnetostatics.pxd +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (C) 2010-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from libcpp cimport bool - -include "myconfig.pxi" - -cdef extern from "SystemInterface.hpp": - cdef cppclass SystemInterface: - pass -cdef extern from "EspressoSystemInterface.hpp": - cdef cppclass EspressoSystemInterface(SystemInterface): - @staticmethod - EspressoSystemInterface & Instance() - -IF DIPOLES == 1: - cdef extern from "electrostatics_magnetostatics/common.hpp": - void mpi_bcast_coulomb_params() - - cdef extern from "electrostatics_magnetostatics/dipole.hpp" namespace "Dipole": - void set_Dprefactor(double prefactor) except + - double get_Dprefactor() - void disable_method_local() - - cdef extern from "electrostatics_magnetostatics/magnetic_non_p3m_methods.hpp": - void dawaanr_set_params() except + - void mdds_set_params(int n_replica) except + - int mdds_get_n_replica() - - IF(CUDA == 1) and (ROTATION == 1): - cdef extern from "actor/DipolarDirectSum.hpp": - cdef cppclass DipolarDirectSum: - DipolarDirectSum(SystemInterface & s) except + - void set_params() - void activate() - void deactivate() - - IF(DIPOLAR_BARNES_HUT == 1): - cdef extern from "actor/DipolarBarnesHut.hpp": - cdef cppclass DipolarBarnesHut: - DipolarBarnesHut(SystemInterface & s) except + - void set_params(float epssq, float itolsq) - void activate() - void deactivate() - -IF DP3M == 1: - from p3m_common cimport P3MParameters - - cdef extern from "electrostatics_magnetostatics/p3m-dipolar.hpp": - void dp3m_set_params(double r_cut, int mesh, int cao, double alpha, double accuracy) except + - void dp3m_set_tune_params(double r_cut, int mesh, int cao, double accuracy) - void dp3m_set_mesh_offset(double x, double y, double z) except + - void dp3m_set_eps(double eps) - int dp3m_adaptive_tune(int timings, bool verbose) - void dp3m_deactivate() - - ctypedef struct dp3m_data_struct: - P3MParameters params - - cdef extern dp3m_data_struct dp3m diff --git a/src/python/espressomd/magnetostatics.py b/src/python/espressomd/magnetostatics.py new file mode 100644 index 00000000000..a34e77e3a29 --- /dev/null +++ b/src/python/espressomd/magnetostatics.py @@ -0,0 +1,391 @@ +# +# Copyright (C) 2013-2019 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from . import utils +from .script_interface import ScriptInterfaceHelper, script_interface_register +from .__init__ import has_features + + +class MagnetostaticInteraction(ScriptInterfaceHelper): + """ + Common interface for magnetostatics solvers. + + Parameters + ---------- + prefactor : :obj:`float` + Magnetostatics prefactor :math:`\\frac{\\mu_0\\mu}{4\\pi}` + + """ + _so_creation_policy = "GLOBAL" + + def __init__(self, **kwargs): + self._check_required_features() + + if 'sip' not in kwargs: + params = self.default_params() + params.update(kwargs) + self.validate_params(params) + super().__init__(**params) + else: + super().__init__(**kwargs) + + def _check_required_features(self): + if not has_features("DIPOLES"): + raise NotImplementedError("Feature DIPOLES not compiled in") + + def validate_params(self, params): + """Check validity of given parameters. + """ + utils.check_type_or_throw_except( + params["prefactor"], 1, float, "prefactor should be a double") + + def default_params(self): + raise NotImplementedError("Derived classes must implement this method") + + def valid_keys(self): + raise NotImplementedError("Derived classes must implement this method") + + def required_keys(self): + raise NotImplementedError("Derived classes must implement this method") + + def _activate(self): + self.call_method("activate") + utils.handle_errors("Dipolar actor activation failed") + + def _deactivate(self): + self.call_method("deactivate") + utils.handle_errors("Dipolar actor deactivation failed") + + def get_magnetostatics_prefactor(self): + """ + Get the magnetostatics prefactor + + """ + return self.prefactor + + +@script_interface_register +class DipolarP3M(MagnetostaticInteraction): + """ + Calculate magnetostatic interactions using the dipolar P3M method. + See :ref:`Dipolar P3M` for more details. + + Parameters + ---------- + prefactor : :obj:`float` + Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) + accuracy : :obj:`float` + P3M tunes its parameters to provide this target accuracy. + alpha : :obj:`float` + Ewald parameter. + cao : :obj:`int` + Charge-assignment order, an integer between 1 and 7. + mesh : :obj:`int` or (3,) array_like of :obj:`int` + The number of mesh points in x, y and z direction. Use a single + value for cubic boxes. + mesh_off : (3,) array_like of :obj:`float`, optional + Mesh offset. + r_cut : :obj:`float` + Real space cutoff. + tune : :obj:`bool`, optional + Activate/deactivate the tuning method on activation + (default is ``True``, i.e., activated). + timings : :obj:`int` + Number of force calculations during tuning. + + """ + _so_name = "Dipoles::DipolarP3M" + + def _check_required_features(self): + if not has_features("DP3M"): + raise NotImplementedError("Feature DP3M not compiled in") + + def validate_params(self, params): + """Check validity of parameters. + + """ + super().validate_params(params) + + if utils.is_valid_type(params["mesh"], int): + params["mesh"] = 3 * [params["mesh"]] + else: + utils.check_type_or_throw_except(params["mesh"], 3, int, + "DipolarP3M mesh has to be an integer or integer list of length 3") + + if params["epsilon"] == "metallic": + params["epsilon"] = 0.0 + + utils.check_type_or_throw_except( + params["epsilon"], 1, float, + "epsilon should be a double or 'metallic'") + + utils.check_type_or_throw_except(params["mesh_off"], 3, float, + "mesh_off should be a (3,) array_like of values between 0.0 and 1.0") + + if not utils.is_valid_type(params["timings"], int): + raise TypeError("DipolarP3M timings has to be an integer") + if params["timings"] <= 0: + raise ValueError("DipolarP3M timings must be > 0") + if not utils.is_valid_type(params["tune"], bool): + raise TypeError("DipolarP3M tune has to be a boolean") + + def valid_keys(self): + return {"prefactor", "alpha_L", "r_cut_iL", "mesh", "mesh_off", + "cao", "accuracy", "epsilon", "cao_cut", "a", "ai", + "alpha", "r_cut", "cao3", "tune", "timings", "verbose"} + + def required_keys(self): + return {"accuracy"} + + def default_params(self): + return {"cao": -1, + "r_cut": -1, + "alpha": -1, + "accuracy": -1, + "mesh": [-1, -1, -1], + "epsilon": 0.0, + "mesh_off": [0.5, 0.5, 0.5], + "prefactor": 0., + "tune": True, + "timings": 10, + "verbose": True} + + +@script_interface_register +class DipolarDirectSumCpu(MagnetostaticInteraction): + """ + Calculate magnetostatic interactions by direct summation over all pairs. + See :ref:`Dipolar direct sum` for more details. + + If the system has periodic boundaries, the minimum image convention is + applied in the respective directions. + + Parameters + ---------- + prefactor : :obj:`float` + Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) + + """ + _so_name = "Dipoles::DipolarDirectSum" + + def default_params(self): + return {} + + def required_keys(self): + return set() + + def valid_keys(self): + return {"prefactor"} + + +@script_interface_register +class DipolarDirectSumWithReplicaCpu(MagnetostaticInteraction): + """ + Calculate magnetostatic interactions by direct summation over all pairs. + See :ref:`Dipolar direct sum` for more details. + + If the system has periodic boundaries, ``n_replica`` copies of the system are + taken into account in the respective directions. Spherical cutoff is applied. + + Parameters + ---------- + prefactor : :obj:`float` + Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) + n_replica : :obj:`int` + Number of replicas to be taken into account at periodic boundaries. + + """ + _so_name = "Dipoles::DipolarDirectSumWithReplica" + + def default_params(self): + return {} + + def required_keys(self): + return {"n_replica"} + + def valid_keys(self): + return {"prefactor", "n_replica"} + + +@script_interface_register +class Scafacos(MagnetostaticInteraction): + + """ + Calculate the dipolar interaction using dipoles-capable methods + from the ScaFaCoS library. See :ref:`ScaFaCoS magnetostatics` for + more details. + + Parameters + ---------- + prefactor : :obj:`float` + Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`). + method_name : :obj:`str` + Name of the ScaFaCoS method to use. + method_params : :obj:`dict` + Dictionary with the key-value pairs of the method parameters as + defined in ScaFaCoS. Note that the values are cast to strings + to match ScaFaCoS' interface. + + Methods + ------- + get_available_methods() + List long-range methods available in the ScaFaCoS library. + + """ + _so_name = "Dipoles::DipolarScafacos" + _so_creation_policy = "GLOBAL" + _so_bind_methods = MagnetostaticInteraction._so_bind_methods + \ + ("get_available_methods", ) + + def _check_required_features(self): + if not has_features("DIPOLES"): + raise NotImplementedError("Feature DIPOLES not compiled in") + if not has_features("SCAFACOS_DIPOLES"): + raise NotImplementedError( + "Feature SCAFACOS_DIPOLES not compiled in") + + def validate_params(self, params): + pass + + def default_params(self): + return {} + + def valid_keys(self): + return {"method_name", "method_params", "prefactor"} + + def required_keys(self): + return {"method_name", "method_params", "prefactor"} + + +@script_interface_register +class DipolarDirectSumGpu(MagnetostaticInteraction): + """ + Calculate magnetostatic interactions by direct summation over all + pairs. See :ref:`Dipolar direct sum` for more details. + + If the system has periodic boundaries, the minimum image convention + is applied in the respective directions. + + This is the GPU version of :class:`espressomd.magnetostatics.DipolarDirectSumCpu` + but uses floating point precision. + + Requires feature ``DIPOLAR_DIRECT_SUM``, which depends on + ``DIPOLES`` and ``CUDA``. + + Parameters + ---------- + prefactor : :obj:`float` + Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) + + """ + _so_name = "Dipoles::DipolarDirectSumGpu" + _so_creation_policy = "GLOBAL" + + def _check_required_features(self): + if not has_features("DIPOLAR_DIRECT_SUM"): + raise NotImplementedError( + "Features CUDA and DIPOLES not compiled in") + + def default_params(self): + return {} + + def required_keys(self): + return set() + + def valid_keys(self): + return {"prefactor"} + + +@script_interface_register +class DipolarBarnesHutGpu(MagnetostaticInteraction): + + """ + Calculates magnetostatic interactions by direct summation over all + pairs. See :ref:`Barnes-Hut octree sum on GPU` for more details. + + TODO: If the system has periodic boundaries, the minimum image + convention is applied. + + Requires feature ``DIPOLAR_BARNES_HUT``, which depends on + ``DIPOLES`` and ``CUDA``. + + """ + _so_name = "Dipoles::DipolarBarnesHutGpu" + _so_creation_policy = "GLOBAL" + + def _check_required_features(self): + if not has_features("DIPOLAR_BARNES_HUT"): + raise NotImplementedError( + "Features CUDA and DIPOLES not compiled in") + + def default_params(self): + return {"epssq": 100.0, "itolsq": 4.0} + + def required_keys(self): + return set() + + def valid_keys(self): + return {"prefactor", "epssq", "itolsq"} + + +@script_interface_register +class DLC(MagnetostaticInteraction): + + """ + Electrostatics solver for systems with two periodic dimensions. + See :ref:`Dipolar Layer Correction (DLC)` for more details. + + Notes + ----- + At present, the empty gap (volume without any particles), is assumed to be + along the z-axis. As a reference for the DLC method, see :cite:`brodka04a`. + + Parameters + ---------- + gap_size : :obj:`float` + The gap size gives the height :math:`h` of the empty region between + the system box and the neighboring artificial images. |es| checks + that the gap is empty and will throw an error if it isn't. Therefore + you should really make sure that the gap region is empty (e.g. + with wall constraints). + maxPWerror : :obj:`float` + Maximal pairwise error of the potential and force. + far_cut : :obj:`float`, optional + Cutoff of the exponential sum. + + """ + _so_name = "Dipoles::DipolarLayerCorrection" + _so_creation_policy = "GLOBAL" + + def validate_params(self, params): + utils.check_type_or_throw_except( + params["maxPWerror"], 1, float, "maxPWerror has to be a float") + utils.check_type_or_throw_except( + params["gap_size"], 1, float, "gap_size has to be a float") + utils.check_type_or_throw_except( + params["far_cut"], 1, float, "far_cut has to be a float") + + def default_params(self): + return {"far_cut": -1.} + + def valid_keys(self): + return {"actor", "maxPWerror", "gap_size", "far_cut"} + + def required_keys(self): + return {"actor", "maxPWerror", "gap_size"} diff --git a/src/python/espressomd/magnetostatics.pyx b/src/python/espressomd/magnetostatics.pyx deleted file mode 100644 index c3dfea76264..00000000000 --- a/src/python/espressomd/magnetostatics.pyx +++ /dev/null @@ -1,404 +0,0 @@ -# -# Copyright (C) 2013-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -from libcpp.memory cimport shared_ptr, make_shared -from cython.operator cimport dereference - -include "myconfig.pxi" -from .actors cimport Actor -IF SCAFACOS == 1: - from .scafacos import ScafacosConnector - from . cimport scafacos - -from .utils import handle_errors -from .utils import is_valid_type, check_type_or_throw_except - -IF DIPOLES == 1: - cdef class MagnetostaticInteraction(Actor): - """Provide magnetostatic interactions. - - Parameters - ---------- - prefactor : :obj:`float` - Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) - - """ - - def validate_params(self): - """Check validity of given parameters. - """ - if not self._params["prefactor"] >= 0: - raise ValueError("prefactor should be a positive float") - - def get_magnetostatics_prefactor(self): - """ - Get the magnetostatics prefactor - - """ - return get_Dprefactor() - - def set_magnetostatics_prefactor(self): - """ - Set the magnetostatics prefactor - - """ - set_Dprefactor(self._params["prefactor"]) - # also necessary on 1 CPU or GPU, does more than just broadcasting - mpi_bcast_coulomb_params() - - def _deactivate_method(self): - set_Dprefactor(0.0) - disable_method_local() - mpi_bcast_coulomb_params() - -IF DP3M == 1: - cdef class DipolarP3M(MagnetostaticInteraction): - """ - Calculate magnetostatic interactions using the dipolar P3M method. - See :ref:`Dipolar P3M` for more details. - - Parameters - ---------- - prefactor : :obj:`float` - Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) - accuracy : :obj:`float` - P3M tunes its parameters to provide this target accuracy. - alpha : :obj:`float` - Ewald parameter. - cao : :obj:`int` - Charge-assignment order, an integer between -1 and 7. - mesh : :obj:`int` or (3,) array_like of :obj:`int` - The number of mesh points in x, y and z direction. Use a single - value for cubic boxes. - mesh_off : (3,) array_like of :obj:`float` - Mesh offset. - r_cut : :obj:`float` - Real space cutoff. - tune : :obj:`bool`, optional - Activate/deactivate the tuning method on activation - (default is ``True``, i.e., activated). - timings : :obj:`int` - Number of force calculations during tuning. - - """ - - def validate_params(self): - """Check validity of parameters. - - """ - super().validate_params() - default_params = self.default_params() - - if is_valid_type(self._params["mesh"], int): - pass - else: - check_type_or_throw_except(self._params["mesh"], 3, int, - "DipolarP3M mesh has to be an integer or integer list of length 3") - if (self._params["mesh"][0] != self._params["mesh"][1]) or \ - (self._params["mesh"][0] != self._params["mesh"][2]): - raise ValueError( - "DipolarP3M requires a cubic box") - - if self._params["epsilon"] == "metallic": - self._params["epsilon"] = 0.0 - - check_type_or_throw_except( - self._params["epsilon"], 1, float, - "epsilon should be a double or 'metallic'") - - if self._params["mesh_off"] != default_params["mesh_off"]: - check_type_or_throw_except(self._params["mesh_off"], 3, float, - "mesh_off should be a (3,) array_like of values between 0.0 and 1.0") - - if not is_valid_type(self._params["timings"], int): - raise TypeError("DipolarP3M timings has to be an integer") - if self._params["timings"] <= 0: - raise ValueError("DipolarP3M timings must be > 0") - - def valid_keys(self): - return {"prefactor", "alpha_L", "r_cut_iL", "mesh", "mesh_off", - "cao", "accuracy", "epsilon", "cao_cut", "a", "ai", - "alpha", "r_cut", "cao3", "tune", "timings", "verbose"} - - def required_keys(self): - return {"accuracy"} - - def default_params(self): - return {"cao": -1, - "r_cut": -1, - "accuracy": -1, - "mesh": -1, - "epsilon": 0.0, - "mesh_off": [-1, -1, -1], - "tune": True, - "timings": 10, - "verbose": True} - - def _get_params_from_es_core(self): - params = {} - params.update(dp3m.params) - params["prefactor"] = self.get_magnetostatics_prefactor() - params["tune"] = self._params["tune"] - params["timings"] = self._params["timings"] - return params - - def _set_params_in_es_core(self): - if hasattr(self._params["mesh"], "__getitem__"): - mesh = self._params["mesh"][0] - else: - mesh = self._params["mesh"] - - self.set_magnetostatics_prefactor() - dp3m_set_eps(self._params["epsilon"]) - dp3m_set_mesh_offset(self._params["mesh_off"][0], - self._params["mesh_off"][1], - self._params["mesh_off"][2]) - dp3m_set_params(self._params["r_cut"], mesh, self._params["cao"], - self._params["alpha"], self._params["accuracy"]) - - def _tune(self): - if hasattr(self._params["mesh"], "__getitem__"): - mesh = self._params["mesh"][0] - else: - mesh = self._params["mesh"] - - self.set_magnetostatics_prefactor() - dp3m_set_eps(self._params["epsilon"]) - dp3m_set_tune_params(self._params["r_cut"], mesh, - self._params["cao"], self._params["accuracy"]) - tuning_error = dp3m_adaptive_tune( - self._params["timings"], self._params["verbose"]) - if tuning_error: - handle_errors("DipolarP3M: tuning failed") - self._params.update(self._get_params_from_es_core()) - - def _activate_method(self): - if self._params["tune"]: - self._tune() - - self._set_params_in_es_core() - mpi_bcast_coulomb_params() - - def _deactivate_method(self): - dp3m_deactivate() - super()._deactivate_method() - -IF DIPOLES == 1: - cdef class DipolarDirectSumCpu(MagnetostaticInteraction): - """ - Calculate magnetostatic interactions by direct summation over all pairs. - See :ref:`Dipolar direct sum` for more details. - - If the system has periodic boundaries, the minimum image convention is - applied in the respective directions. - - Parameters - ---------- - prefactor : :obj:`float` - Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) - - """ - - def default_params(self): - return {} - - def required_keys(self): - return set() - - def valid_keys(self): - return {"prefactor"} - - def _get_params_from_es_core(self): - return {"prefactor": self.get_magnetostatics_prefactor()} - - def _activate_method(self): - self._set_params_in_es_core() - mpi_bcast_coulomb_params() - - def _set_params_in_es_core(self): - self.set_magnetostatics_prefactor() - dawaanr_set_params() - - class DipolarDirectSumWithReplicaCpu(MagnetostaticInteraction): - - """ - Calculate magnetostatic interactions by direct summation over all pairs. - See :ref:`Dipolar direct sum` for more details. - - If the system has periodic boundaries, ``n_replica`` copies of the system are - taken into account in the respective directions. Spherical cutoff is applied. - - Parameters - ---------- - prefactor : :obj:`float` - Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) - n_replica : :obj:`int` - Number of replicas to be taken into account at periodic boundaries. - - """ - - def default_params(self): - return {} - - def required_keys(self): - return {"n_replica"} - - def valid_keys(self): - return {"prefactor", "n_replica"} - - def _get_params_from_es_core(self): - return {"prefactor": self.get_magnetostatics_prefactor(), - "n_replica": mdds_get_n_replica()} - - def _activate_method(self): - self._set_params_in_es_core() - mpi_bcast_coulomb_params() - - def _set_params_in_es_core(self): - self.set_magnetostatics_prefactor() - mdds_set_params(self._params["n_replica"]) - - IF SCAFACOS_DIPOLES == 1: - class Scafacos(ScafacosConnector, MagnetostaticInteraction): - - """ - Calculate the dipolar interaction using dipoles-capable methods - from the ScaFaCoS library. See :ref:`ScaFaCoS magnetostatics` for - more details. - - Parameters - ---------- - prefactor : :obj:`float` - Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`). - method_name : :obj:`str` - Name of the ScaFaCoS method to use. - method_params : :obj:`dict` - Dictionary with the key-value pairs of the method parameters as - defined in ScaFaCoS. Note that the values are cast to strings - to match ScaFaCoS' interface. - - """ - - dipolar = True - - # Explicit constructor needed due to multiple inheritance - def __init__(self, *args, **kwargs): - Actor.__init__(self, *args, **kwargs) - - def _activate_method(self): - self.set_magnetostatics_prefactor() - self._set_params_in_es_core() - - def _deactivate_method(self): - scafacos.free_handle(self.dipolar) - super()._deactivate_method() - - def default_params(self): - return {} - - IF(CUDA == 1) and (DIPOLES == 1) and (ROTATION == 1): - cdef class DipolarDirectSumGpu(MagnetostaticInteraction): - """ - Calculate magnetostatic interactions by direct summation over all - pairs. See :ref:`Dipolar direct sum` for more details. - - If the system has periodic boundaries, the minimum image convention - is applied in the respective directions. - - This is the GPU version of :class:`espressomd.magnetostatics.DipolarDirectSumCpu` - but uses floating point precision. - - Requires features ``DIPOLAR_DIRECT_SUM`` and ``CUDA``. - - Parameters - ---------- - prefactor : :obj:`float` - Magnetostatics prefactor (:math:`\\mu_0/(4\\pi)`) - - """ - cdef shared_ptr[DipolarDirectSum] sip - - def __cinit__(self): - self.sip = make_shared[DipolarDirectSum]( - EspressoSystemInterface.Instance()) - - def default_params(self): - return {} - - def required_keys(self): - return set() - - def valid_keys(self): - return {"prefactor"} - - def _get_params_from_es_core(self): - return {"prefactor": self.get_magnetostatics_prefactor()} - - def _activate_method(self): - self._set_params_in_es_core() - dereference(self.sip).activate() - - def _deactivate_method(self): - super()._deactivate_method() - dereference(self.sip).deactivate() - - def _set_params_in_es_core(self): - self.set_magnetostatics_prefactor() - dereference(self.sip).set_params() - - IF(DIPOLAR_BARNES_HUT == 1): - cdef class DipolarBarnesHutGpu(MagnetostaticInteraction): - - """ - Calculates magnetostatic interactions by direct summation over all - pairs. See :ref:`Barnes-Hut octree sum on GPU` for more details. - - TODO: If the system has periodic boundaries, the minimum image - convention is applied. - """ - cdef shared_ptr[DipolarBarnesHut] sip - - def __cinit__(self): - self.sip = make_shared[DipolarBarnesHut]( - EspressoSystemInterface.Instance()) - - def default_params(self): - return {"epssq": 100.0, - "itolsq": 4.0} - - def required_keys(self): - return set() - - def valid_keys(self): - return {"prefactor", "epssq", "itolsq"} - - def _get_params_from_es_core(self): - return {"prefactor": self.get_magnetostatics_prefactor()} - - def _activate_method(self): - self._set_params_in_es_core() - dereference(self.sip).activate() - - def _deactivate_method(self): - super()._deactivate_method() - dereference(self.sip).deactivate() - - def _set_params_in_es_core(self): - self.set_magnetostatics_prefactor() - dereference(self.sip).set_params( - self._params["epssq"], self._params["itolsq"]) diff --git a/src/python/espressomd/p3m_common.pxd b/src/python/espressomd/p3m_common.pxd deleted file mode 100644 index aaf7f92b4d9..00000000000 --- a/src/python/espressomd/p3m_common.pxd +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2010-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -include "myconfig.pxi" - -IF P3M == 1 or DP3M == 1: - cdef extern from "electrostatics_magnetostatics/p3m-common.hpp": - ctypedef struct P3MParameters: - double alpha_L - double r_cut_iL - int mesh[3] - double mesh_off[3] - int cao - double accuracy - double epsilon - double cao_cut[3] - double a[3] - double alpha - double r_cut diff --git a/src/python/espressomd/reaction_methods.py b/src/python/espressomd/reaction_methods.py index 66a678cc418..ac8ea88e894 100644 --- a/src/python/espressomd/reaction_methods.py +++ b/src/python/espressomd/reaction_methods.py @@ -134,7 +134,7 @@ class ReactionAlgorithm(ScriptInterfaceHelper): ... system.non_bonded_inter[particle_type, types["wall"]].wca.set_params(epsilon=1.0, sigma=1.0) >>> # add ELC actor >>> p3m = espressomd.electrostatics.P3M(prefactor=1.0, accuracy=1e-2) - >>> elc = espressomd.electrostatics.ELC(p3m_actor=p3m, maxPWerror=1.0, gap_size=elc_gap) + >>> elc = espressomd.electrostatics.ELC(actor=p3m, maxPWerror=1.0, gap_size=elc_gap) >>> system.actors.add(elc) >>> # add constant pH method >>> RE = espressomd.reaction_methods.ConstantpHEnsemble(kT=1, exclusion_range=1, seed=77) diff --git a/src/python/espressomd/scafacos.pxd b/src/python/espressomd/scafacos.pxd deleted file mode 100644 index a14e1bc4656..00000000000 --- a/src/python/espressomd/scafacos.pxd +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2010-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# Interface to the scafacos library. These are the methods shared between -# dipolar and electrostatics methods - -include "myconfig.pxi" - -from libcpp.string cimport string -from libcpp cimport bool -from libcpp.list cimport list -IF SCAFACOS: - cdef extern from "electrostatics_magnetostatics/scafacos.hpp" namespace "Scafacos": - void set_parameters(string & method_name, string & params, bool dipolar) except+ - string get_method_and_parameters(bool dipolar) except+ - void free_handle(bool dipolar) - list[string] available_methods_core "Scafacos::available_methods" () diff --git a/src/python/espressomd/scafacos.pyx b/src/python/espressomd/scafacos.pyx deleted file mode 100644 index 04dd88496da..00000000000 --- a/src/python/espressomd/scafacos.pyx +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (C) 2010-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -"""Code shared by charge and dipole methods based on the SCAFACOS library.""" - - -from .actors cimport Actor -from libcpp.string cimport string # import std::string -from . cimport electrostatics -from . cimport magnetostatics -from .utils import to_char_pointer, to_str, handle_errors - - -include "myconfig.pxi" - -# Interface to the scafacos library. These are the methods shared between -# dipolar and electrostatics methods - -IF SCAFACOS == 1: - class ScafacosConnector(Actor): - - """Scafacos interface class shared by charge and dipole methods. Do not use directly. - - """ - - def get_params(self): - return self._get_params_from_es_core() - - def set_params(self, params): - self._params = params - self._set_params_in_es_core() - - def valid_keys(self): - tmp = self.required_keys().copy() - if not self.dipolar: - tmp.add("check_neutrality") - return tmp - - def required_keys(self): - return {"method_name", "method_params", "prefactor"} - - def validate_params(self): - if self._params["method_name"] not in available_methods(): - raise ValueError( - f"method '{self._params['method_name']}' is unknown or not compiled in ScaFaCoS") - if not self._params["method_params"]: - raise ValueError( - "ScaFaCoS methods require at least 1 parameter") - - def _get_params_from_es_core(self): - # Parameters are returned as strings - # First word is method name, rest are key value pairs - # which we convert to a dict - p = to_str(get_method_and_parameters(self.dipolar)).split(" ") - res = {} - res["method_name"] = p.pop(0) - - method_params = {} - while p: - pname = p.pop(0) - - # The first item after the parameter name is always a value - # But in the case of array-like properties, there can also - # follow several values. Therefore, we treat the next - # words as part of the value, if they begin with a digit - pvalues = [p.pop(0)] - while p: - if p[0][0] in "-0123456789": - pvalues.append(p.pop(0)) - else: - break - - # If there is one value, cast away the list - if len(pvalues) == 1: - pvalues = pvalues[0] - else: - # Cast array elements to strings and join them by commas - # to achieve consistency with setting array-likes - # such as "pnfft_n":"128,128,128" - pvalues = ",".join(pvalues) - method_params[pname] = pvalues - - res["method_params"] = method_params - - # Re-add the prefactor to the parameter set - if self.dipolar: - IF DIPOLES == 1: - res["prefactor"] = magnetostatics.get_Dprefactor() - pass - else: - IF ELECTROSTATICS == 1: - res["prefactor"] = electrostatics.coulomb.prefactor - pass - return res - - def _set_params_in_es_core(self): - # Convert the parameter dictionary to a list of strings - method_params = self._params["method_params"] - param_string = "" - for k in method_params: - param_string += f"{k} {method_params[k]} " - # Format list as a single string - param_string = param_string.rstrip().replace(" ", ",") - - set_parameters(to_char_pointer(self._params["method_name"]), - to_char_pointer(param_string), self.dipolar) - handle_errors("Scafacos not initialized.") - - def default_params(self): - if not self.dipolar: - return {"check_neutrality": True} - return {} - - def available_methods(): - """Lists long range methods available in the Scafacos library. - - """ - methods = available_methods_core() - res = [] - for m in methods: - res.append(to_str(m)) - return res diff --git a/src/python/espressomd/utils.pxd b/src/python/espressomd/utils.pxd index 2fe8602757b..4a9cf5b676f 100644 --- a/src/python/espressomd/utils.pxd +++ b/src/python/espressomd/utils.pxd @@ -39,6 +39,7 @@ cdef extern from "utils/Span.hpp" namespace "Utils": cdef np.ndarray create_nparray_from_double_array(double * x, int n) cdef np.ndarray create_nparray_from_double_span(Span[double] x) +cpdef check_array_type_or_throw_except(x, n, t, msg) cpdef check_type_or_throw_except(x, n, t, msg) cdef check_range_or_except(D, x, v_min, incl_min, v_max, incl_max) diff --git a/src/python/espressomd/utils.pyx b/src/python/espressomd/utils.pyx index b1340b570ed..237c6f2eae9 100644 --- a/src/python/espressomd/utils.pyx +++ b/src/python/espressomd/utils.pyx @@ -19,41 +19,54 @@ cimport numpy as np import numpy as np from libcpp.vector cimport vector -include "myconfig.pxi" + + +cdef _check_type_or_throw_except_assertion(x, t): + return isinstance(x, t) or (t == int and is_valid_type(x, int)) or ( + t == float and (is_valid_type(x, int) or is_valid_type(x, float))) or ( + t == bool and (is_valid_type(x, bool) or x in (0, 1))) + + +cpdef check_array_type_or_throw_except(x, n, t, msg): + """ + Check that ``x`` is of type ``t`` and that ``n`` values are given, + otherwise raise a ``ValueError`` with message ``msg``. + Integers are accepted when a float was asked for. + + """ + if not hasattr(x, "__getitem__"): + raise ValueError( + msg + f" -- A single value was given but {n} were expected.") + if len(x) != n: + raise ValueError( + msg + f" -- {len(x)} values were given but {n} were expected.") + if isinstance(x, np.ndarray): + value = x.dtype.type() # default-constructed value of the same type + if not _check_type_or_throw_except_assertion(value, t): + raise ValueError( + msg + f" -- Array was of type {type(value).__name__}") + return + for i in range(len(x)): + if not _check_type_or_throw_except_assertion(x[i], t): + raise ValueError( + msg + f" -- Item {i} was of type {type(x[i]).__name__}") + cpdef check_type_or_throw_except(x, n, t, msg): """ - Checks that x is of type t and that n values are given, otherwise throws - ValueError with the message msg. If x is an array/list/tuple, the type - checking is done on the elements, and all elements are checked. Integers - are accepted when a float was asked for. + Check that ``x`` is of type ``t`` and that ``n`` values are given, + otherwise raise a ``ValueError`` with message ``msg``. If ``x`` is an + array/list/tuple, the type checking is done on the elements, and all + elements are checked. If ``n`` is 1, ``x`` is assumed to be a scalar. + Integers are accepted when a float was asked for. """ # Check whether x is an array/list/tuple or a single value if n > 1: - if hasattr(x, "__getitem__"): - if len(x) != n: - raise ValueError( - msg + f" -- {len(x)} values were given but {n} were expected.") - for i in range(len(x)): - if not (isinstance(x[i], t) - or (t == float and (is_valid_type(x[i], int) or is_valid_type(x[i], float))) - or (t == int and is_valid_type(x[i], int)) - or (t == bool and (is_valid_type(x[i], bool) or x[i] in (0, 1)))): - raise ValueError( - msg + f" -- Item {i} was of type {type(x[i]).__name__}") - else: - # if n>1, but the user passed a single value, also throw exception - raise ValueError( - msg + f" -- A single value was given but {n} were expected.") + check_array_type_or_throw_except(x, n, t, msg) else: - # N=1 and a single value - if not isinstance(x, t): - if not (isinstance(x, t) - or (t == float and is_valid_type(x, int)) - or (t == int and is_valid_type(x, int)) - or (t == bool and is_valid_type(x, bool))): - raise ValueError(msg + f" -- Got an {type(x).__name__}") + if not _check_type_or_throw_except_assertion(x, t): + raise ValueError(msg + f" -- Got an {type(x).__name__}") cdef np.ndarray create_nparray_from_double_array(double * x, int len_x): diff --git a/src/scafacos/CMakeLists.txt b/src/scafacos/CMakeLists.txt index dac665b72dc..01eca9cf838 100644 --- a/src/scafacos/CMakeLists.txt +++ b/src/scafacos/CMakeLists.txt @@ -1,8 +1,14 @@ -set(Scafacos_SRC Scafacos.cpp) -add_library(Scafacos SHARED ${Scafacos_SRC}) -target_include_directories(Scafacos PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_include_directories(Scafacos SYSTEM PUBLIC ${SCAFACOS_INCLUDE_DIRS}) -target_link_libraries(Scafacos PRIVATE ${SCAFACOS_LDFLAGS}) -target_link_libraries(Scafacos PUBLIC MPI::MPI_CXX PRIVATE cxx_interface) +set(SOURCE_FILES src/Scafacos.cpp src/Coulomb.cpp src/Dipoles.cpp) -install(TARGETS Scafacos DESTINATION ${PYTHON_INSTDIR}/espressomd) +add_library(EspressoScafacos SHARED ${SOURCE_FILES}) +target_link_libraries(EspressoScafacos PRIVATE ${SCAFACOS_LDFLAGS}) +target_link_libraries(EspressoScafacos PUBLIC MPI::MPI_CXX + PRIVATE cxx_interface) + +target_include_directories( + EspressoScafacos PUBLIC $ + $) +target_include_directories(EspressoScafacos SYSTEM + PUBLIC ${SCAFACOS_INCLUDE_DIRS}) + +install(TARGETS EspressoScafacos DESTINATION ${PYTHON_INSTDIR}/espressomd) diff --git a/src/scafacos/Scafacos.cpp b/src/scafacos/Scafacos.cpp deleted file mode 100644 index 881a7518be4..00000000000 --- a/src/scafacos/Scafacos.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "Scafacos.hpp" - -#include -#include -#include -#include -#include - -namespace Scafacos { - -#define handle_error(stmt) \ - { \ - const FCSResult res = stmt; \ - if (res) \ - throw std::runtime_error(fcs_result_get_message(res)); \ - } - -std::string Scafacos::get_parameters() { return m_last_parameters; } -std::string Scafacos::get_method() { return method; } - -std::list Scafacos::available_methods() { - std::list methods; - -#ifdef FCS_ENABLE_DIRECT - methods.emplace_back("direct"); -#endif -#ifdef FCS_ENABLE_EWALD - methods.emplace_back("ewald"); -#endif -#ifdef FCS_ENABLE_FMM - methods.emplace_back("fmm"); -#endif -#ifdef FCS_ENABLE_MEMD - methods.emplace_back("memd"); -#endif -#ifdef FCS_ENABLE_MMM1D - methods.emplace_back("mmm1d"); -#endif -#ifdef FCS_ENABLE_MMM2D - methods.emplace_back("mmm2d"); -#endif -#ifdef FCS_ENABLE_P2NFFT - methods.emplace_back("p2nfft"); -#endif -#ifdef FCS_ENABLE_P3M - methods.emplace_back("p3m"); -#endif -#ifdef FCS_ENABLE_PEPC - methods.emplace_back("pepc"); -#endif -#ifdef FCS_ENABLE_PP3MG - methods.emplace_back("pp3mg"); -#endif -#ifdef FCS_ENABLE_VMG - methods.emplace_back("vmg"); -#endif -#ifdef FCS_ENABLE_WOLF - methods.emplace_back("wolf"); -#endif - - return methods; -} - -Scafacos::Scafacos(std::string method, MPI_Comm comm, - const std::string ¶meters) - : method(std::move(method)) { - handle_error(fcs_init(&handle, this->method.c_str(), comm)); - - int near_flag; - fcs_get_near_field_delegation(handle, &near_flag); - has_near = near_flag != 0; - - fcs_set_resort(handle, 0); - - parse_parameters(parameters); -} - -Scafacos::~Scafacos() { fcs_destroy(handle); } - -void Scafacos::parse_parameters(const std::string &s) { - m_last_parameters = s; - handle_error(fcs_parser(handle, s.c_str(), 0)); -} - -double Scafacos::r_cut() const { - if (has_near) { - fcs_float r_cut; - fcs_get_r_cut(handle, &r_cut); - return r_cut; - } - return 0.0; -} - -void Scafacos::set_r_cut(double r_cut) { - if (has_near) { - fcs_set_r_cut(handle, r_cut); - } -} - -void Scafacos::run(std::vector &charges, std::vector &positions, - std::vector &fields, - std::vector &potentials) { - const int local_n_part = charges.size(); - - fields.resize(3 * local_n_part); - potentials.resize(local_n_part); - - handle_error( - fcs_tune(handle, local_n_part, positions.data(), charges.data())); - - handle_error(fcs_run(handle, local_n_part, positions.data(), charges.data(), - fields.data(), potentials.data())); -} - -#ifdef FCS_ENABLE_DIPOLES -void Scafacos::run_dipolar(std::vector &dipoles, - std::vector &positions, - std::vector &fields, - std::vector &potentials) { - assert(dipoles.size() % 3 == 0); - auto const local_n_part = dipoles.size() / 3; - - fields.resize(6 * local_n_part); - potentials.resize(3 * local_n_part); - - handle_error(fcs_set_dipole_particles(handle, static_cast(local_n_part), - positions.data(), dipoles.data(), - fields.data(), potentials.data())); - handle_error(fcs_run(handle, 0, nullptr, nullptr, nullptr, nullptr)); -} -#endif - -void Scafacos::tune(std::vector &charges, - std::vector &positions) { - handle_error( - fcs_tune(handle, charges.size(), positions.data(), charges.data())); -} - -void Scafacos::set_common_parameters(const double *box_l, - const int *periodicity, - int total_particles) { - double boxa[3] = {0., 0., 0.}, boxb[3] = {0., 0., 0.}, boxc[3] = {0., 0., 0.}, - off[3] = {0., 0., 0.}; - boxa[0] = box_l[0]; - boxb[1] = box_l[1]; - boxc[2] = box_l[2]; - // Does scafacos calculate the near field part? - // For charges, ESPResSo calculates near field if the method supports it. - // For dipoles, ScaFaCoS calculates near field. - int const sr = dipolar() || !has_near; - handle_error(fcs_set_common(handle, sr, boxa, boxb, boxc, off, periodicity, - total_particles)); -#ifdef FCS_ENABLE_DIPOLES - if (m_dipolar) - handle_error(fcs_set_total_dipole_particles(handle, total_particles)); -#endif -} - -void Scafacos::set_dipolar(bool d) { -#ifndef FCS_ENABLE_DIPOLES - if (d) { - throw std::runtime_error("Scafacos library does not have dipole support."); - } -#endif - m_dipolar = d; -} - -} // namespace Scafacos diff --git a/src/scafacos/Scafacos.hpp b/src/scafacos/Scafacos.hpp deleted file mode 100644 index 99f2dba653c..00000000000 --- a/src/scafacos/Scafacos.hpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef ES_SCAFACOS_SCAFACOS_HPP -#define ES_SCAFACOS_SCAFACOS_HPP - -#include - -#include - -#include -#include -#include - -namespace Scafacos { - -/** \brief Abstraction of a method from the scafacos library */ - -struct Scafacos { - Scafacos(std::string method, MPI_Comm comm, const std::string ¶meters); - ~Scafacos(); - /** Parse parameter string */ - void parse_parameters(const std::string &s); - /** Get the parameters from the library */ - std::string get_parameters(); - /** Get active method name */ - std::string get_method(); - - /** Set parameters common to all methods */ - void set_common_parameters(const double *box_l, const int *periodicity, - int total_particles); - /** Calculate short range pair force if supported by the method */ - inline double pair_force(double dist) const { - if (has_near) { - fcs_float field; - fcs_compute_near_field(handle, dist, &field); - return field; - } - - return 0.0; - } - - /** Get pair energy for near field contrib */ - inline double pair_energy(double dist) const { - if (has_near) { - fcs_float potential; - fcs_compute_near_potential(handle, dist, &potential); - return potential; - } - - return 0.0; - } - - /** Calculate the fields and potentials for charges */ - void run(std::vector &charges, std::vector &positions, - std::vector &fields, std::vector &potentials); - -#ifdef FCS_ENABLE_DIPOLES - /** Calculate fields and potentials for dipoles */ - void run_dipolar(std::vector &dipoles, std::vector &positions, - std::vector &fields, - std::vector &potentials); -#endif - - /** Tune parameters */ - void tune(std::vector &charges, std::vector &positions); - /** Get shortrange cutoff (0.0 if not supported) */ - double r_cut() const; - /** Set cutoff */ - void set_r_cut(double r_cut); - - /** Get a list of methods supported by the library */ - static std::list available_methods(); - - /** Scafacos used for dipolar ia */ - bool dipolar() { return m_dipolar; } - - /** Switch scafacos to dipolar ia */ - void set_dipolar(bool d); - - /** Handle from the library */ - FCS handle; - /** Whether the method supports near field delegation */ - bool has_near; - /** The scafacos method name of this instance */ - const std::string method; - /** The last parameters set */ - std::string m_last_parameters; - - /** Scafacos used for dipolar ia */ - bool m_dipolar; -}; - -} // namespace Scafacos -#endif diff --git a/src/scafacos/include/scafacos/Coulomb.hpp b/src/scafacos/include/scafacos/Coulomb.hpp new file mode 100644 index 00000000000..cf54358289d --- /dev/null +++ b/src/scafacos/include/scafacos/Coulomb.hpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCAFACOS_COULOMB_HPP +#define ESPRESSO_SRC_SCAFACOS_COULOMB_HPP + +#include "scafacos/Scafacos.hpp" + +#include + +#include + +#include +#include + +namespace Scafacos { + +/** @brief Abstraction of a Coulomb method from the ScaFaCoS library. */ +struct Coulomb : public Scafacos { + Coulomb(MPI_Comm comm, std::string method, std::string parameters); + + /** @brief Set box geometry and number of particles. */ + void set_runtime_parameters(double const *box_l, int const *periodicity, + int total_particles); + + /** @brief Calculate the fields and potentials for charges. */ + void run(std::vector &charges, std::vector &positions, + std::vector &fields, std::vector &potentials); + + /** + * @brief Delegate the short-range calculation. + * By default, ESPResSo calculates the short-range forces and energies + * if the chosen ScaFaCoS method support delegation. This decision can + * be overriden to obtain the result from a full ScaFaCos calculation. + * @param delegate Delegate short-range calculation to ESPResSo if true, + * or to ScaFaCoS if false. + */ + void set_near_field_delegation(bool delegate); + + bool get_near_field_delegation() const { return m_delegate_near_field; } + + /** @brief Calculate short-range pair force. */ + double pair_force(double dist) const { + if (m_delegate_near_field) { + fcs_float field; + fcs_compute_near_field(m_handle, dist, &field); + return field; + } + + return 0.0; + } + + /** @brief Calculate short-range pair energy. */ + double pair_energy(double dist) const { + if (m_delegate_near_field) { + fcs_float potential; + fcs_compute_near_potential(m_handle, dist, &potential); + return potential; + } + + return 0.0; + } + + /** @brief Tune parameters */ + void tune(std::vector &charges, std::vector &positions); + /** @brief Get short-range cutoff (0.0 if not supported by the method). */ + double r_cut() const; + /** @brief Set short-range cutoff */ + void set_r_cut(double r_cut); + +private: + auto get_near_field_flag() const { + return static_cast((m_delegate_near_field) ? 0 : 1); + } + + /** Whether the method supports delegating the short-range calculation. */ + bool m_method_can_delegate_near_field; + /** Whether to delegate short-range calculation to ESPResSo. */ + bool m_delegate_near_field; +}; + +} // namespace Scafacos +#endif diff --git a/src/scafacos/include/scafacos/Dipoles.hpp b/src/scafacos/include/scafacos/Dipoles.hpp new file mode 100644 index 00000000000..ce903fa36ff --- /dev/null +++ b/src/scafacos/include/scafacos/Dipoles.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCAFACOS_DIPOLES_HPP +#define ESPRESSO_SRC_SCAFACOS_DIPOLES_HPP + +#include "scafacos/Scafacos.hpp" + +#include + +#include + +#include +#include + +#ifdef FCS_ENABLE_DIPOLES + +namespace Scafacos { + +/** @brief Abstraction of a dipolar method from the ScaFaCoS library. */ +struct Dipoles : public Scafacos { + Dipoles(MPI_Comm comm, std::string method, std::string parameters); + + /** @brief Set box geometry and number of particles. */ + void set_runtime_parameters(double const *box_l, int const *periodicity, + int total_particles); + + /** @brief Calculate the fields and potentials for dipoles. */ + void run(std::vector &dipoles, std::vector &positions, + std::vector &fields, std::vector &potentials); +}; + +} // namespace Scafacos + +#endif // FCS_ENABLE_DIPOLES + +#endif diff --git a/src/scafacos/include/scafacos/Scafacos.hpp b/src/scafacos/include/scafacos/Scafacos.hpp new file mode 100644 index 00000000000..d48b9f2b11d --- /dev/null +++ b/src/scafacos/include/scafacos/Scafacos.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCAFACOS_SCAFACOS_HPP +#define ESPRESSO_SRC_SCAFACOS_SCAFACOS_HPP + +#include + +#include + +#include +#include +#include + +namespace Scafacos { + +/** @brief Abstraction of a method from the ScaFaCoS library. */ +struct Scafacos { + Scafacos(MPI_Comm comm, std::string method, std::string parameters); + ~Scafacos(); + /** Get the parameters from the library */ + std::string get_parameters() const { return m_parameters; } + /** Get active method name */ + std::string get_method() const { return m_method_name; } + + /** Set box geometry, number of particles and calculation type. */ + void set_runtime_parameters(double const *box_l, int const *periodicity, + int total_particles, int near_field_flag); + + /** Get a list of methods supported by the library */ + static std::vector available_methods(); + +protected: + /** Handle from the library */ + FCS m_handle; + +private: + /** The method name */ + std::string m_method_name; + /** The parameters set */ + std::string m_parameters; +}; + +static_assert(std::is_same::value, + "ScaFaCoS must be compiled with fcs_int = int"); +static_assert(std::is_same::value, + "ScaFaCoS must be compiled with fcs_float = double"); + +} // namespace Scafacos +#endif diff --git a/src/scafacos/src/Coulomb.cpp b/src/scafacos/src/Coulomb.cpp new file mode 100644 index 00000000000..ce160efa6fc --- /dev/null +++ b/src/scafacos/src/Coulomb.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "scafacos/Coulomb.hpp" + +#include "utils.hpp" + +#include + +#include +#include +#include +#include + +namespace Scafacos { + +Coulomb::Coulomb(MPI_Comm comm, std::string method, std::string parameters) + : Scafacos{comm, std::move(method), std::move(parameters)} { + fcs_int near_field_delegation; + fcs_get_near_field_delegation(m_handle, &near_field_delegation); + m_method_can_delegate_near_field = static_cast(near_field_delegation); + m_delegate_near_field = m_method_can_delegate_near_field; +} + +void Coulomb::set_runtime_parameters(double const *const box_l, + int const *const periodicity, + int const total_particles) { + auto const near_field_flag = get_near_field_flag(); + Scafacos::set_runtime_parameters(box_l, periodicity, total_particles, + near_field_flag); +} + +void Coulomb::set_near_field_delegation(bool delegate) { + if (delegate != m_delegate_near_field) { + if (delegate and not m_method_can_delegate_near_field) { + throw std::runtime_error("Method '" + get_method() + + "' cannot delegate short-range calculation"); + } + m_delegate_near_field = delegate; + auto const near_field_flag = get_near_field_flag(); + handle_error(fcs_set_near_field_flag(m_handle, near_field_flag)); + } +} + +double Coulomb::r_cut() const { + if (m_delegate_near_field) { + fcs_float r_cut; + fcs_get_r_cut(m_handle, &r_cut); + return r_cut; + } + return 0.0; +} + +void Coulomb::set_r_cut(double r_cut) { + if (m_delegate_near_field) { + fcs_set_r_cut(m_handle, r_cut); + } +} + +void Coulomb::run(std::vector &charges, std::vector &positions, + std::vector &fields, + std::vector &potentials) { + + auto const n_part = charges.size(); + fields.resize(3ul * n_part); + potentials.resize(n_part); + + tune(charges, positions); + auto const size = static_cast(n_part); + handle_error(fcs_run(m_handle, size, positions.data(), charges.data(), + fields.data(), potentials.data())); +} + +void Coulomb::tune(std::vector &charges, + std::vector &positions) { + auto const n_part = charges.size(); + auto const size = static_cast(n_part); + handle_error(fcs_tune(m_handle, size, positions.data(), charges.data())); +} + +} // namespace Scafacos diff --git a/src/scafacos/src/Dipoles.cpp b/src/scafacos/src/Dipoles.cpp new file mode 100644 index 00000000000..28e2557931c --- /dev/null +++ b/src/scafacos/src/Dipoles.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "scafacos/Dipoles.hpp" + +#include "utils.hpp" + +#include + +#include +#include +#include +#include + +#ifdef FCS_ENABLE_DIPOLES + +namespace Scafacos { + +Dipoles::Dipoles(MPI_Comm comm, std::string method, std::string parameters) + : Scafacos{comm, std::move(method), std::move(parameters)} {} + +void Dipoles::set_runtime_parameters(double const *const box_l, + int const *const periodicity, + int const total_particles) { + // magnetostatics: ScaFaCoS calculates near field + auto const near_field_flag = fcs_int{1}; + Scafacos::set_runtime_parameters(box_l, periodicity, total_particles, + near_field_flag); + handle_error(fcs_set_total_dipole_particles(m_handle, total_particles)); +} + +void Dipoles::run(std::vector &dipoles, std::vector &positions, + std::vector &fields, + std::vector &potentials) { + + assert(dipoles.size() % 3ul == 0ul); + + fields.resize(2ul * dipoles.size()); + potentials.resize(dipoles.size()); + + auto const n_part = static_cast(dipoles.size() / 3ul); + handle_error(fcs_set_dipole_particles(m_handle, n_part, positions.data(), + dipoles.data(), fields.data(), + potentials.data())); + handle_error(fcs_run(m_handle, 0, nullptr, nullptr, nullptr, nullptr)); +} + +} // namespace Scafacos + +#endif // FCS_ENABLE_DIPOLES diff --git a/src/scafacos/src/Scafacos.cpp b/src/scafacos/src/Scafacos.cpp new file mode 100644 index 00000000000..ee211289af8 --- /dev/null +++ b/src/scafacos/src/Scafacos.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2010-2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "scafacos/Scafacos.hpp" + +#include "utils.hpp" + +#include +#include +#include +#include + +namespace Scafacos { + +std::vector Scafacos::available_methods() { + std::vector methods; + +#ifdef FCS_ENABLE_DIRECT + methods.emplace_back("direct"); +#endif +#ifdef FCS_ENABLE_EWALD + methods.emplace_back("ewald"); +#endif +#ifdef FCS_ENABLE_FMM + methods.emplace_back("fmm"); +#endif +#ifdef FCS_ENABLE_MEMD + methods.emplace_back("memd"); +#endif +#ifdef FCS_ENABLE_MMM1D + methods.emplace_back("mmm1d"); +#endif +#ifdef FCS_ENABLE_MMM2D + methods.emplace_back("mmm2d"); +#endif +#ifdef FCS_ENABLE_P2NFFT + methods.emplace_back("p2nfft"); +#endif +#ifdef FCS_ENABLE_P3M + methods.emplace_back("p3m"); +#endif +#ifdef FCS_ENABLE_PEPC + methods.emplace_back("pepc"); +#endif +#ifdef FCS_ENABLE_PP3MG + methods.emplace_back("pp3mg"); +#endif +#ifdef FCS_ENABLE_VMG + methods.emplace_back("vmg"); +#endif +#ifdef FCS_ENABLE_WOLF + methods.emplace_back("wolf"); +#endif + + return methods; +} + +static std::string parse_method_parameters( + std::vector> const ¶meters) { + std::string method_params = ""; + for (auto const ¶meter : parameters) { + for (auto const &value : parameter) { + method_params += " " + value; + } + } + std::replace(method_params.begin(), method_params.end(), ' ', ','); + return method_params.substr(1); +} + +Scafacos::Scafacos(MPI_Comm comm, std::string method, std::string parameters) + : m_method_name{std::move(method)}, m_parameters{std::move(parameters)} { + + handle_error(fcs_init(&m_handle, m_method_name.c_str(), comm)); + + fcs_set_resort(m_handle, 0); + + handle_error(fcs_parser(m_handle, m_parameters.c_str(), 0)); +} + +Scafacos::~Scafacos() { fcs_destroy(m_handle); } + +void Scafacos::set_runtime_parameters(double const *box_l, + int const *periodicity, + int total_particles, + int near_field_flag) { + // define rectangular box + double boxa[3] = {box_l[0], 0., 0.}; + double boxb[3] = {0., box_l[1], 0.}; + double boxc[3] = {0., 0., box_l[2]}; + double off[3] = {0., 0., 0.}; + handle_error(fcs_set_common(m_handle, near_field_flag, boxa, boxb, boxc, off, + periodicity, total_particles)); +} + +} // namespace Scafacos diff --git a/src/core/actor/Actor.hpp b/src/scafacos/src/utils.hpp similarity index 55% rename from src/core/actor/Actor.hpp rename to src/scafacos/src/utils.hpp index e866eec957c..d1dda083e91 100644 --- a/src/core/actor/Actor.hpp +++ b/src/scafacos/src/utils.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2019 The ESPResSo project + * Copyright (C) 2010-2022 The ESPResSo project * * This file is part of ESPResSo. * @@ -16,21 +16,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CORE_ACTOR_ACTOR_HPP -#define CORE_ACTOR_ACTOR_HPP -#include "SystemInterface.hpp" +#include -/** - * Generic abstract potential class. - * All potentials should be derived from this one. - */ -class Actor { -public: - virtual void computeForces(SystemInterface &) {} - virtual void computeTorques(SystemInterface &) {} - virtual void computeEnergy(SystemInterface &) {} - virtual ~Actor() = default; -}; +#include + +namespace Scafacos { + +#define handle_error(stmt) \ + { \ + const FCSResult res = stmt; \ + if (res) \ + throw std::runtime_error(fcs_result_get_message(res)); \ + } -#endif /* CORE_ACTOR_ACTOR_HPP */ +} // namespace Scafacos diff --git a/src/script_interface/CMakeLists.txt b/src/script_interface/CMakeLists.txt index b906335fb3d..15b938803a5 100644 --- a/src/script_interface/CMakeLists.txt +++ b/src/script_interface/CMakeLists.txt @@ -9,13 +9,16 @@ add_subdirectory(cell_system) add_subdirectory(collision_detection) add_subdirectory(constraints) add_subdirectory(cluster_analysis) +add_subdirectory(electrostatics) add_subdirectory(interactions) add_subdirectory(lbboundaries) add_subdirectory(lees_edwards) +add_subdirectory(magnetostatics) add_subdirectory(virtual_sites) add_subdirectory(observables) add_subdirectory(pair_criteria) add_subdirectory(mpiio) +add_subdirectory(scafacos) add_subdirectory(shapes) add_subdirectory(h5md) add_subdirectory(reaction_methods) diff --git a/src/script_interface/cell_system/CellSystem.hpp b/src/script_interface/cell_system/CellSystem.hpp index 8346bf44d14..cd9be86815c 100644 --- a/src/script_interface/cell_system/CellSystem.hpp +++ b/src/script_interface/cell_system/CellSystem.hpp @@ -91,8 +91,9 @@ class CellSystem : public AutoParameters { if (vec.size() != 3ul) { throw std::invalid_argument(error_msg + " must be 3 ints"); } + auto const old_node_grid = ::node_grid; auto const new_node_grid = Utils::Vector3i{vec.begin(), vec.end()}; - auto const n_nodes_old = Utils::product(::node_grid); + auto const n_nodes_old = Utils::product(old_node_grid); auto const n_nodes_new = Utils::product(new_node_grid); if (n_nodes_new != n_nodes_old) { std::stringstream reason; @@ -100,8 +101,14 @@ class CellSystem : public AutoParameters { << "with new node grid [" << new_node_grid << "]"; throw std::invalid_argument(error_msg + reason.str()); } - ::node_grid = new_node_grid; - on_nodegrid_change(); + try { + ::node_grid = new_node_grid; + on_node_grid_change(); + } catch (...) { + ::node_grid = old_node_grid; + on_node_grid_change(); + throw; + } }); }, []() { return pack_vector(::node_grid); }}, diff --git a/src/script_interface/cluster_analysis/ClusterStructure.hpp b/src/script_interface/cluster_analysis/ClusterStructure.hpp index f9ad0e4b8ef..034b3f0ca50 100644 --- a/src/script_interface/cluster_analysis/ClusterStructure.hpp +++ b/src/script_interface/cluster_analysis/ClusterStructure.hpp @@ -26,7 +26,7 @@ #include "script_interface/ScriptInterface.hpp" #include "script_interface/cluster_analysis/Cluster.hpp" -#include "script_interface/pair_criteria/pair_criteria.hpp" +#include "script_interface/pair_criteria/PairCriterion.hpp" #include #include diff --git a/src/script_interface/electrostatics/Actor.hpp b/src/script_interface/electrostatics/Actor.hpp new file mode 100644 index 00000000000..090b6529794 --- /dev/null +++ b/src/script_interface/electrostatics/Actor.hpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_ACTOR_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_ACTOR_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "script_interface/Context.hpp" +#include "script_interface/Variant.hpp" +#include "script_interface/auto_parameters/AutoParameters.hpp" +#include "script_interface/get_value.hpp" + +#include +#include +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +/** + * @brief Common interface for electrostatic actors. + * Several methods are defined in initialize.cpp since they + * depend on symbols only available in @ref coulomb.hpp, which cannot be + * included in this header file for separation of concerns reasons. + */ +template +class Actor : public AutoParameters> { +protected: + using SIActorClass = SIClass; + using CoreActorClass = CoreClass; + using AutoParameters>::context; + using AutoParameters>::add_parameters; + using AutoParameters>::do_set_parameter; + std::shared_ptr m_actor; + +public: + Actor(); + + Variant do_call_method(std::string const &name, + VariantMap const ¶ms) override; + + std::shared_ptr actor() { return m_actor; } + std::shared_ptr actor() const { return m_actor; } + +protected: + void set_charge_neutrality_tolerance(VariantMap const ¶ms) { + auto const key_chk = std::string("check_neutrality"); + auto const key_tol = std::string("charge_neutrality_tolerance"); + if (params.count(key_tol)) { + do_set_parameter(key_tol, params.at(key_tol)); + } + do_set_parameter(key_chk, params.at(key_chk)); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // ELECTROSTATICS +#endif diff --git a/src/script_interface/electrostatics/Actor_impl.hpp b/src/script_interface/electrostatics/Actor_impl.hpp new file mode 100644 index 00000000000..b16c426afda --- /dev/null +++ b/src/script_interface/electrostatics/Actor_impl.hpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "Actor.hpp" + +#include "core/electrostatics/coulomb.hpp" +#include "core/electrostatics/registration.hpp" + +#include "script_interface/auto_parameters/AutoParameter.hpp" + +namespace ScriptInterface { +namespace Coulomb { + +template Actor::Actor() { + add_parameters({ + {"prefactor", AutoParameter::read_only, + [this]() { return actor()->prefactor; }}, + {"check_neutrality", + [this](Variant const &value) { + auto const flag = get_value(value); + auto &tolerance = actor()->charge_neutrality_tolerance; + if (flag) { + if (tolerance == -1.) { + tolerance = actor()->charge_neutrality_tolerance_default; + } + } else { + tolerance = -1.; + } + }, + [this]() { + auto const tolerance = actor()->charge_neutrality_tolerance; + return Variant{tolerance != -1.}; + }}, + {"charge_neutrality_tolerance", + [this](Variant const &value) { + auto &tolerance = actor()->charge_neutrality_tolerance; + if (is_none(value)) { + tolerance = -1.; + } else { + auto const new_tolerance = get_value(value); + if (new_tolerance < 0.) { + if (context()->is_head_node()) { + throw std::domain_error( + "Parameter 'charge_neutrality_tolerance' must be >= 0"); + } + throw Exception(""); + } + tolerance = new_tolerance; + } + }, + [this]() { + auto const tolerance = actor()->charge_neutrality_tolerance; + if (tolerance == -1.) { + return make_variant(none); + } + return Variant{tolerance}; + }}, + }); +} + +template +Variant Actor::do_call_method(std::string const &name, + VariantMap const ¶ms) { + if (name == "activate") { + context()->parallel_try_catch([&]() { ::Coulomb::add_actor(actor()); }); + return {}; + } + if (name == "deactivate") { + context()->parallel_try_catch([&]() { ::Coulomb::remove_actor(actor()); }); + return {}; + } + return {}; +} + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // ELECTROSTATICS diff --git a/src/script_interface/electrostatics/CMakeLists.txt b/src/script_interface/electrostatics/CMakeLists.txt new file mode 100644 index 00000000000..43c571ff9a2 --- /dev/null +++ b/src/script_interface/electrostatics/CMakeLists.txt @@ -0,0 +1,2 @@ +target_sources(ScriptInterface + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/initialize.cpp) diff --git a/src/script_interface/electrostatics/CoulombMMM1D.hpp b/src/script_interface/electrostatics/CoulombMMM1D.hpp new file mode 100644 index 00000000000..a872c33d42e --- /dev/null +++ b/src/script_interface/electrostatics/CoulombMMM1D.hpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_MMM1D_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_MMM1D_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "Actor.hpp" + +#include "core/electrostatics/mmm1d.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class CoulombMMM1D : public Actor { + +public: + CoulombMMM1D() { + add_parameters({ + {"is_tuned", AutoParameter::read_only, + [this]() { return actor()->is_tuned(); }}, + {"far_switch_radius", AutoParameter::read_only, + [this]() { return actor()->far_switch_radius; }}, + {"maxPWerror", AutoParameter::read_only, + [this]() { return actor()->maxPWerror; }}, + {"timings", AutoParameter::read_only, + [this]() { return actor()->tune_timings; }}, + {"verbose", AutoParameter::read_only, + [this]() { return actor()->tune_verbose; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + context()->parallel_try_catch([this, ¶ms]() { + m_actor = std::make_shared( + get_value(params, "prefactor"), + get_value(params, "maxPWerror"), + get_value(params, "far_switch_radius"), + get_value(params, "timings"), + get_value(params, "verbose")); + }); + set_charge_neutrality_tolerance(params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // ELECTROSTATICS +#endif diff --git a/src/script_interface/electrostatics/CoulombMMM1DGpu.hpp b/src/script_interface/electrostatics/CoulombMMM1DGpu.hpp new file mode 100644 index 00000000000..ebd356fc974 --- /dev/null +++ b/src/script_interface/electrostatics/CoulombMMM1DGpu.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_MMM1D_GPU_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_MMM1D_GPU_HPP + +#include "config.hpp" + +#ifdef MMM1D_GPU + +#include "Actor.hpp" + +#include "core/electrostatics/mmm1d_gpu.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class CoulombMMM1DGpu : public Actor { + +public: + CoulombMMM1DGpu() { + add_parameters({ + {"is_tuned", AutoParameter::read_only, + [this]() { return actor()->is_tuned(); }}, + {"far_switch_radius", AutoParameter::read_only, + [this]() { return actor()->far_switch_radius; }}, + {"maxPWerror", AutoParameter::read_only, + [this]() { return actor()->maxPWerror; }}, + {"bessel_cutoff", AutoParameter::read_only, + [this]() { return actor()->bessel_cutoff; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + context()->parallel_try_catch([this, ¶ms]() { + m_actor = std::make_shared( + get_value(params, "prefactor"), + get_value(params, "maxPWerror"), + get_value(params, "far_switch_radius"), + get_value(params, "bessel_cutoff")); + }); + set_charge_neutrality_tolerance(params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // MMM1D_GPU +#endif diff --git a/src/script_interface/electrostatics/CoulombP3M.hpp b/src/script_interface/electrostatics/CoulombP3M.hpp new file mode 100644 index 00000000000..14e0b3e7643 --- /dev/null +++ b/src/script_interface/electrostatics/CoulombP3M.hpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_COULOMB_P3M_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_COULOMB_P3M_HPP + +#include "config.hpp" + +#ifdef P3M + +#include "Actor.hpp" + +#include "core/electrostatics/p3m.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class CoulombP3M : public Actor { + bool m_tune; + +public: + CoulombP3M() { + add_parameters({ + {"alpha_L", AutoParameter::read_only, + [this]() { return actor()->p3m.params.alpha_L; }}, + {"r_cut_iL", AutoParameter::read_only, + [this]() { return actor()->p3m.params.r_cut_iL; }}, + {"mesh", AutoParameter::read_only, + [this]() { return actor()->p3m.params.mesh; }}, + {"mesh_off", AutoParameter::read_only, + [this]() { return actor()->p3m.params.mesh_off; }}, + {"cao", AutoParameter::read_only, + [this]() { return actor()->p3m.params.cao; }}, + {"accuracy", AutoParameter::read_only, + [this]() { return actor()->p3m.params.accuracy; }}, + {"epsilon", AutoParameter::read_only, + [this]() { return actor()->p3m.params.epsilon; }}, + {"a", AutoParameter::read_only, + [this]() { return actor()->p3m.params.a; }}, + {"alpha", AutoParameter::read_only, + [this]() { return actor()->p3m.params.alpha; }}, + {"r_cut", AutoParameter::read_only, + [this]() { return actor()->p3m.params.r_cut; }}, + {"is_tuned", AutoParameter::read_only, + [this]() { return actor()->is_tuned(); }}, + {"verbose", AutoParameter::read_only, + [this]() { return actor()->tune_verbose; }}, + {"timings", AutoParameter::read_only, + [this]() { return actor()->tune_timings; }}, + {"tune", AutoParameter::read_only, [this]() { return m_tune; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + m_tune = get_value(params, "tune"); + context()->parallel_try_catch([&]() { + auto p3m = P3MParameters{get_value(params, "tune"), + get_value(params, "epsilon"), + get_value(params, "r_cut"), + get_value(params, "mesh"), + get_value(params, "mesh_off"), + get_value(params, "cao"), + get_value(params, "alpha"), + get_value(params, "accuracy")}; + m_actor = std::make_shared( + std::move(p3m), get_value(params, "prefactor"), + get_value(params, "timings"), + get_value(params, "verbose")); + }); + set_charge_neutrality_tolerance(params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // P3M +#endif diff --git a/src/script_interface/electrostatics/CoulombP3MGPU.hpp b/src/script_interface/electrostatics/CoulombP3MGPU.hpp new file mode 100644 index 00000000000..fec2219523c --- /dev/null +++ b/src/script_interface/electrostatics/CoulombP3MGPU.hpp @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_COULOMB_P3M_GPU_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_COULOMB_P3M_GPU_HPP + +#include "config.hpp" + +#ifdef P3M +#ifdef CUDA + +#include "Actor.hpp" + +#include "core/electrostatics/p3m_gpu.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class CoulombP3MGPU : public Actor { + bool m_tune; + +public: + CoulombP3MGPU() { + add_parameters({ + {"alpha_L", AutoParameter::read_only, + [this]() { return actor()->p3m.params.alpha_L; }}, + {"r_cut_iL", AutoParameter::read_only, + [this]() { return actor()->p3m.params.r_cut_iL; }}, + {"mesh", AutoParameter::read_only, + [this]() { return actor()->p3m.params.mesh; }}, + {"mesh_off", AutoParameter::read_only, + [this]() { return actor()->p3m.params.mesh_off; }}, + {"cao", AutoParameter::read_only, + [this]() { return actor()->p3m.params.cao; }}, + {"accuracy", AutoParameter::read_only, + [this]() { return actor()->p3m.params.accuracy; }}, + {"epsilon", AutoParameter::read_only, + [this]() { return actor()->p3m.params.epsilon; }}, + {"a", AutoParameter::read_only, + [this]() { return actor()->p3m.params.a; }}, + {"alpha", AutoParameter::read_only, + [this]() { return actor()->p3m.params.alpha; }}, + {"r_cut", AutoParameter::read_only, + [this]() { return actor()->p3m.params.r_cut; }}, + {"is_tuned", AutoParameter::read_only, + [this]() { return actor()->is_tuned(); }}, + {"verbose", AutoParameter::read_only, + [this]() { return actor()->tune_verbose; }}, + {"timings", AutoParameter::read_only, + [this]() { return actor()->tune_timings; }}, + {"tune", AutoParameter::read_only, [this]() { return m_tune; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + m_tune = get_value(params, "tune"); + context()->parallel_try_catch([&]() { + auto p3m = P3MParameters{get_value(params, "tune"), + get_value(params, "epsilon"), + get_value(params, "r_cut"), + get_value(params, "mesh"), + get_value(params, "mesh_off"), + get_value(params, "cao"), + get_value(params, "alpha"), + get_value(params, "accuracy")}; + m_actor = std::make_shared( + std::move(p3m), get_value(params, "prefactor"), + get_value(params, "timings"), + get_value(params, "verbose")); + }); + m_actor->request_gpu(); + set_charge_neutrality_tolerance(params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // CUDA +#endif // P3M +#endif diff --git a/src/script_interface/electrostatics/CoulombScafacos.hpp b/src/script_interface/electrostatics/CoulombScafacos.hpp new file mode 100644 index 00000000000..57c7ed059f3 --- /dev/null +++ b/src/script_interface/electrostatics/CoulombScafacos.hpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_COULOMB_SCAFACOS_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_COULOMB_SCAFACOS_HPP + +#include "config.hpp" + +#ifdef SCAFACOS + +#include "Actor.hpp" + +#include "core/electrostatics/scafacos.hpp" +#include "core/scafacos/ScafacosContextBase.hpp" + +#include "script_interface/get_value.hpp" +#include "script_interface/scafacos/scafacos.hpp" + +#include +#include +#include +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class CoulombScafacos : public Actor { +public: + CoulombScafacos() { + add_parameters({ + {"method_name", AutoParameter::read_only, + [this]() { return actor()->get_method(); }}, + {"method_params", AutoParameter::read_only, + [this]() { + auto const m_tuned_methods = + std::set{"ewald", "p2nfft", "p3m"}; + auto parameters_string = actor()->get_parameters(); + auto const method_name = actor()->get_method(); + auto const delegate = actor()->get_near_field_delegation(); + if (delegate and m_tuned_methods.count(method_name)) { + auto const tuned_r_cut = actor()->get_r_cut(); + auto const field_name = method_name + "_r_cut"; + std::ostringstream serializer; + serializer << std::scientific << std::setprecision(17); + serializer << tuned_r_cut; + auto const tuned_r_cut_string = serializer.str(); + auto const replacement = + "," + field_name + "," + tuned_r_cut_string; + if (parameters_string.find(field_name) == field_name.npos) { + parameters_string += replacement; + } else { + auto const field_pattern = + std::regex("," + field_name + ",[0-9eE\\-\\+\\.]+"); + parameters_string = std::regex_replace( + parameters_string, std::regex(field_pattern), replacement); + } + } + return Scafacos::deserialize_parameters(parameters_string); + }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + auto const method_name = get_value(params, "method_name"); + auto const param_list = params.at("method_params"); + auto const prefactor = get_value(params, "prefactor"); + + context()->parallel_try_catch([&]() { + ScafacosContextBase::sanity_check_method(method_name); + auto const method_params = Scafacos::serialize_parameters(param_list); + m_actor = make_coulomb_scafacos(method_name, method_params); + actor()->set_prefactor(prefactor); + }); + set_charge_neutrality_tolerance(params); + } + + Variant do_call_method(std::string const &name, + VariantMap const ¶ms) override { + if (name == "set_near_field_delegation") { + auto const delegate = get_value(params, "delegate"); + context()->parallel_try_catch( + [&]() { actor()->set_near_field_delegation(delegate); }); + return {}; + } + if (name == "get_near_field_delegation") { + return actor()->get_near_field_delegation(); + } + if (name == "get_available_methods") { + return make_vector_of_variants(Scafacos::available_methods()); + } + return Actor::do_call_method(name, params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // SCAFACOS +#endif diff --git a/src/script_interface/electrostatics/DebyeHueckel.hpp b/src/script_interface/electrostatics/DebyeHueckel.hpp new file mode 100644 index 00000000000..f60b427f54f --- /dev/null +++ b/src/script_interface/electrostatics/DebyeHueckel.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_DH_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_DH_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "Actor.hpp" + +#include "core/electrostatics/debye_hueckel.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class DebyeHueckel : public Actor { + +public: + DebyeHueckel() { + add_parameters({ + {"kappa", AutoParameter::read_only, + [this]() { return actor()->kappa; }}, + {"r_cut", AutoParameter::read_only, + [this]() { return actor()->r_cut; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + context()->parallel_try_catch([&]() { + m_actor = std::make_shared( + get_value(params, "prefactor"), + get_value(params, "kappa"), + get_value(params, "r_cut")); + }); + set_charge_neutrality_tolerance(params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // ELECTROSTATICS +#endif diff --git a/src/script_interface/electrostatics/ElectrostaticLayerCorrection.hpp b/src/script_interface/electrostatics/ElectrostaticLayerCorrection.hpp new file mode 100644 index 00000000000..44c4ef8f8ab --- /dev/null +++ b/src/script_interface/electrostatics/ElectrostaticLayerCorrection.hpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_ELC_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_ELC_HPP + +#include "config.hpp" + +#ifdef P3M + +#include "Actor.hpp" + +#include "CoulombP3M.hpp" + +#include "core/electrostatics/elc.hpp" + +#include "script_interface/get_value.hpp" + +#include "boost/variant.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class ElectrostaticLayerCorrection + : public Actor { + + using BaseSolver = boost::variant< +#ifdef CUDA + std::shared_ptr, +#endif // CUDA + std::shared_ptr>; + BaseSolver m_solver; + +public: + ElectrostaticLayerCorrection() { + add_parameters({ + {"maxPWerror", AutoParameter::read_only, + [this]() { return actor()->elc.maxPWerror; }}, + {"gap_size", AutoParameter::read_only, + [this]() { return actor()->elc.gap_size; }}, + {"far_cut", AutoParameter::read_only, + [this]() { return actor()->elc.far_cut; }}, + {"neutralize", AutoParameter::read_only, + [this]() { return actor()->elc.neutralize; }}, + {"delta_mid_top", AutoParameter::read_only, + [this]() { return actor()->elc.delta_mid_top; }}, + {"delta_mid_bot", AutoParameter::read_only, + [this]() { return actor()->elc.delta_mid_bot; }}, + {"const_pot", AutoParameter::read_only, + [this]() { return actor()->elc.const_pot; }}, + {"pot_diff", AutoParameter::read_only, + [this]() { return actor()->elc.pot_diff; }}, + {"actor", AutoParameter::read_only, + [this]() { + return boost::apply_visitor( + [](auto &solver) { return Variant{solver}; }, m_solver); + }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + ::ElectrostaticLayerCorrection::BaseSolver solver; + auto so_ptr = get_value(params, "actor"); + context()->parallel_try_catch([&]() { +#ifdef CUDA + if (auto so_solver = std::dynamic_pointer_cast(so_ptr)) { + solver = so_solver->actor(); + m_solver = so_solver; + } else +#endif // CUDA + if (auto so_solver = std::dynamic_pointer_cast(so_ptr)) { + solver = so_solver->actor(); + m_solver = so_solver; + } else { + throw std::invalid_argument("Parameter 'actor' of type " + + so_ptr->name().to_string() + + " isn't supported by ELC"); + } + }); + context()->parallel_try_catch([&]() { + auto elc = elc_data{get_value(params, "maxPWerror"), + get_value(params, "gap_size"), + get_value(params, "far_cut"), + get_value(params, "neutralize"), + get_value(params, "delta_mid_top"), + get_value(params, "delta_mid_bot"), + get_value(params, "const_pot"), + get_value(params, "pot_diff")}; + m_actor = + std::make_shared(std::move(elc), std::move(solver)); + }); + set_charge_neutrality_tolerance(params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // P3M +#endif diff --git a/src/script_interface/electrostatics/ICCStar.hpp b/src/script_interface/electrostatics/ICCStar.hpp new file mode 100644 index 00000000000..bf1734a1c16 --- /dev/null +++ b/src/script_interface/electrostatics/ICCStar.hpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_ICC_STAR_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_ICC_STAR_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "core/electrostatics/icc.hpp" + +#include "core/electrostatics/registration.hpp" + +#include + +#include "script_interface/Context.hpp" +#include "script_interface/auto_parameters/AutoParameters.hpp" +#include "script_interface/get_value.hpp" + +#include +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class ICCStar : public AutoParameters<::ICCStar> { + using CoreActorClass = ::ICCStar; + std::shared_ptr m_actor; + +public: + ICCStar() { + add_parameters({ + {"n_icc", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.n_icc; }}, + {"max_iterations", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.max_iterations; }}, + {"eps_out", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.eps_out; }}, + {"areas", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.areas; }}, + {"epsilons", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.epsilons; }}, + {"sigmas", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.sigmas; }}, + {"convergence", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.convergence; }}, + {"normals", AutoParameter::read_only, + [this]() { + return make_vector_of_variants(actor()->icc_cfg.normals); + }}, + {"ext_field", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.ext_field; }}, + {"relaxation", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.relaxation; }}, + {"citeration", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.citeration; }}, + {"first_id", AutoParameter::read_only, + [this]() { return actor()->icc_cfg.first_id; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + auto icc_parameters = ::icc_data{ + get_value(params, "n_icc"), + get_value(params, "max_iterations"), + get_value(params, "eps_out"), + get_value>(params, "areas"), + get_value>(params, "epsilons"), + get_value>(params, "sigmas"), + get_value(params, "convergence"), + get_value>(params, "normals"), + get_value(params, "ext_field"), + get_value(params, "relaxation"), + 0, + get_value(params, "first_id"), + }; + context()->parallel_try_catch([&]() { + m_actor = std::make_shared(std::move(icc_parameters)); + }); + } + + Variant do_call_method(std::string const &name, + VariantMap const ¶ms) override { + if (name == "activate") { + context()->parallel_try_catch([&]() { ::Coulomb::add_actor(actor()); }); + return {}; + } + if (name == "deactivate") { + context()->parallel_try_catch( + [&]() { ::Coulomb::remove_actor(actor()); }); + return {}; + } + return {}; + } + + std::shared_ptr actor() { return m_actor; } + std::shared_ptr actor() const { return m_actor; } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // ELECTROSTATICS +#endif diff --git a/src/script_interface/electrostatics/ReactionField.hpp b/src/script_interface/electrostatics/ReactionField.hpp new file mode 100644 index 00000000000..f07dde735cd --- /dev/null +++ b/src/script_interface/electrostatics/ReactionField.hpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_RF_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_RF_HPP + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "Actor.hpp" + +#include "core/electrostatics/reaction_field.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +class ReactionField : public Actor { + +public: + ReactionField() { + add_parameters({ + {"kappa", AutoParameter::read_only, + [this]() { return actor()->kappa; }}, + {"epsilon1", AutoParameter::read_only, + [this]() { return actor()->epsilon1; }}, + {"epsilon2", AutoParameter::read_only, + [this]() { return actor()->epsilon2; }}, + {"r_cut", AutoParameter::read_only, + [this]() { return actor()->r_cut; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + context()->parallel_try_catch([&]() { + m_actor = std::make_shared( + get_value(params, "prefactor"), + get_value(params, "kappa"), + get_value(params, "epsilon1"), + get_value(params, "epsilon2"), + get_value(params, "r_cut")); + }); + set_charge_neutrality_tolerance(params); + } +}; + +} // namespace Coulomb +} // namespace ScriptInterface + +#endif // ELECTROSTATICS +#endif diff --git a/src/script_interface/electrostatics/initialize.cpp b/src/script_interface/electrostatics/initialize.cpp new file mode 100644 index 00000000000..743323670e5 --- /dev/null +++ b/src/script_interface/electrostatics/initialize.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "initialize.hpp" + +#include "config.hpp" + +#ifdef ELECTROSTATICS + +#include "Actor_impl.hpp" + +#include "CoulombMMM1D.hpp" +#include "CoulombMMM1DGpu.hpp" +#include "CoulombP3M.hpp" +#include "CoulombP3MGPU.hpp" +#include "CoulombScafacos.hpp" +#include "DebyeHueckel.hpp" +#include "ElectrostaticLayerCorrection.hpp" +#include "ICCStar.hpp" +#include "ReactionField.hpp" + +#include "core/electrostatics/coulomb.hpp" +#include "core/electrostatics/registration.hpp" + +#include "script_interface/auto_parameters/AutoParameter.hpp" + +#endif // ELECTROSTATICS + +#include + +namespace ScriptInterface { +namespace Coulomb { + +void initialize(Utils::Factory *om) { +#ifdef ELECTROSTATICS + om->register_new("Coulomb::DebyeHueckel"); +#ifdef P3M + om->register_new("Coulomb::CoulombP3M"); +#ifdef CUDA + om->register_new("Coulomb::CoulombP3MGPU"); +#endif + om->register_new( + "Coulomb::ElectrostaticLayerCorrection"); +#endif // P3M + om->register_new("Coulomb::ICCStar"); +#ifdef MMM1D_GPU + om->register_new("Coulomb::CoulombMMM1DGpu"); +#endif + om->register_new("Coulomb::CoulombMMM1D"); +#ifdef SCAFACOS + om->register_new("Coulomb::CoulombScafacos"); +#endif + om->register_new("Coulomb::ReactionField"); +#endif // ELECTROSTATICS +} + +} // namespace Coulomb +} // namespace ScriptInterface diff --git a/src/core/actor/ActorList.cpp b/src/script_interface/electrostatics/initialize.hpp similarity index 61% rename from src/core/actor/ActorList.cpp rename to src/script_interface/electrostatics/initialize.hpp index e30999161d8..62a7c2063e6 100644 --- a/src/core/actor/ActorList.cpp +++ b/src/script_interface/electrostatics/initialize.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2019 The ESPResSo project + * Copyright (C) 2022 The ESPResSo project * * This file is part of ESPResSo. * @@ -17,15 +17,19 @@ * along with this program. If not, see . */ -#include "ActorList.hpp" -#include "Actor.hpp" -#include -#include +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_INITIALIZE_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_ELECTROSTATICS_INITIALIZE_HPP -void ActorList::add(Actor *actor) { this->push_back(actor); } +#include -void ActorList::remove(Actor *actor) { - auto needle = std::find(this->begin(), this->end(), actor); - assert(needle != this->end()); - this->erase(needle); -} +#include + +namespace ScriptInterface { +namespace Coulomb { + +void initialize(Utils::Factory *om); + +} /* namespace Coulomb */ +} /* namespace ScriptInterface */ + +#endif diff --git a/src/script_interface/initialize.cpp b/src/script_interface/initialize.cpp index ee49180da10..fdb7eb4ed99 100644 --- a/src/script_interface/initialize.cpp +++ b/src/script_interface/initialize.cpp @@ -34,9 +34,11 @@ #include "bond_breakage/initialize.hpp" #include "cell_system/initialize.hpp" #include "collision_detection/initialize.hpp" +#include "electrostatics/initialize.hpp" #include "interactions/initialize.hpp" #include "lbboundaries/initialize.hpp" #include "lees_edwards/initialize.hpp" +#include "magnetostatics/initialize.hpp" #include "mpiio/initialize.hpp" #include "observables/initialize.hpp" #include "reaction_methods/initialize.hpp" @@ -54,6 +56,8 @@ void initialize(Utils::Factory *f) { CellSystem::initialize(f); Observables::initialize(f); ClusterAnalysis::initialize(f); + Coulomb::initialize(f); + Dipoles::initialize(f); Interactions::initialize(f); LBBoundaries::initialize(f); LeesEdwards::initialize(f); diff --git a/src/script_interface/magnetostatics/Actor.hpp b/src/script_interface/magnetostatics/Actor.hpp new file mode 100644 index 00000000000..f307293ae15 --- /dev/null +++ b/src/script_interface/magnetostatics/Actor.hpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_ACTOR_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_ACTOR_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "script_interface/Context.hpp" +#include "script_interface/Variant.hpp" +#include "script_interface/auto_parameters/AutoParameters.hpp" +#include "script_interface/get_value.hpp" + +#include +#include +#include + +namespace ScriptInterface { +namespace Dipoles { + +/** + * @brief Common interface for magnetostatic actors. + * Several methods are defined in initialize.cpp since they + * depend on symbols only available in @ref dipoles.hpp, which cannot be + * included in this header file for separation of concerns reasons. + */ +template +class Actor : public AutoParameters> { +protected: + using SIActorClass = SIClass; + using CoreActorClass = CoreClass; + using AutoParameters>::context; + using AutoParameters>::add_parameters; + std::shared_ptr m_actor; + +public: + Actor() { + add_parameters({ + {"prefactor", AutoParameter::read_only, + [this]() { return actor()->prefactor; }}, + }); + } + + Variant do_call_method(std::string const &name, + VariantMap const ¶ms) override; + + std::shared_ptr actor() { return m_actor; } + std::shared_ptr actor() const { return m_actor; } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DIPOLES +#endif diff --git a/src/script_interface/magnetostatics/Actor_impl.hpp b/src/script_interface/magnetostatics/Actor_impl.hpp new file mode 100644 index 00000000000..486cf4e78cd --- /dev/null +++ b/src/script_interface/magnetostatics/Actor_impl.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#ifdef DIPOLES + +#include "Actor.hpp" + +#include "core/magnetostatics/dipoles.hpp" +#include "core/magnetostatics/registration.hpp" + +#include "script_interface/auto_parameters/AutoParameter.hpp" + +namespace ScriptInterface { +namespace Dipoles { + +template +Variant Actor::do_call_method(std::string const &name, + VariantMap const ¶ms) { + if (name == "activate") { + context()->parallel_try_catch([&]() { ::Dipoles::add_actor(actor()); }); + return {}; + } + if (name == "deactivate") { + context()->parallel_try_catch([&]() { ::Dipoles::remove_actor(actor()); }); + return {}; + } + return {}; +} + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DIPOLES diff --git a/src/script_interface/magnetostatics/CMakeLists.txt b/src/script_interface/magnetostatics/CMakeLists.txt new file mode 100644 index 00000000000..43c571ff9a2 --- /dev/null +++ b/src/script_interface/magnetostatics/CMakeLists.txt @@ -0,0 +1,2 @@ +target_sources(ScriptInterface + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/initialize.cpp) diff --git a/src/script_interface/magnetostatics/DipolarBarnesHutGpu.hpp b/src/script_interface/magnetostatics/DipolarBarnesHutGpu.hpp new file mode 100644 index 00000000000..18bbf46f614 --- /dev/null +++ b/src/script_interface/magnetostatics/DipolarBarnesHutGpu.hpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_BH_GPU_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_BH_GPU_HPP + +#include "config.hpp" + +#ifdef DIPOLAR_BARNES_HUT + +#include "Actor.hpp" + +#include "core/magnetostatics/barnes_hut_gpu.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Dipoles { + +class DipolarBarnesHutGpu + : public Actor { +public: + DipolarBarnesHutGpu() { + add_parameters({ + {"epssq", AutoParameter::read_only, + [this]() { return actor()->m_epssq; }}, + {"itolsq", AutoParameter::read_only, + [this]() { return actor()->m_itolsq; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + m_actor = + std::make_shared(get_value(params, "prefactor"), + get_value(params, "epssq"), + get_value(params, "itolsq")); + } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DIPOLAR_BARNES_HUT +#endif diff --git a/src/script_interface/magnetostatics/DipolarDirectSum.hpp b/src/script_interface/magnetostatics/DipolarDirectSum.hpp new file mode 100644 index 00000000000..ac69ba75f92 --- /dev/null +++ b/src/script_interface/magnetostatics/DipolarDirectSum.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "Actor.hpp" + +#include "core/magnetostatics/dds.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Dipoles { + +class DipolarDirectSum : public Actor { +public: + void do_construct(VariantMap const ¶ms) override { + context()->parallel_try_catch([this, ¶ms]() { + m_actor = std::make_shared( + get_value(params, "prefactor")); + }); + } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DIPOLES +#endif diff --git a/src/script_interface/magnetostatics/DipolarDirectSumGpu.hpp b/src/script_interface/magnetostatics/DipolarDirectSumGpu.hpp new file mode 100644 index 00000000000..226711e7445 --- /dev/null +++ b/src/script_interface/magnetostatics/DipolarDirectSumGpu.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_GPU_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_DIRECT_SUM_GPU_HPP + +#include "config.hpp" + +#ifdef DIPOLAR_DIRECT_SUM + +#include "Actor.hpp" + +#include "core/magnetostatics/dds_gpu.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Dipoles { + +class DipolarDirectSumGpu + : public Actor { +public: + DipolarDirectSumGpu() = default; + + void do_construct(VariantMap const ¶ms) override { + context()->parallel_try_catch([this, ¶ms]() { + m_actor = std::make_shared( + get_value(params, "prefactor")); + }); + } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DIPOLAR_DIRECT_SUM +#endif diff --git a/src/script_interface/magnetostatics/DipolarDirectSumWithReplica.hpp b/src/script_interface/magnetostatics/DipolarDirectSumWithReplica.hpp new file mode 100644 index 00000000000..d7965def6e9 --- /dev/null +++ b/src/script_interface/magnetostatics/DipolarDirectSumWithReplica.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DDS_REPLICA_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DDS_REPLICA_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "Actor.hpp" + +#include "core/magnetostatics/dds_replica.hpp" + +#include "script_interface/get_value.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Dipoles { + +class DipolarDirectSumWithReplica + : public Actor { +public: + DipolarDirectSumWithReplica() { + add_parameters({ + {"n_replica", AutoParameter::read_only, + [this]() { return actor()->n_replica; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + context()->parallel_try_catch([this, ¶ms]() { + m_actor = std::make_shared( + get_value(params, "prefactor"), + get_value(params, "n_replica")); + }); + } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DIPOLES +#endif diff --git a/src/script_interface/magnetostatics/DipolarLayerCorrection.hpp b/src/script_interface/magnetostatics/DipolarLayerCorrection.hpp new file mode 100644 index 00000000000..abed1215cce --- /dev/null +++ b/src/script_interface/magnetostatics/DipolarLayerCorrection.hpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_DIPOLAR_LAYER_CORRECTION_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_DIPOLAR_LAYER_CORRECTION_HPP + +#include "config.hpp" + +#ifdef DIPOLES + +#include "Actor.hpp" + +#include "DipolarDirectSumWithReplica.hpp" +#include "DipolarP3M.hpp" + +#include "core/magnetostatics/dlc.hpp" + +#include "script_interface/get_value.hpp" + +#include "boost/variant.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Dipoles { + +class DipolarLayerCorrection + : public Actor { + using DipolarDSR = DipolarDirectSumWithReplica; + using BaseSolver = boost::variant< +#ifdef DP3M + std::shared_ptr, +#endif + std::shared_ptr>; + BaseSolver m_solver; + +public: + DipolarLayerCorrection() { + add_parameters({ + {"maxPWerror", AutoParameter::read_only, + [this]() { return actor()->dlc.maxPWerror; }}, + {"gap_size", AutoParameter::read_only, + [this]() { return actor()->dlc.gap_size; }}, + {"far_cut", AutoParameter::read_only, + [this]() { return actor()->dlc.far_cut; }}, + {"actor", AutoParameter::read_only, + [this]() { + return boost::apply_visitor( + [](auto &solver) { return Variant{solver}; }, m_solver); + }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + ::DipolarLayerCorrection::BaseSolver solver; + auto so_ptr = get_value(params, "actor"); + context()->parallel_try_catch([&]() { +#ifdef DP3M + if (auto so_solver = std::dynamic_pointer_cast(so_ptr)) { + solver = so_solver->actor(); + m_solver = so_solver; + } else +#endif // DP3M + if (auto so_solver = std::dynamic_pointer_cast(so_ptr)) { + solver = so_solver->actor(); + m_solver = so_solver; + } else { + throw std::invalid_argument("Parameter 'actor' of type " + + so_ptr->name().to_string() + + " isn't supported by DLC"); + } + }); + context()->parallel_try_catch([&]() { + auto dlc = dlc_data(get_value(params, "maxPWerror"), + get_value(params, "gap_size"), + get_value(params, "far_cut")); + m_actor = + std::make_shared(std::move(dlc), std::move(solver)); + }); + } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DIPOLES +#endif diff --git a/src/script_interface/magnetostatics/DipolarP3M.hpp b/src/script_interface/magnetostatics/DipolarP3M.hpp new file mode 100644 index 00000000000..6923ded75b6 --- /dev/null +++ b/src/script_interface/magnetostatics/DipolarP3M.hpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_P3M_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_P3M_HPP + +#include "config.hpp" + +#ifdef DP3M + +#include "Actor.hpp" + +#include "core/magnetostatics/dp3m.hpp" + +#include "script_interface/get_value.hpp" + +#include + +namespace ScriptInterface { +namespace Dipoles { + +class DipolarP3M : public Actor { + bool m_tune; + +public: + DipolarP3M() { + add_parameters({ + {"alpha_L", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.alpha_L; }}, + {"r_cut_iL", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.r_cut_iL; }}, + {"mesh", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.mesh; }}, + {"mesh_off", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.mesh_off; }}, + {"cao", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.cao; }}, + {"accuracy", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.accuracy; }}, + {"epsilon", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.epsilon; }}, + {"a", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.a; }}, + {"alpha", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.alpha; }}, + {"r_cut", AutoParameter::read_only, + [this]() { return actor()->dp3m.params.r_cut; }}, + {"is_tuned", AutoParameter::read_only, + [this]() { return actor()->is_tuned(); }}, + {"verbose", AutoParameter::read_only, + [this]() { return actor()->tune_verbose; }}, + {"timings", AutoParameter::read_only, + [this]() { return actor()->tune_timings; }}, + {"tune", AutoParameter::read_only, [this]() { return m_tune; }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + m_tune = get_value(params, "tune"); + context()->parallel_try_catch([&]() { + auto p3m = P3MParameters{get_value(params, "tune"), + get_value(params, "epsilon"), + get_value(params, "r_cut"), + get_value(params, "mesh"), + get_value(params, "mesh_off"), + get_value(params, "cao"), + get_value(params, "alpha"), + get_value(params, "accuracy")}; + m_actor = std::make_shared( + std::move(p3m), get_value(params, "prefactor"), + get_value(params, "timings"), + get_value(params, "verbose")); + }); + } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // DP3M +#endif diff --git a/src/script_interface/magnetostatics/DipolarScafacos.hpp b/src/script_interface/magnetostatics/DipolarScafacos.hpp new file mode 100644 index 00000000000..ef53205ef87 --- /dev/null +++ b/src/script_interface/magnetostatics/DipolarScafacos.hpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_SCAFACOS_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_DIPOLAR_SCAFACOS_HPP + +#include "config.hpp" + +#ifdef SCAFACOS_DIPOLES + +#include "Actor.hpp" + +#include "core/magnetostatics/scafacos.hpp" +#include "core/scafacos/ScafacosContextBase.hpp" + +#include "script_interface/scafacos/scafacos.hpp" + +#include +#include + +namespace ScriptInterface { +namespace Dipoles { + +class DipolarScafacos : public Actor { + +public: + DipolarScafacos() { + add_parameters({ + {"method_name", AutoParameter::read_only, + [this]() { return actor()->get_method(); }}, + {"method_params", AutoParameter::read_only, + [this]() { + return Scafacos::deserialize_parameters(actor()->get_parameters()); + }}, + }); + } + + void do_construct(VariantMap const ¶ms) override { + auto const method_name = get_value(params, "method_name"); + auto const param_list = params.at("method_params"); + auto const prefactor = get_value(params, "prefactor"); + + context()->parallel_try_catch([&]() { + if (prefactor <= 0.) { + throw std::domain_error("Parameter 'prefactor' must be > 0"); + } + ScafacosContextBase::sanity_check_method(method_name); + auto const method_params = Scafacos::serialize_parameters(param_list); + m_actor = make_dipolar_scafacos(method_name, method_params); + actor()->prefactor = prefactor; + }); + } + + Variant do_call_method(std::string const &name, + VariantMap const ¶ms) override { + if (name == "get_available_methods") { + return make_vector_of_variants(Scafacos::available_methods()); + } + return Actor::do_call_method(name, params); + } +}; + +} // namespace Dipoles +} // namespace ScriptInterface + +#endif // SCAFACOS_DIPOLES +#endif diff --git a/src/script_interface/magnetostatics/initialize.cpp b/src/script_interface/magnetostatics/initialize.cpp new file mode 100644 index 00000000000..7fd32e36886 --- /dev/null +++ b/src/script_interface/magnetostatics/initialize.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "initialize.hpp" + +#include "config.hpp" + +#ifdef DIPOLES + +#include "Actor_impl.hpp" + +#include "DipolarBarnesHutGpu.hpp" +#include "DipolarDirectSum.hpp" +#include "DipolarDirectSumGpu.hpp" +#include "DipolarDirectSumWithReplica.hpp" +#include "DipolarLayerCorrection.hpp" +#include "DipolarP3M.hpp" +#include "DipolarScafacos.hpp" + +#include "core/magnetostatics/dipoles.hpp" +#include "core/magnetostatics/registration.hpp" + +#include "script_interface/auto_parameters/AutoParameter.hpp" + +#endif // DIPOLES + +#include + +namespace ScriptInterface { +namespace Dipoles { + +void initialize(Utils::Factory *om) { +#ifdef DIPOLES + om->register_new("Dipoles::DipolarDirectSum"); +#ifdef DIPOLAR_DIRECT_SUM + om->register_new("Dipoles::DipolarDirectSumGpu"); +#endif +#ifdef DIPOLAR_BARNES_HUT + om->register_new("Dipoles::DipolarBarnesHutGpu"); +#endif +#ifdef DP3M + om->register_new("Dipoles::DipolarP3M"); +#endif +#ifdef SCAFACOS_DIPOLES + om->register_new("Dipoles::DipolarScafacos"); +#endif + om->register_new("Dipoles::DipolarLayerCorrection"); + om->register_new( + "Dipoles::DipolarDirectSumWithReplica"); +#endif // DIPOLES +} + +} // namespace Dipoles +} // namespace ScriptInterface diff --git a/src/core/electrostatics_magnetostatics/p3m_gpu.hpp b/src/script_interface/magnetostatics/initialize.hpp similarity index 61% rename from src/core/electrostatics_magnetostatics/p3m_gpu.hpp rename to src/script_interface/magnetostatics/initialize.hpp index f173a74c945..e4d771d4cd5 100644 --- a/src/core/electrostatics_magnetostatics/p3m_gpu.hpp +++ b/src/script_interface/magnetostatics/initialize.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2019 The ESPResSo project + * Copyright (C) 2022 The ESPResSo project * * This file is part of ESPResSo. * @@ -16,15 +16,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_GPU_HPP -#define CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_GPU_HPP -/** \file - * P3M electrostatics on GPU. - * - * Implementation in p3m_gpu_cuda.cu. - */ -void p3m_gpu_init(int cao, const int mesh[3], double alpha); -void p3m_gpu_add_farfield_force(); +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_INITIALIZE_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_MAGNETOSTATICS_INITIALIZE_HPP + +#include + +#include + +namespace ScriptInterface { +namespace Dipoles { + +void initialize(Utils::Factory *om); + +} /* namespace Dipoles */ +} /* namespace ScriptInterface */ -#endif /* CORE_ELECTROSTATICS_MAGNETOSTATICS_P3M_GPU_HPP */ +#endif diff --git a/src/script_interface/pair_criteria/BondCriterion.hpp b/src/script_interface/pair_criteria/BondCriterion.hpp new file mode 100644 index 00000000000..e6984e18a15 --- /dev/null +++ b/src/script_interface/pair_criteria/BondCriterion.hpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_BOND_CRITERION_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_BOND_CRITERION_HPP + +#include "script_interface/auto_parameters/AutoParameters.hpp" +#include "script_interface/get_value.hpp" +#include "script_interface/pair_criteria/PairCriterion.hpp" + +#include "core/pair_criteria/BondCriterion.hpp" + +#include +#include + +namespace ScriptInterface { +namespace PairCriteria { + +class BondCriterion : public PairCriterion { +public: + BondCriterion() : m_c(std::make_shared<::PairCriteria::BondCriterion>()) { + add_parameters( + {{"bond_type", + [this](Variant const &v) { m_c->set_bond_type(get_value(v)); }, + [this]() { return m_c->get_bond_type(); }}}); + } + + std::shared_ptr<::PairCriteria::PairCriterion> + pair_criterion() const override { + return m_c; + } + +private: + std::shared_ptr<::PairCriteria::BondCriterion> m_c; +}; + +} /* namespace PairCriteria */ +} /* namespace ScriptInterface */ + +#endif diff --git a/src/script_interface/pair_criteria/DistanceCriterion.hpp b/src/script_interface/pair_criteria/DistanceCriterion.hpp new file mode 100644 index 00000000000..837f76ce3f9 --- /dev/null +++ b/src/script_interface/pair_criteria/DistanceCriterion.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_DISTANCE_CRITERION_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_DISTANCE_CRITERION_HPP + +#include "script_interface/auto_parameters/AutoParameters.hpp" +#include "script_interface/get_value.hpp" +#include "script_interface/pair_criteria/PairCriterion.hpp" + +#include "core/pair_criteria/DistanceCriterion.hpp" + +#include +#include + +namespace ScriptInterface { +namespace PairCriteria { + +class DistanceCriterion : public PairCriterion { +public: + DistanceCriterion() + : m_c(std::make_shared<::PairCriteria::DistanceCriterion>()) { + add_parameters( + {{"cut_off", + [this](Variant const &v) { m_c->set_cut_off(get_value(v)); }, + [this]() { return m_c->get_cut_off(); }}}); + } + + std::shared_ptr<::PairCriteria::PairCriterion> + pair_criterion() const override { + return m_c; + } + +private: + std::shared_ptr<::PairCriteria::DistanceCriterion> m_c; +}; + +} /* namespace PairCriteria */ +} /* namespace ScriptInterface */ + +#endif diff --git a/src/script_interface/pair_criteria/EnergyCriterion.hpp b/src/script_interface/pair_criteria/EnergyCriterion.hpp new file mode 100644 index 00000000000..0211ec73de8 --- /dev/null +++ b/src/script_interface/pair_criteria/EnergyCriterion.hpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_ENERGY_CRITERION_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_ENERGY_CRITERION_HPP + +#include "script_interface/auto_parameters/AutoParameters.hpp" +#include "script_interface/get_value.hpp" +#include "script_interface/pair_criteria/PairCriterion.hpp" + +#include "core/pair_criteria/EnergyCriterion.hpp" + +#include +#include + +namespace ScriptInterface { +namespace PairCriteria { + +class EnergyCriterion : public PairCriterion { +public: + EnergyCriterion() : m_c(std::make_shared<::PairCriteria::EnergyCriterion>()) { + add_parameters( + {{"cut_off", + [this](Variant const &v) { m_c->set_cut_off(get_value(v)); }, + [this]() { return m_c->get_cut_off(); }}}); + } + + std::shared_ptr<::PairCriteria::PairCriterion> + pair_criterion() const override { + return m_c; + } + +private: + std::shared_ptr<::PairCriteria::EnergyCriterion> m_c; +}; + +} /* namespace PairCriteria */ +} /* namespace ScriptInterface */ + +#endif diff --git a/src/script_interface/pair_criteria/PairCriterion.hpp b/src/script_interface/pair_criteria/PairCriterion.hpp new file mode 100644 index 00000000000..d16c4ac2925 --- /dev/null +++ b/src/script_interface/pair_criteria/PairCriterion.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010-2019 The ESPResSo project + * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 + * Max-Planck-Institute for Polymer Research, Theory Group + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_PAIR_CRITERION_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_PAIR_CRITERIA_PAIR_CRITERION_HPP + +#include "script_interface/auto_parameters/AutoParameters.hpp" +#include "script_interface/get_value.hpp" + +#include "core/pair_criteria/PairCriterion.hpp" + +#include +#include +#include + +namespace ScriptInterface { +namespace PairCriteria { + +class PairCriterion : public AutoParameters { +public: + virtual std::shared_ptr<::PairCriteria::PairCriterion> + pair_criterion() const = 0; + Variant do_call_method(std::string const &method, + VariantMap const ¶meters) override { + if (method == "decide") { + return pair_criterion()->decide(get_value(parameters.at("id1")), + get_value(parameters.at("id2"))); + } + throw std::runtime_error("Unknown method called."); + } +}; + +} /* namespace PairCriteria */ +} /* namespace ScriptInterface */ + +#endif diff --git a/src/script_interface/pair_criteria/initialize.cpp b/src/script_interface/pair_criteria/initialize.cpp index c9f490b4cf5..b27407f0ca8 100644 --- a/src/script_interface/pair_criteria/initialize.cpp +++ b/src/script_interface/pair_criteria/initialize.cpp @@ -18,7 +18,11 @@ */ #include "initialize.hpp" -#include "pair_criteria.hpp" + +#include "script_interface/pair_criteria/BondCriterion.hpp" +#include "script_interface/pair_criteria/DistanceCriterion.hpp" +#include "script_interface/pair_criteria/EnergyCriterion.hpp" +#include "script_interface/pair_criteria/PairCriterion.hpp" namespace ScriptInterface { namespace PairCriteria { diff --git a/src/script_interface/pair_criteria/pair_criteria.hpp b/src/script_interface/pair_criteria/pair_criteria.hpp deleted file mode 100644 index 22c1a12a7e0..00000000000 --- a/src/script_interface/pair_criteria/pair_criteria.hpp +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2010-2019 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef SCRIPT_INTERFACE_PAIR_CRITERIA_PAIR_CRITERIA_HPP -#define SCRIPT_INTERFACE_PAIR_CRITERIA_PAIR_CRITERIA_HPP - -#include "../auto_parameters/AutoParameters.hpp" -#include "core/pair_criteria/pair_criteria.hpp" - -#include -#include - -namespace ScriptInterface { -namespace PairCriteria { - -class PairCriterion : public AutoParameters { -public: - virtual std::shared_ptr<::PairCriteria::PairCriterion> - pair_criterion() const = 0; - Variant do_call_method(std::string const &method, - VariantMap const ¶meters) override { - if (method == "decide") { - return pair_criterion()->decide(get_value(parameters.at("id1")), - get_value(parameters.at("id2"))); - } - throw std::runtime_error("Unknown method called."); - } -}; - -class DistanceCriterion : public PairCriterion { -public: - DistanceCriterion() - : m_c(std::make_shared<::PairCriteria::DistanceCriterion>()) { - add_parameters( - {{"cut_off", - [this](Variant const &v) { m_c->set_cut_off(get_value(v)); }, - [this]() { return m_c->get_cut_off(); }}}); - } - - std::shared_ptr<::PairCriteria::PairCriterion> - pair_criterion() const override { - return m_c; - } - -private: - std::shared_ptr<::PairCriteria::DistanceCriterion> m_c; -}; - -class EnergyCriterion : public PairCriterion { -public: - EnergyCriterion() : m_c(std::make_shared<::PairCriteria::EnergyCriterion>()) { - add_parameters( - {{"cut_off", - [this](Variant const &v) { m_c->set_cut_off(get_value(v)); }, - [this]() { return m_c->get_cut_off(); }}}); - } - - std::shared_ptr<::PairCriteria::PairCriterion> - pair_criterion() const override { - return m_c; - } - -private: - std::shared_ptr<::PairCriteria::EnergyCriterion> m_c; -}; - -class BondCriterion : public PairCriterion { -public: - BondCriterion() : m_c(std::make_shared<::PairCriteria::BondCriterion>()) { - add_parameters( - {{"bond_type", - [this](Variant const &v) { m_c->set_bond_type(get_value(v)); }, - [this]() { return m_c->get_bond_type(); }}}); - } - - std::shared_ptr<::PairCriteria::PairCriterion> - pair_criterion() const override { - return m_c; - } - -private: - std::shared_ptr<::PairCriteria::BondCriterion> m_c; -}; - -} /* namespace PairCriteria */ -} /* namespace ScriptInterface */ - -#endif diff --git a/src/script_interface/scafacos/CMakeLists.txt b/src/script_interface/scafacos/CMakeLists.txt new file mode 100644 index 00000000000..68a6b27a036 --- /dev/null +++ b/src/script_interface/scafacos/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(ScriptInterface PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/scafacos.cpp) diff --git a/src/script_interface/scafacos/scafacos.cpp b/src/script_interface/scafacos/scafacos.cpp new file mode 100644 index 00000000000..130dcd29f72 --- /dev/null +++ b/src/script_interface/scafacos/scafacos.cpp @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.hpp" + +#if defined(SCAFACOS) or defined(SCAFACOS_DIPOLES) + +#include "script_interface/Variant.hpp" +#include "script_interface/get_value.hpp" + +#include "scafacos.hpp" + +#include "core/scafacos/ScafacosContextBase.hpp" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ScriptInterface { +namespace Scafacos { + +std::vector available_methods() { + return ScafacosContextBase::available_methods(); +} + +struct ConvertToStringVector + : public boost::static_visitor> { + auto operator()(std::string const &value) const { + return std::vector{value}; + } + + template ::value>> + std::vector operator()(T const &value) const { + throw std::runtime_error("Cannot convert " + Utils::demangle()); + } + + template ::value>> + auto operator()(T const &value) const { + return operator()(to_str(value)); + } + + auto operator()(double const &value) const { + return operator()(to_str(value)); + } + + auto operator()(std::vector const &values) const { + return values; + } + + auto operator()(std::vector const &values) const { + std::vector values_str; + for (auto const &v : values) { + values_str.emplace_back(boost::apply_visitor(*this, v).front()); + } + return values_str; + } + + template ::value>> + auto operator()(std::vector const &values) const { + std::vector values_str; + for (auto const &v : values) { + values_str.emplace_back(to_str(v)); + } + return values_str; + } + +private: + std::string to_str(int const value) const { return std::to_string(value); } + + std::string to_str(double const value) const { + std::ostringstream serializer; + serializer << std::scientific << std::setprecision(17); + serializer << value; + return serializer.str(); + } +}; + +struct GetParameterList + : public boost::static_visitor> { + auto operator()(std::unordered_map const &obj) const { + return obj; + } + + template + auto operator()(std::unordered_map const &obj) const { + // handle the case of the empty dict, which can have any key type + return (obj.empty()) ? result_type{} : invalid(obj); + } + + template auto operator()(T const &obj) const { + return invalid(obj); + } + +private: + template auto invalid(T const &obj) const { + return get_value(obj); + } +}; + +std::string serialize_parameters(Variant const &pack) { + auto const parameters = boost::apply_visitor(GetParameterList{}, pack); + if (parameters.empty()) { + throw std::invalid_argument( + "ScaFaCoS methods require at least 1 parameter"); + } + auto const visitor = ConvertToStringVector{}; + std::string method_params = ""; + for (auto const &kv : parameters) { + method_params += "," + kv.first; + for (auto const &value : boost::apply_visitor(visitor, kv.second)) { + method_params += "," + value; + } + } + return method_params.substr(1); +} + +template +boost::optional string_to_number(std::string const &s) { + auto deserializer = std::istringstream(s); + T result; + deserializer >> result; + if (deserializer.fail() or not deserializer.eof()) { + return {}; + } + return Variant{result}; +} + +std::unordered_map +deserialize_parameters(std::string const ¶meters) { + /* + * ScaFaCoS parameters are serialized to a comma-separated string. + * Key-value pairs can be split with a look ahead: when the next + * item is a string, it is a parameter name and the current list + * of arithmetic values belong to the current parameter name. + * The only exception is string-valued parameters; in that case + * the current list of arithmetic values is empty. + */ + auto const numbers = std::string("-0123456789"); + std::unordered_map method_params{}; + std::vector flat_array; + // Clang 10 false positive: https://github.com/boostorg/algorithm/issues/63 + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + boost::split(flat_array, parameters, boost::is_any_of(",")); + for (auto it = flat_array.begin(); it != flat_array.end();) { + auto const parameter_name = *it; + auto parameter_list = std::vector{}; + for (++it; it != flat_array.end(); ++it) { + if ((numbers.find(it->front()) == std::string::npos) and + not parameter_list.empty()) { + break; + } + auto result = Variant{*it}; + if (auto converted = string_to_number(*it)) { + result = Variant{*converted}; + } else if (auto converted = string_to_number(*it)) { + result = Variant{*converted}; + } + parameter_list.emplace_back(result); + } + assert(not parameter_list.empty()); + if (parameter_list.size() == 1ul) { + method_params[parameter_name] = parameter_list.front(); + } else { + method_params[parameter_name] = Variant{std::move(parameter_list)}; + } + } + return method_params; +} + +} // namespace Scafacos +} // namespace ScriptInterface + +#endif // SCAFACOS or SCAFACOS_DIPOLES diff --git a/src/script_interface/scafacos/scafacos.hpp b/src/script_interface/scafacos/scafacos.hpp new file mode 100644 index 00000000000..6f8f2d69282 --- /dev/null +++ b/src/script_interface/scafacos/scafacos.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ESPRESSO_SRC_SCRIPT_INTERFACE_SCAFACOS_SCAFACOS_HPP +#define ESPRESSO_SRC_SCRIPT_INTERFACE_SCAFACOS_SCAFACOS_HPP + +#include "config.hpp" + +#if defined(SCAFACOS) or defined(SCAFACOS_DIPOLAR) + +#include "script_interface/Variant.hpp" + +#include +#include +#include + +namespace ScriptInterface { +namespace Scafacos { + +/** @brief Fetch list of methods compiled in ScaFaCoS. */ +std::vector available_methods(); + +/** @brief Flatten a parameter map. */ +std::string serialize_parameters(Variant const &pack); + +/** @brief Convert flattened parameters to a map. */ +std::unordered_map +deserialize_parameters(std::string const ¶meters); + +} // namespace Scafacos +} // namespace ScriptInterface + +#endif // SCAFACOS or SCAFACOS_DIPOLAR +#endif diff --git a/src/script_interface/tests/Actors_test.cpp b/src/script_interface/tests/Actors_test.cpp new file mode 100644 index 00000000000..9a4bd19e1c3 --- /dev/null +++ b/src/script_interface/tests/Actors_test.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define BOOST_TEST_MODULE "Long-range actors test" +#define BOOST_TEST_DYN_LINK + +#include "config.hpp" + +#if defined(ELECTROSTATICS) or defined(DIPOLES) or defined(SCAFACOS) or \ + defined(SCAFACOS_DIPOLES) + +#include + +#include "script_interface/electrostatics/Actor.hpp" +#include "script_interface/electrostatics/Actor_impl.hpp" +#include "script_interface/magnetostatics/Actor.hpp" +#include "script_interface/magnetostatics/Actor_impl.hpp" +#include "script_interface/scafacos/scafacos.hpp" + +#include "core/actor/visitors.hpp" +#include "core/electrostatics/coulomb.hpp" +#include "core/electrostatics/debye_hueckel.hpp" +#include "core/electrostatics/registration.hpp" +#include "core/magnetostatics/dds.hpp" +#include "core/magnetostatics/dipoles.hpp" +#include "core/magnetostatics/registration.hpp" + +#include "core/communication.hpp" + +#include "script_interface/Variant.hpp" +#include "script_interface/get_value.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +namespace ScriptInterface { +namespace Coulomb { + +#ifdef ELECTROSTATICS +struct MockDebyeHueckel : public Actor { + MockDebyeHueckel() = default; + + void do_construct(VariantMap const ¶ms) override { + m_actor = std::make_shared( + get_value(params, "prefactor"), + get_value(params, "kappa"), get_value(params, "r_cut")); + } +}; +#endif // ELECTROSTATICS + +} // namespace Coulomb + +namespace Dipoles { + +#ifdef DIPOLES +struct MockDipolarDirectSum + : public Actor { + MockDipolarDirectSum() = default; + + void do_construct(VariantMap const ¶ms) override { + m_actor = std::make_shared( + get_value(params, "prefactor")); + } +}; +#endif // DIPOLES + +} // namespace Dipoles +} // namespace ScriptInterface + +#ifdef ELECTROSTATICS +BOOST_AUTO_TEST_CASE(coulomb_actor) { + auto constexpr tol = 100. * std::numeric_limits::epsilon(); + ScriptInterface::Coulomb::MockDebyeHueckel actor; + actor.do_construct({{"prefactor", 2.}, {"kappa", 3.}, {"r_cut", 4.}}); + // check const and non-const access + BOOST_CHECK_CLOSE(actor.actor()->prefactor, 2., tol); + BOOST_CHECK_CLOSE(Utils::as_const(actor).actor()->prefactor, 2., tol); + // check visitors + BOOST_CHECK(has_actor_of_type<::DebyeHueckel>( + boost::optional(actor.actor()))); + BOOST_CHECK(not has_actor_of_type( + boost::optional(actor.actor()))); + BOOST_CHECK(is_already_stored( + actor.actor(), boost::optional(actor.actor()))); + BOOST_CHECK(not is_already_stored( + std::shared_ptr<::DebyeHueckel>{}, + boost::optional(actor.actor()))); + BOOST_CHECK(not is_already_stored( + std::shared_ptr{}, + boost::optional(actor.actor()))); +} +#endif // ELECTROSTATICS + +#ifdef DIPOLES +BOOST_AUTO_TEST_CASE(dipoles_actor) { + auto constexpr tol = 100. * std::numeric_limits::epsilon(); + n_nodes = 1; + ScriptInterface::Dipoles::MockDipolarDirectSum actor; + actor.do_construct({{"prefactor", 2.}}); + // check const and non-const access + BOOST_CHECK_CLOSE(actor.actor()->prefactor, 2., tol); + BOOST_CHECK_CLOSE(Utils::as_const(actor).actor()->prefactor, 2., tol); +} +#endif // DIPOLES + +#if defined(SCAFACOS) or defined(SCAFACOS_DIPOLES) + +BOOST_AUTO_TEST_CASE(scafacos_parameters_serialization) { + using ScriptInterface::Variant; + { + auto const vec_int = std::vector{1, 2, 3}; + auto const vec_var = std::vector{4, 5, std::string("ab")}; + auto const parameters = ScriptInterface::Scafacos::serialize_parameters( + Variant{std::unordered_map{ + {"key", Variant{2}}, + {"vec_int", Variant{vec_int}}, + {"vec_var", Variant{vec_var}}}}); + auto const components = + std::vector{"key,2", "vec_int,1,2,3", "vec_var,4,5,ab"}; + for (auto const &item : components) { + BOOST_CHECK(parameters.find(item) != item.npos); + } + } +} + +BOOST_AUTO_TEST_CASE(scafacos_parameters_serialization_exceptions) { + using ScriptInterface::Variant; + { + auto caught = false; + try { + ScriptInterface::Scafacos::serialize_parameters(Variant{5.}); + } catch (ScriptInterface::Exception const &) { + caught = true; + } + BOOST_CHECK(caught); + } + { + auto caught = false; + try { + auto const empty_map = std::unordered_map{}; + ScriptInterface::Scafacos::serialize_parameters(Variant{empty_map}); + } catch (std::invalid_argument const &) { + caught = true; + } + BOOST_CHECK(caught); + } + { + auto caught = false; + try { + auto const invalid_map = std::unordered_map{ + {"k", Variant{std::unordered_map{}}}}; + ScriptInterface::Scafacos::serialize_parameters(Variant{invalid_map}); + } catch (std::runtime_error const &) { + caught = true; + } + BOOST_CHECK(caught); + } +} +#endif // SCAFACOS or SCAFACOS_DIPOLES + +#else +int main(int argc, char **argv) {} +#endif // ELECTROSTATICS or DIPOLES or SCAFACOS or SCAFACOS_DIPOLES diff --git a/src/script_interface/tests/CMakeLists.txt b/src/script_interface/tests/CMakeLists.txt index 6c619ca271a..1f11e6c2393 100644 --- a/src/script_interface/tests/CMakeLists.txt +++ b/src/script_interface/tests/CMakeLists.txt @@ -47,3 +47,4 @@ unit_test(NAME Accumulators_test SRC Accumulators_test.cpp DEPENDS ScriptInterface) unit_test(NAME Constraints_test SRC Constraints_test.cpp DEPENDS ScriptInterface) +unit_test(NAME Actors_test SRC Actors_test.cpp DEPENDS ScriptInterface) diff --git a/testsuite/python/CMakeLists.txt b/testsuite/python/CMakeLists.txt index 042955f5347..f319a756545 100644 --- a/testsuite/python/CMakeLists.txt +++ b/testsuite/python/CMakeLists.txt @@ -96,7 +96,7 @@ checkpoint_test(MODES therm_lb__p3m_cpu__lj__lb_cpu_ascii SUFFIX 1_core MAX_NUM_PROC 1) checkpoint_test(MODES therm_lb__p3m_cpu__lj__lb_cpu_ascii) checkpoint_test(MODES therm_lb__elc_cpu__lj__lb_cpu_binary) -checkpoint_test(MODES therm_lb__elc_cpu__lj__lb_gpu_ascii LABELS gpu) +checkpoint_test(MODES therm_lb__elc_gpu__lj__lb_gpu_ascii LABELS gpu) checkpoint_test(MODES therm_lb__p3m_gpu__lj__lb_gpu_binary LABELS gpu) checkpoint_test(MODES therm_npt__int_npt) checkpoint_test(MODES int_sd__lj) @@ -122,11 +122,12 @@ python_test(FILE accumulator_time_series.py MAX_NUM_PROC 1) python_test(FILE dawaanr-and-dds-gpu.py MAX_NUM_PROC 1 LABELS gpu) python_test(FILE dawaanr-and-bh-gpu.py MAX_NUM_PROC 1 LABELS gpu) python_test(FILE dds-and-bh-gpu.py MAX_NUM_PROC 4 LABELS gpu) -python_test(FILE electrostaticInteractions.py MAX_NUM_PROC 2) +python_test(FILE electrostatic_interactions.py MAX_NUM_PROC 2) python_test(FILE engine_langevin.py MAX_NUM_PROC 4) python_test(FILE engine_lb.py MAX_NUM_PROC 2 LABELS gpu) python_test(FILE engine_lb.py MAX_NUM_PROC 1 LABELS gpu SUFFIX n_square) python_test(FILE icc.py MAX_NUM_PROC 4) +python_test(FILE icc_interface.py MAX_NUM_PROC 1 LABELS gpu) python_test(FILE mass-and-rinertia_per_particle.py MAX_NUM_PROC 2 LABELS long) python_test(FILE integrate.py MAX_NUM_PROC 4) python_test(FILE interactions_bond_angle.py MAX_NUM_PROC 4) @@ -137,11 +138,12 @@ python_test(FILE interactions_dihedral.py MAX_NUM_PROC 4) python_test(FILE interactions_non-bonded_interface.py MAX_NUM_PROC 4) python_test(FILE interactions_non-bonded.py MAX_NUM_PROC 4) python_test(FILE observables.py MAX_NUM_PROC 4) -python_test(FILE p3m_gpu.py MAX_NUM_PROC 4 LABELS gpu) python_test(FILE particle.py MAX_NUM_PROC 4) python_test(FILE pressure.py MAX_NUM_PROC 4) python_test(FILE scafacos_dipoles_1d_2d.py MAX_NUM_PROC 4) python_test(FILE scafacos_interface.py MAX_NUM_PROC 2) +python_test(FILE long_range_actors.py MAX_NUM_PROC 4 LABELS gpu) +python_test(FILE long_range_actors.py MAX_NUM_PROC 1 LABELS gpu SUFFIX 1_core) python_test(FILE tabulated.py MAX_NUM_PROC 2) python_test(FILE particle_slice.py MAX_NUM_PROC 4) python_test(FILE rigid_bond.py MAX_NUM_PROC 4) @@ -188,8 +190,11 @@ python_test(FILE ibm.py MAX_NUM_PROC 2) python_test(FILE dipolar_mdlc_p3m_scafacos_p2nfft.py MAX_NUM_PROC 1) python_test(FILE dipolar_direct_summation.py MAX_NUM_PROC 1 LABELS gpu) python_test(FILE dipolar_p3m.py MAX_NUM_PROC 2) -python_test(FILE dipolar_interface.py MAX_NUM_PROC 1 LABELS gpu) -python_test(FILE dipolar_mpi_exceptions.py MAX_NUM_PROC 2) +python_test(FILE dipolar_interface.py MAX_NUM_PROC 1 LABELS gpu SUFFIX + non_p3m_methods) +python_test(FILE dipolar_interface.py MAX_NUM_PROC 2 LABELS gpu SUFFIX + p3m_methods) +python_test(FILE coulomb_interface.py MAX_NUM_PROC 2 LABELS gpu) python_test(FILE lb.py MAX_NUM_PROC 2 LABELS gpu) python_test(FILE lb_stats.py MAX_NUM_PROC 2 LABELS gpu long) python_test(FILE lb_stats.py MAX_NUM_PROC 1 LABELS gpu long SUFFIX n_square) @@ -198,7 +203,7 @@ python_test(FILE force_cap.py MAX_NUM_PROC 2) python_test(FILE dpd.py MAX_NUM_PROC 4) python_test(FILE dpd_stats.py MAX_NUM_PROC 4 LABELS long) python_test(FILE hat.py MAX_NUM_PROC 4) -python_test(FILE analyze_energy.py MAX_NUM_PROC 2) +python_test(FILE analyze_energy.py MAX_NUM_PROC 2 LABELS gpu) python_test(FILE analyze_mass_related.py MAX_NUM_PROC 4) python_test(FILE rdf.py MAX_NUM_PROC 1) python_test(FILE sf_simple_lattice.py MAX_NUM_PROC 1) @@ -254,7 +259,7 @@ python_test(FILE lb_buoyancy_force.py MAX_NUM_PROC 4 LABELS gpu) python_test(FILE lb_momentum_conservation.py MAX_NUM_PROC 4 LABELS gpu) python_test(FILE lb_momentum_conservation.py MAX_NUM_PROC 1 LABELS gpu SUFFIX n_square) -python_test(FILE p3m_electrostatic_pressure.py MAX_NUM_PROC 2) +python_test(FILE p3m_electrostatic_pressure.py MAX_NUM_PROC 2 LABELS gpu) python_test(FILE sigint.py DEPENDENCIES sigint_child.py MAX_NUM_PROC 1) python_test(FILE lb_density.py MAX_NUM_PROC 1) python_test(FILE observable_chain.py MAX_NUM_PROC 4) @@ -266,12 +271,11 @@ python_test(FILE decorators.py MAX_NUM_PROC 1) python_test(FILE galilei.py MAX_NUM_PROC 4) python_test(FILE linear_momentum.py MAX_NUM_PROC 4) python_test(FILE linear_momentum_lb.py MAX_NUM_PROC 2 LABELS gpu) -python_test(FILE mmm1d.py MAX_NUM_PROC 2) -python_test(FILE mmm1d_gpu.py MAX_NUM_PROC 1 LABELS gpu) +python_test(FILE mmm1d.py MAX_NUM_PROC 2 LABELS gpu) python_test(FILE stokesian_dynamics.py MAX_NUM_PROC 2 LABELS long) python_test(FILE stokesian_thermostat.py MAX_NUM_PROC 2) -python_test(FILE elc.py MAX_NUM_PROC 2) -python_test(FILE elc_vs_analytic.py MAX_NUM_PROC 2) +python_test(FILE elc.py MAX_NUM_PROC 2 LABELS gpu) +python_test(FILE elc_vs_analytic.py MAX_NUM_PROC 2 LABELS gpu) python_test(FILE rotation.py MAX_NUM_PROC 1) python_test(FILE shapes.py MAX_NUM_PROC 1) python_test(FILE h5md.py MAX_NUM_PROC 2) diff --git a/testsuite/python/actor.py b/testsuite/python/actor.py index 897bf1c7ba0..e05effff9ac 100644 --- a/testsuite/python/actor.py +++ b/testsuite/python/actor.py @@ -23,11 +23,12 @@ """ import unittest as ut +import espressomd.lb import espressomd.actors import espressomd.highlander -class TestActor(espressomd.actors.Actor): +class TestActor(espressomd.lb.FluidActor): def __init__(self, *args, **kwargs): self._core_args = None diff --git a/testsuite/python/analyze_energy.py b/testsuite/python/analyze_energy.py index 4fb1aa9e474..14b672260cd 100644 --- a/testsuite/python/analyze_energy.py +++ b/testsuite/python/analyze_energy.py @@ -51,6 +51,7 @@ def setUp(self): def tearDown(self): self.system.part.clear() + self.system.actors.clear() def test_kinetic(self): p0, p1 = self.system.part.all() @@ -163,20 +164,19 @@ def test_all(self): self.assertAlmostEqual(energy["non_bonded"], 0.5 * sum( [self.system.analysis.particle_energy(p) for p in self.system.part.all()]), delta=1e-7) - @utx.skipIfMissingFeatures(["ELECTROSTATICS", "P3M"]) - def test_electrostatics(self): + def check_electrostatics(self, p3m_class): p0, p1 = self.system.part.all() p0.pos = [1, 2, 2] p1.pos = [3, 2, 2] p0.q = 1 p1.q = -1 - p3m = espressomd.electrostatics.P3M( + p3m = p3m_class( prefactor=1.0, - accuracy=9.910945054074526e-08, + accuracy=1e-7, mesh=[22, 22, 22], cao=7, - r_cut=8.906249999999998, - alpha=0.387611049779351, + r_cut=8.90625, + alpha=0.38761105, tune=False) self.system.actors.add(p3m) @@ -190,6 +190,15 @@ def test_electrostatics(self): self.assertAlmostEqual(energy["non_bonded"], 0, delta=1e-7) self.assertAlmostEqual(energy["coulomb"], u_p3m, delta=1e-5) + @utx.skipIfMissingFeatures(["P3M"]) + def test_electrostatics_cpu(self): + self.check_electrostatics(espressomd.electrostatics.P3M) + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["P3M"]) + def test_electrostatics_gpu(self): + self.check_electrostatics(espressomd.electrostatics.P3MGPU) + if __name__ == "__main__": ut.main() diff --git a/testsuite/python/coulomb_cloud_wall.py b/testsuite/python/coulomb_cloud_wall.py index 26d3f048c8a..34e834b9180 100644 --- a/testsuite/python/coulomb_cloud_wall.py +++ b/testsuite/python/coulomb_cloud_wall.py @@ -21,93 +21,77 @@ import numpy as np import espressomd -import espressomd.cuda_init import espressomd.electrostatics -import espressomd.scafacos import tests_common @utx.skipIfMissingFeatures(["ELECTROSTATICS"]) class CoulombCloudWall(ut.TestCase): - """This compares p3m, p3m_gpu, scafacos_p3m and scafacos_p2nfft - electrostatic forces and energy against stored data. + """ + Compare P3M CPU, P3M GPU and ScaFaCoS P2NFFT electrostatic forces + and energy against stored data. """ - system = espressomd.System(box_l=[1.0, 1.0, 1.0]) + system = espressomd.System(box_l=[10., 10., 10.]) data = np.genfromtxt(tests_common.data_path( "coulomb_cloud_wall_system.data")) tolerance = 1E-3 + p3m_params = {'r_cut': 1.001, 'accuracy': 1e-3, + 'mesh': [64, 64, 64], 'cao': 7, 'alpha': 2.70746} - # Reference energy from p3m in the tcl test case + # Reference energy from P3M reference_energy = 148.94229549 def setUp(self): - self.system.box_l = (10, 10, 10) self.system.time_step = 0.01 self.system.cell_system.skin = 0.4 # Add particles to system and store reference forces in hash # Input format: id pos q f self.system.part.add(pos=self.data[:, 1:4], q=self.data[:, 4]) - self.forces = self.data[:, 5:8] + self.reference_forces = self.data[:, 5:8] def tearDown(self): self.system.part.clear() self.system.actors.clear() - def compare(self, method_name, energy=True, prefactor=None): - # Compare forces and energy now in the system to stored ones + def compare(self, method_name, prefactor, force_tol, energy_tol): + # Compare forces and energy now in the system to reference data + err_msg = f"difference too large for method {method_name}" # Force - force_diff = np.linalg.norm(self.system.part.all().f / prefactor - self.forces, - axis=1) - self.assertLess( - np.mean(force_diff), self.tolerance, - msg="Absolute force difference too large for method " + method_name) + np.testing.assert_allclose( + np.copy(self.system.part.all().f) / prefactor, + self.reference_forces, atol=force_tol, err_msg=f"Force {err_msg}") # Energy - if energy: - self.assertAlmostEqual( - self.system.analysis.energy()["total"] / prefactor, - self.reference_energy, delta=self.tolerance, - msg="Absolute energy difference too large for " + method_name) - - # Tests for individual methods + self.assertAlmostEqual( + self.system.analysis.energy()["total"] / prefactor, + self.reference_energy, delta=energy_tol, + msg=f"Energy {err_msg}") @utx.skipIfMissingFeatures(["P3M"]) - def test_p3m_direct(self): - """ - This checks P3M. - - """ - + def test_p3m_cpu(self): self.system.actors.add( espressomd.electrostatics.P3M( - prefactor=3, r_cut=1.001, accuracy=1e-3, - mesh=64, cao=7, alpha=2.70746, tune=False)) + **self.p3m_params, prefactor=3., tune=False)) self.system.integrator.run(0) - self.compare("p3m", energy=True, prefactor=3) + self.compare("p3m", prefactor=3., force_tol=2e-3, energy_tol=1e-3) @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["P3M"]) def test_p3m_gpu(self): self.system.actors.add( espressomd.electrostatics.P3MGPU( - prefactor=2.2, - r_cut=1.001, - accuracy=1e-3, - mesh=64, - cao=7, - alpha=2.70746, - tune=False)) + **self.p3m_params, prefactor=2.2, tune=False)) self.system.integrator.run(0) - self.compare("p3m_gpu", energy=False, prefactor=2.2) + self.compare("p3m_gpu", prefactor=2.2, force_tol=2e-3, energy_tol=1e-3) - @ut.skipIf(not espressomd.has_features("SCAFACOS") - or 'p2nfft' not in espressomd.scafacos.available_methods(), - 'Skipping test: missing feature SCAFACOS or p2nfft method') + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("p2nfft") def test_scafacos_p2nfft(self): self.system.actors.add( espressomd.electrostatics.Scafacos( @@ -115,7 +99,11 @@ def test_scafacos_p2nfft(self): method_name="p2nfft", method_params={"p2nfft_r_cut": 1.001, "tolerance_field": 1E-4})) self.system.integrator.run(0) - self.compare("scafacos_p2nfft", energy=True, prefactor=2.8) + self.compare( + "scafacos_p2nfft", + prefactor=2.8, + force_tol=1e-3, + energy_tol=1e-3) def test_zz_deactivation(self): # Is the energy and force 0, if no methods active diff --git a/testsuite/python/coulomb_cloud_wall_duplicated.py b/testsuite/python/coulomb_cloud_wall_duplicated.py index 492f170dd91..2eca84b3ecc 100644 --- a/testsuite/python/coulomb_cloud_wall_duplicated.py +++ b/testsuite/python/coulomb_cloud_wall_duplicated.py @@ -38,6 +38,9 @@ class CoulombCloudWall(ut.TestCase): tests_common.data_path("coulomb_cloud_wall_duplicated_system.data")) tolerance = 1E-3 + p3m_params = {'prefactor': 1., 'r_cut': 1.001, 'accuracy': 1e-3, + 'mesh': [64, 64, 128], 'mesh_off': [0.5, 0.5, 0.5], + 'cao': 7, 'alpha': 2.70746} # Reference energy from p3m in the tcl test case reference_energy = 2. * 148.94229549 @@ -74,23 +77,14 @@ def compare(self, method_name, energy=True): @utx.skipIfMissingFeatures("P3M") def test_p3m(self): self.system.actors.add( - espressomd.electrostatics.P3M( - prefactor=1, r_cut=1.001, accuracy=1e-3, - mesh=[64, 64, 128], cao=7, alpha=2.70746, tune=False)) + espressomd.electrostatics.P3M(**self.p3m_params, tune=False)) self.system.integrator.run(0) self.compare("p3m", energy=True) @utx.skipIfMissingGPU() def test_p3m_gpu(self): self.system.actors.add( - espressomd.electrostatics.P3MGPU( - prefactor=1, - r_cut=1.001, - accuracy=1e-3, - mesh=[64, 64, 128], - cao=7, - alpha=2.70746, - tune=False)) + espressomd.electrostatics.P3MGPU(**self.p3m_params, tune=False)) self.system.integrator.run(0) self.compare("p3m_gpu", energy=False) diff --git a/testsuite/python/coulomb_interface.py b/testsuite/python/coulomb_interface.py new file mode 100644 index 00000000000..9dfa043c63b --- /dev/null +++ b/testsuite/python/coulomb_interface.py @@ -0,0 +1,251 @@ +# +# Copyright (C) 2013-2022 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import unittest as ut +import unittest_decorators as utx +import tests_common + +import espressomd.electrostatics + + +@utx.skipIfMissingFeatures(["ELECTROSTATICS"]) +class Test(ut.TestCase): + system = espressomd.System(box_l=[10., 10., 10.]) + original_node_grid = system.cell_system.node_grid + + def setUp(self): + self.system.box_l = [10., 10., 10.] + self.system.periodicity = [True, True, True] + self.system.cell_system.set_regular_decomposition() + self.system.cell_system.node_grid = self.original_node_grid + self.system.time_step = 0.01 + self.system.part.add(pos=(0.0, 0.0, 0.0), q=+1.) + self.system.part.add(pos=(0.1, 0.1, 0.1), q=-1.) + + def tearDown(self): + self.system.part.clear() + self.system.actors.clear() + + if espressomd.has_features(["ELECTROSTATICS"]): + test_dh = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.DH, + dict(prefactor=2., kappa=3., r_cut=1.5, + check_neutrality=True, charge_neutrality_tolerance=7e-12)) + + if espressomd.has_features(["ELECTROSTATICS"]): + test_rf = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.ReactionField, + dict(prefactor=2., kappa=3., epsilon1=4., epsilon2=5., r_cut=1.5, + check_neutrality=True, charge_neutrality_tolerance=7e-12)) + + if espressomd.has_features(["P3M"]): + test_p3m_cpu_metallic = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.P3M, + dict(prefactor=2., epsilon=0., mesh_off=[0.6, 0.7, 0.8], r_cut=1.5, + cao=2, mesh=[8, 10, 8], alpha=12., accuracy=0.01, tune=False, + check_neutrality=True, charge_neutrality_tolerance=7e-12)) + test_p3m_cpu_non_metallic = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.P3M, + dict(prefactor=2., epsilon=3., mesh_off=[0.6, 0.7, 0.8], r_cut=1.5, + cao=2, mesh=[8, 8, 8], alpha=12., accuracy=0.01, tune=False, + check_neutrality=True, charge_neutrality_tolerance=7e-12)) + test_p3m_cpu_elc = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.ELC, + dict(gap_size=2., maxPWerror=1e-3, const_pot=True, pot_diff=-3., + delta_mid_top=0.5, delta_mid_bot=0.5, check_neutrality=False, + actor=espressomd.electrostatics.P3M( + prefactor=2., r_cut=1.5, cao=2, mesh=[8, 8, 8], + alpha=12., accuracy=0.01, tune=False))) + + if espressomd.has_features(["P3M", "CUDA"]) and espressomd.gpu_available(): + test_p3m_gpu_metallic = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.P3MGPU, + dict(prefactor=2., epsilon=0., mesh_off=[0.6, 0.7, 0.8], r_cut=1.5, + cao=2, mesh=[8, 10, 8], alpha=12., accuracy=0.01, tune=False, + check_neutrality=True, charge_neutrality_tolerance=7e-12)) + test_p3m_gpu_non_metallic = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.P3MGPU, + dict(prefactor=2., epsilon=3., mesh_off=[0.6, 0.7, 0.8], r_cut=1.5, + cao=2, mesh=[8, 8, 8], alpha=12., accuracy=0.01, tune=False, + check_neutrality=True, charge_neutrality_tolerance=7e-12)) + test_p3m_gpu_elc = tests_common.generate_test_for_actor_class( + system, espressomd.electrostatics.ELC, + dict(gap_size=2., maxPWerror=1e-3, const_pot=True, pot_diff=-3., + delta_mid_top=0.5, delta_mid_bot=0.5, check_neutrality=False, + actor=espressomd.electrostatics.P3MGPU( + prefactor=2., r_cut=1.5, cao=2, mesh=[8, 8, 8], + alpha=12., accuracy=0.01, tune=False))) + + def test_mmm1d_cpu(self): + self.system.periodicity = [False, False, True] + self.system.cell_system.set_n_square() + tests_common.generate_test_for_actor_class( + self.system, espressomd.electrostatics.MMM1D, + dict(prefactor=1.0, maxPWerror=1e-3, far_switch_radius=1., + check_neutrality=True, charge_neutrality_tolerance=7e-12, + timings=5, verbose=False))(self) + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["CUDA", "MMM1D_GPU"]) + def test_mmm1d_gpu(self): + self.system.periodicity = [False, False, True] + self.system.cell_system.set_n_square() + tests_common.generate_test_for_actor_class( + self.system, espressomd.electrostatics.MMM1DGPU, + dict(prefactor=1., maxPWerror=1e-3, far_switch_radius=1., + check_neutrality=True, charge_neutrality_tolerance=7e-12, + bessel_cutoff=1))(self) + + def test_charge_neutrality_check(self): + self.system.part.add(pos=(0.0, 0.0, 0.0), q=1.) + self.system.periodicity = [False, False, True] + self.system.cell_system.set_n_square() + actor = espressomd.electrostatics.MMM1D(prefactor=1.0, maxPWerror=1e-3) + with self.assertRaisesRegex(RuntimeError, "The system is not charge neutral"): + self.system.actors.add(actor) + self.assertEqual(len(self.system.actors), 0) + self.assertFalse(actor.is_tuned) + self.assertTrue(actor.check_neutrality) + self.assertAlmostEqual(actor.charge_neutrality_tolerance, 2e-12) + with self.assertRaisesRegex(ValueError, "Parameter 'charge_neutrality_tolerance' must be >= 0"): + actor.charge_neutrality_tolerance = -1. + actor.charge_neutrality_tolerance = None + self.assertIsNone(actor.charge_neutrality_tolerance) + self.assertFalse(actor.check_neutrality) + actor.check_neutrality = True + self.assertTrue(actor.check_neutrality) + self.assertAlmostEqual(actor.charge_neutrality_tolerance, 2e-12) + actor.check_neutrality = False + self.assertFalse(actor.check_neutrality) + self.assertIsNone(actor.charge_neutrality_tolerance) + + def test_mmm1d_cpu_tuning_exceptions(self): + self.system.periodicity = [False, False, True] + self.system.cell_system.set_n_square() + actor = espressomd.electrostatics.MMM1D( + prefactor=1., maxPWerror=1e-3, far_switch_radius=0.1) + with self.assertRaisesRegex(RuntimeError, "MMM1D could not find a reasonable Bessel cutoff"): + self.system.actors.add(actor) + self.assertEqual(len(self.system.actors), 0) + self.assertFalse(actor.is_tuned) + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["CUDA", "MMM1D_GPU"]) + def test_mmm1d_gpu_tuning_exceptions(self): + self.system.periodicity = [False, False, True] + self.system.cell_system.set_n_square() + actor = espressomd.electrostatics.MMM1DGPU( + prefactor=1., maxPWerror=1e-3, far_switch_radius=0.1) + with self.assertRaisesRegex(RuntimeError, "No reasonable Bessel cutoff could be determined"): + self.system.actors.add(actor) + self.assertEqual(len(self.system.actors), 0) + self.assertFalse(actor.is_tuned) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_elc_p3m_exceptions(self): + P3M = espressomd.electrostatics.P3M + ELC = espressomd.electrostatics.ELC + # create valid actors + dh = espressomd.electrostatics.DH(prefactor=1.2, kappa=0.8, r_cut=2.0) + p3m_params = dict(prefactor=1., epsilon=0.1, accuracy=1e-2, + mesh=[16, 16, 16], cao=7, r_cut=1.5, alpha=0.9) + p3m = P3M(**p3m_params) + elc = ELC(gap_size=2., maxPWerror=1., actor=p3m) + + # check runtime errors and input parameters + with self.assertRaisesRegex(ValueError, "Parameter 'prefactor' must be > 0"): + P3M(**{**p3m_params, 'prefactor': -2.}) + with self.assertRaisesRegex(ValueError, "P3M mesh has to be an integer or integer list of length 3"): + P3M(**{**p3m_params, 'mesh': [8, 8]}) + with self.assertRaisesRegex(ValueError, "Parameter 'actor' of type Coulomb::ElectrostaticLayerCorrection isn't supported by ELC"): + ELC(gap_size=2., maxPWerror=1., actor=elc) + with self.assertRaisesRegex(ValueError, "Parameter 'actor' of type Coulomb::DebyeHueckel isn't supported by ELC"): + ELC(gap_size=2., maxPWerror=1., actor=dh) + with self.assertRaisesRegex(RuntimeError, "Parameter 'actor' is missing"): + ELC(gap_size=2., maxPWerror=1.) + with self.assertRaisesRegex(ValueError, "Parameter 'const_pot' must be True when 'pot_diff' is non-zero"): + ELC(gap_size=2., maxPWerror=1., actor=p3m, + const_pot=False, pot_diff=1.) + with self.assertRaisesRegex(ValueError, "ELC with two parallel metallic boundaries requires the const_pot option"): + ELC(gap_size=2., maxPWerror=1., actor=p3m, const_pot=False, + delta_mid_top=-1., delta_mid_bot=-1.) + # check contrasts are clamped if deviation from -1 is small + epsilon = 1e-6 + with self.assertRaisesRegex(ValueError, r"Parameter 'delta_mid_top' must be >= -1 and <= \+1"): + ELC(gap_size=2., maxPWerror=1., actor=p3m, + delta_mid_top=-1. - epsilon, delta_mid_bot=-1.) + with self.assertRaisesRegex(ValueError, r"Parameter 'delta_mid_bot' must be >= -1 and <= \+1"): + ELC(gap_size=2., maxPWerror=1., actor=p3m, + delta_mid_top=-1., delta_mid_bot=-1. - epsilon) + epsilon = 1e-8 + elc = ELC(gap_size=2., maxPWerror=1., actor=p3m, const_pot=True, + delta_mid_top=-1. - epsilon, delta_mid_bot=1. + epsilon) + self.assertAlmostEqual(elc.delta_mid_top, -1., delta=epsilon / 100.) + self.assertAlmostEqual(elc.delta_mid_bot, +1., delta=epsilon / 100.) + + # run sanity checks + elc = espressomd.electrostatics.ELC( + gap_size=4., maxPWerror=1., actor=p3m) + self.system.actors.add(elc) + with self.assertRaisesRegex(Exception, r"ELC: requires periodicity \(1 1 1\)"): + self.system.periodicity = [False, False, False] + with self.assertRaisesRegex(Exception, r"requires periodicity \(1 1 1\)"): + self.system.integrator.run(0, recalc_forces=True) + with self.assertRaisesRegex(Exception, r"requires periodicity \(1 1 1\)"): + self.system.analysis.energy() + with self.assertRaisesRegex(Exception, r"requires periodicity \(1 1 1\)"): + self.system.analysis.pressure() + self.system.periodicity = [True, True, True] + n_nodes = self.system.cell_system.get_state()["n_nodes"] + if n_nodes > 1: + with self.assertRaisesRegex(Exception, "P3M: node grid must be sorted, largest first"): + self.system.cell_system.node_grid = [1, n_nodes, 1] + self.assertEqual( + self.system.cell_system.node_grid, + self.original_node_grid) + with self.assertRaisesRegex(Exception, "Exception while updating the box length: ERROR: ELC gap size .+ larger than box length in z-direction"): + self.system.box_l = [10., 10., 2.5] + self.system.box_l = [10., 10., 10.] + self.system.actors.clear() + with self.assertRaisesRegex(RuntimeError, "P3M real-space cutoff too large for ELC w/ dielectric contrast"): + self.system.box_l = [10., 10., 5.] + elc = espressomd.electrostatics.ELC( + actor=p3m, + gap_size=1., + maxPWerror=1e-3, + delta_mid_top=-1., + delta_mid_bot=-1., + const_pot=True, + pot_diff=-3, + check_neutrality=False, + ) + self.system.actors.add(elc) + self.assertEqual(len(self.system.actors), 0) + self.system.box_l = [10., 10., 10.] + self.system.periodicity = [True, True, False] + with self.assertRaisesRegex(RuntimeError, r"ELC: requires periodicity \(1 1 1\)"): + elc = espressomd.electrostatics.ELC( + gap_size=2., maxPWerror=1., actor=p3m) + self.system.actors.add(elc) + self.assertEqual(len(self.system.actors), 0) + self.system.periodicity = [True, True, True] + + +if __name__ == "__main__": + ut.main() diff --git a/testsuite/python/coulomb_mixed_periodicity.py b/testsuite/python/coulomb_mixed_periodicity.py index 730e42a0993..a4ef2f874d8 100644 --- a/testsuite/python/coulomb_mixed_periodicity.py +++ b/testsuite/python/coulomb_mixed_periodicity.py @@ -21,7 +21,6 @@ import numpy as np import espressomd import espressomd.electrostatics -import espressomd.scafacos import tests_common @@ -30,48 +29,54 @@ class CoulombMixedPeriodicity(ut.TestCase): """Test mixed periodicity electrostatics""" - system = espressomd.System(box_l=[10, 10, 10]) + system = espressomd.System(box_l=[10., 10., 10.]) data = np.genfromtxt(tests_common.data_path( "coulomb_mixed_periodicity_system.data")) - tolerance_force = 5E-4 - tolerance_energy = 1.8E-3 - # Reference energy from MMM2D - reference_energy = 216.640984711 + ref_energy = 216.640984711 def setUp(self): + self.system.box_l = [10., 10., 10.] self.system.time_step = 0.01 self.system.cell_system.skin = 0. # Add particles to system and store reference forces in hash # Input format: id pos q f self.system.part.add(pos=self.data[:, 1:4], q=self.data[:, 4]) - self.forces = self.data[:, 5:8] + self.ref_forces = self.data[:, 5:8] def tearDown(self): self.system.part.clear() self.system.actors.clear() - def compare(self, method_name, energy=True): - # Compare forces and energy now in the system to stored ones - - # Force - force_diff = np.linalg.norm( - self.system.part.all().f - self.forces, axis=1) - self.assertLessEqual( - np.mean(force_diff), self.tolerance_force, - "Absolute force difference too large for method " + method_name) + def compare(self, method_name, force_tol, energy_tol): + self.system.integrator.run(0) + forces_step1 = np.copy(self.system.part.all().f) + energy_step1 = self.system.analysis.energy()["total"] + + err_msg = f"difference too large for method {method_name}" + np.testing.assert_allclose(forces_step1, self.ref_forces, atol=force_tol, + err_msg=f"Force {err_msg}") + np.testing.assert_allclose(energy_step1, self.ref_energy, atol=energy_tol, + err_msg=f"Energy {err_msg}") + + # triggering a solver re-initialization via a box resize + # should not affect the forces nor the energies + original_box_l = np.copy(self.system.box_l) + self.system.box_l = original_box_l * 1.1 + self.system.box_l = original_box_l + self.system.integrator.run(0) + forces_step2 = np.copy(self.system.part.all().f) + energy_step2 = self.system.analysis.energy()["total"] - # Energy - if energy: - self.assertAlmostEqual( - self.system.analysis.energy()["total"], - self.reference_energy, delta=self.tolerance_energy, - msg="Absolute energy difference too large for " + method_name) + err_msg = f"method {method_name} deviates after cells reinitialization" + np.testing.assert_allclose(forces_step1, forces_step2, atol=1e-12, + err_msg=f"Force {err_msg}") + np.testing.assert_allclose(energy_step2, energy_step1, rtol=1e-12, + err_msg=f"Energy {err_msg}") - @utx.skipIfMissingFeatures(["P3M"]) - def test_elc(self): + def setup_elc_system(self): # Make sure, the data satisfies the gap for p in self.system.part: assert p.pos[2] >= 0. and p.pos[2] <= 9., f'particle {p.id} in gap' @@ -81,19 +86,37 @@ def test_elc(self): self.system.cell_system.node_grid, key=lambda x: -x) self.system.periodicity = [1, 1, 1] + @utx.skipIfMissingFeatures(["P3M"]) + def test_elc_cpu(self): + self.system.box_l = [10., 10., 12.] + self.setup_elc_system() + p3m = espressomd.electrostatics.P3M( - prefactor=1, accuracy=1e-6, mesh=(64, 64, 64)) + prefactor=1., accuracy=1e-6, mesh=[42, 42, 50], r_cut=3.5) elc = espressomd.electrostatics.ELC( - p3m_actor=p3m, maxPWerror=1E-6, gap_size=1) + actor=p3m, maxPWerror=1E-6, gap_size=3) self.system.actors.add(elc) - self.system.integrator.run(0) - self.compare("elc", energy=True) + self.compare("elc", force_tol=1e-5, energy_tol=1e-4) - @ut.skipIf(not espressomd.has_features("SCAFACOS") - or 'p2nfft' not in espressomd.scafacos.available_methods(), - 'Skipping test: missing feature SCAFACOS or p2nfft method') + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["P3M"]) + def test_elc_gpu(self): + self.system.box_l = [10., 10., 12.] + self.setup_elc_system() + + p3m = espressomd.electrostatics.P3M( + prefactor=1., accuracy=1e-6, mesh=[42, 42, 50], r_cut=3.5) + elc = espressomd.electrostatics.ELC( + actor=p3m, maxPWerror=1E-6, gap_size=3.) + + self.system.actors.add(elc) + self.compare("elc", force_tol=1e-5, energy_tol=1e-4) + + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("p2nfft") def test_scafacos_p2nfft(self): + self.system.box_l = [10., 10., 10.] self.system.periodicity = [1, 1, 0] self.system.cell_system.set_regular_decomposition() @@ -107,8 +130,13 @@ def test_scafacos_p2nfft(self): "r_cut": 2.4, "pnfft_m": 3}) self.system.actors.add(scafacos) - self.system.integrator.run(0) - self.compare("scafacos_p2nfft", energy=True) + self.assertTrue(scafacos.call_method("get_near_field_delegation")) + self.compare("scafacos_p2nfft", force_tol=1e-4, energy_tol=1.8e-3) + + # calculating near field in ScaFaCoS should yield the same result + scafacos.call_method("set_near_field_delegation", delegate=False) + self.assertFalse(scafacos.call_method("get_near_field_delegation")) + self.compare("scafacos_p2nfft", force_tol=1e-4, energy_tol=1.8e-3) if __name__ == "__main__": diff --git a/testsuite/python/coulomb_tuning.py b/testsuite/python/coulomb_tuning.py index dbe70141ded..ba43d6bca34 100644 --- a/testsuite/python/coulomb_tuning.py +++ b/testsuite/python/coulomb_tuning.py @@ -21,60 +21,43 @@ import unittest_decorators as utx import espressomd -import espressomd.cuda_init import espressomd.electrostatics import tests_common -@utx.skipIfMissingFeatures(["ELECTROSTATICS"]) +@utx.skipIfMissingFeatures(["P3M"]) class CoulombCloudWallTune(ut.TestCase): - """This compares p3m, p3m_gpu electrostatic forces against stored data.""" - system = espressomd.System(box_l=[1.0, 1.0, 1.0]) - - tolerance = 1E-3 + """This compares P3M electrostatic forces against reference values.""" + system = espressomd.System(box_l=[10.0, 10.0, 10.0]) + system.time_step = 0.01 + system.cell_system.skin = 0.4 def setUp(self): - self.system.box_l = (10, 10, 10) - self.system.time_step = 0.01 - self.system.cell_system.skin = 0.4 - data = np.load(tests_common.data_path("coulomb_tuning_system.npz")) - self.forces = data['forces'] + self.ref_forces = data['forces'] self.system.part.add(pos=data['pos'], q=data['charges']) def tearDown(self): self.system.actors.clear() self.system.part.clear() - def compare(self, method_name): - # Compare forces now in the system to stored ones - difference = np.linalg.norm( - self.system.part.all().f - self.forces, axis=1) - self.assertLessEqual( - np.mean(difference), self.tolerance, - "Absolute force difference too large for method " + method_name) - - # Tests for individual methods - @utx.skipIfMissingFeatures(["P3M"]) - def test_p3m(self): - # We have to add some tolerance here, because the reference - # system is not homogeneous - self.system.actors.add( - espressomd.electrostatics.P3M(prefactor=1., accuracy=5e-4, - tune=True)) + def compare(self, actor): + self.system.actors.add(actor) self.system.integrator.run(0) - self.compare("p3m") + np.testing.assert_allclose( + np.copy(self.system.part.all().f), self.ref_forces, atol=2e-3) + + def test_p3m_cpu(self): + actor = espressomd.electrostatics.P3M( + prefactor=1., accuracy=5e-4, tune=True) + self.compare(actor) @utx.skipIfMissingGPU() def test_p3m_gpu(self): - # We have to add some tolerance here, because the reference - # system is not homogeneous - self.system.actors.add( - espressomd.electrostatics.P3MGPU(prefactor=1., accuracy=5e-4, - tune=True)) - self.system.integrator.run(0) - self.compare("p3m_gpu") + actor = espressomd.electrostatics.P3MGPU( + prefactor=1., accuracy=5e-4, tune=True) + self.compare(actor) if __name__ == "__main__": diff --git a/testsuite/python/dawaanr-and-bh-gpu.py b/testsuite/python/dawaanr-and-bh-gpu.py index 1d0fb3d021d..3fbe77e1391 100644 --- a/testsuite/python/dawaanr-and-bh-gpu.py +++ b/testsuite/python/dawaanr-and-bh-gpu.py @@ -45,56 +45,57 @@ def test(self): pf_bh_gpu = 2.34 pf_dawaanr = 3.524 ratio_dawaanr_bh_gpu = pf_dawaanr / pf_bh_gpu - self.system.box_l = 3 * [15] - self.system.periodicity = [0, 0, 0] - self.system.time_step = 1E-4 - self.system.cell_system.skin = 0.1 + system = self.system + system.box_l = 3 * [15] + system.periodicity = [0, 0, 0] + system.time_step = 1E-4 + system.cell_system.skin = 0.1 for n in [128, 541]: dipole_modulus = 1.3 part_dip = dipole_modulus * tests_common.random_dipoles(n) - part_pos = np.random.random((n, 3)) * self.system.box_l[0] - self.system.part.add(pos=part_pos, dip=part_dip) + part_pos = np.random.random((n, 3)) * system.box_l[0] + system.part.add(pos=part_pos, dip=part_dip) - self.system.non_bonded_inter[0, 0].lennard_jones.set_params( + system.non_bonded_inter[0, 0].lennard_jones.set_params( epsilon=10.0, sigma=0.5, cutoff=0.55, shift="auto") - self.system.thermostat.set_langevin(kT=0.0, gamma=10.0, seed=42) + system.thermostat.set_langevin(kT=0.0, gamma=10.0, seed=42) g = espressomd.galilei.GalileiTransform() g.kill_particle_motion(rotation=True) - self.system.integrator.set_vv() + system.integrator.set_vv() - self.system.non_bonded_inter[0, 0].lennard_jones.set_params( + system.non_bonded_inter[0, 0].lennard_jones.set_params( epsilon=0.0, sigma=0.0, cutoff=-1, shift=0.0) - self.system.cell_system.skin = 0.0 - self.system.time_step = 0.01 - self.system.thermostat.turn_off() + system.cell_system.skin = 0.0 + system.time_step = 0.01 + system.thermostat.turn_off() # gamma should be zero in order to avoid the noise term in force # and torque - self.system.thermostat.set_langevin(kT=1.297, gamma=0.0) + system.thermostat.set_langevin(kT=1.297, gamma=0.0) dds_cpu = espressomd.magnetostatics.DipolarDirectSumCpu( prefactor=pf_dawaanr) - self.system.actors.add(dds_cpu) - self.system.integrator.run(steps=0, recalc_forces=True) + system.actors.add(dds_cpu) + system.integrator.run(steps=0, recalc_forces=True) - dawaanr_f = np.copy(self.system.part.all().f) - dawaanr_t = np.copy(self.system.part.all().torque_lab) - dawaanr_e = self.system.analysis.energy()["total"] + dawaanr_f = np.copy(system.part.all().f) + dawaanr_t = np.copy(system.part.all().torque_lab) + dawaanr_e = system.analysis.energy()["total"] del dds_cpu - self.system.actors.clear() + system.actors.clear() - self.system.integrator.run(steps=0, recalc_forces=True) + system.integrator.run(steps=0, recalc_forces=True) bh_gpu = espressomd.magnetostatics.DipolarBarnesHutGpu( prefactor=pf_bh_gpu, epssq=200.0, itolsq=8.0) - self.system.actors.add(bh_gpu) - self.system.integrator.run(steps=0, recalc_forces=True) + system.actors.add(bh_gpu) + system.integrator.run(steps=0, recalc_forces=True) - bhgpu_f = np.copy(self.system.part.all().f) - bhgpu_t = np.copy(self.system.part.all().torque_lab) - bhgpu_e = self.system.analysis.energy()["total"] + bhgpu_f = np.copy(system.part.all().f) + bhgpu_t = np.copy(system.part.all().torque_lab) + bhgpu_e = system.analysis.energy()["total"] # compare for i in range(n): @@ -120,11 +121,11 @@ def test(self): msg='Energies for dawaanr {0} and bh_gpu {1} do not match.' .format(dawaanr_e, ratio_dawaanr_bh_gpu * bhgpu_e)) - self.system.integrator.run(steps=0, recalc_forces=True) + system.integrator.run(steps=0, recalc_forces=True) del bh_gpu - self.system.actors.clear() - self.system.part.clear() + system.actors.clear() + system.part.clear() if __name__ == '__main__': diff --git a/testsuite/python/dds-and-bh-gpu.py b/testsuite/python/dds-and-bh-gpu.py index ad6e463ef75..8fd3a70add9 100644 --- a/testsuite/python/dds-and-bh-gpu.py +++ b/testsuite/python/dds-and-bh-gpu.py @@ -43,10 +43,11 @@ def test(self): pf_dds_gpu = 3.524 ratio_dawaanr_bh_gpu = pf_dds_gpu / pf_bh_gpu l = 15 - self.system.box_l = 3 * [l] - self.system.periodicity = 3 * [False] - self.system.time_step = 1E-4 - self.system.cell_system.skin = 0.1 + system = self.system + system.box_l = 3 * [l] + system.periodicity = 3 * [False] + system.time_step = 1E-4 + system.cell_system.skin = 0.1 part_dip = np.zeros((3)) @@ -54,48 +55,56 @@ def test(self): dipole_modulus = 1.3 part_pos = np.random.random((n, 3)) * l part_dip = dipole_modulus * tests_common.random_dipoles(n) - self.system.part.add(pos=part_pos, dip=part_dip, - v=n * [(0, 0, 0)], omega_body=n * [(0, 0, 0)]) + system.part.add(pos=part_pos, dip=part_dip, + v=n * [(0, 0, 0)], omega_body=n * [(0, 0, 0)]) - self.system.non_bonded_inter[0, 0].lennard_jones.set_params( + system.non_bonded_inter[0, 0].lennard_jones.set_params( epsilon=10.0, sigma=0.5, cutoff=0.55, shift="auto") - self.system.thermostat.set_langevin(kT=0.0, gamma=10.0, seed=42) + system.thermostat.set_langevin(kT=0.0, gamma=10.0, seed=42) g = espressomd.galilei.GalileiTransform() g.kill_particle_motion(rotation=True) - self.system.integrator.set_vv() + system.integrator.set_vv() - self.system.non_bonded_inter[0, 0].lennard_jones.set_params( + system.non_bonded_inter[0, 0].lennard_jones.set_params( epsilon=0.0, sigma=0.0, cutoff=-1, shift=0.0) - self.system.cell_system.skin = 0.0 - self.system.time_step = 0.01 - self.system.thermostat.turn_off() + system.cell_system.skin = 0.0 + system.time_step = 0.01 + system.thermostat.turn_off() # gamma should be zero in order to avoid the noise term in force # and torque - self.system.thermostat.set_langevin(kT=1.297, gamma=0.0) + system.thermostat.set_langevin(kT=1.297, gamma=0.0) dds_gpu = espressomd.magnetostatics.DipolarDirectSumGpu( prefactor=pf_dds_gpu) - self.system.actors.add(dds_gpu) - self.system.integrator.run(steps=0, recalc_forces=True) + system.actors.add(dds_gpu) + # check MD cell reset has no impact + system.box_l = system.box_l + system.periodicity = system.periodicity + system.cell_system.node_grid = system.cell_system.node_grid + system.integrator.run(steps=0, recalc_forces=True) - dawaanr_f = np.copy(self.system.part.all().f) - dawaanr_t = np.copy(self.system.part.all().torque_lab) - dawaanr_e = self.system.analysis.energy()["total"] + dawaanr_f = np.copy(system.part.all().f) + dawaanr_t = np.copy(system.part.all().torque_lab) + dawaanr_e = system.analysis.energy()["total"] del dds_gpu - self.system.actors.clear() + system.actors.clear() - self.system.integrator.run(steps=0, recalc_forces=True) + system.integrator.run(steps=0, recalc_forces=True) bh_gpu = espressomd.magnetostatics.DipolarBarnesHutGpu( prefactor=pf_bh_gpu, epssq=200.0, itolsq=8.0) - self.system.actors.add(bh_gpu) - self.system.integrator.run(steps=0, recalc_forces=True) + system.actors.add(bh_gpu) + # check MD cell reset has no impact + system.box_l = system.box_l + system.periodicity = system.periodicity + system.cell_system.node_grid = system.cell_system.node_grid + system.integrator.run(steps=0, recalc_forces=True) - bhgpu_f = np.copy(self.system.part.all().f) - bhgpu_t = np.copy(self.system.part.all().torque_lab) - bhgpu_e = self.system.analysis.energy()["total"] + bhgpu_f = np.copy(system.part.all().f) + bhgpu_t = np.copy(system.part.all().torque_lab) + bhgpu_e = system.analysis.energy()["total"] # compare for i in range(n): @@ -121,11 +130,11 @@ def test(self): msg='Energies for dawaanr {0} and bh_gpu {1} do not match.' .format(dawaanr_e, ratio_dawaanr_bh_gpu * bhgpu_e)) - self.system.integrator.run(steps=0, recalc_forces=True) + system.integrator.run(steps=0, recalc_forces=True) del bh_gpu - self.system.actors.clear() - self.system.part.clear() + system.actors.clear() + system.part.clear() if __name__ == '__main__': diff --git a/testsuite/python/dipolar_direct_summation.py b/testsuite/python/dipolar_direct_summation.py index f341261e504..2ea1ffba4d4 100644 --- a/testsuite/python/dipolar_direct_summation.py +++ b/testsuite/python/dipolar_direct_summation.py @@ -18,7 +18,6 @@ # import espressomd import espressomd.magnetostatics -import espressomd.magnetostatic_extensions import pathlib import numpy as np import unittest as ut @@ -48,6 +47,10 @@ def dds_gpu_data(self): dds_cpu = espressomd.magnetostatics.DipolarDirectSumGpu(prefactor=1.2) system.actors.add(dds_cpu) + # check MD cell reset has no impact + self.system.box_l = self.system.box_l + self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid system.integrator.run(steps=0, recalc_forces=True) ref_e = system.analysis.energy()["dipolar"] @@ -63,6 +66,10 @@ def dds_data(self): dds_cpu = espressomd.magnetostatics.DipolarDirectSumCpu(prefactor=1.2) system.actors.add(dds_cpu) + # check MD cell reset has no impact + self.system.box_l = self.system.box_l + self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid system.integrator.run(steps=0, recalc_forces=True) ref_e = system.analysis.energy()["dipolar"] @@ -79,6 +86,10 @@ def dds_replica_data(self): dds_cpu = espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu( prefactor=1.2, n_replica=0) system.actors.add(dds_cpu) + # check MD cell reset has no impact + self.system.box_l = self.system.box_l + self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid system.integrator.run(steps=0, recalc_forces=True) ref_e = system.analysis.energy()["dipolar"] @@ -200,9 +211,8 @@ def test_dds_gpu(self): force_tol=1E-4, torque_tol=1E-4) - @ut.skipIf(not espressomd.has_features("SCAFACOS_DIPOLES") or - "direct" not in espressomd.scafacos.available_methods(), - "Skipping test: missing SCAFACOS_DIPOLES or 'direct' method") + @utx.skipIfMissingFeatures(["SCAFACOS_DIPOLES"]) + @utx.skipIfMissingScafacosMethod("direct") def test_dds_scafacos(self): self.check_open_bc( self.fcs_data, diff --git a/testsuite/python/dipolar_interface.py b/testsuite/python/dipolar_interface.py index 32eaae4d2c6..b4b5e5005cc 100644 --- a/testsuite/python/dipolar_interface.py +++ b/testsuite/python/dipolar_interface.py @@ -22,84 +22,158 @@ import tests_common import espressomd.magnetostatics -import espressomd.magnetostatic_extensions @utx.skipIfMissingFeatures(["DIPOLES"]) -class MagnetostaticsInterface(ut.TestCase): +class Test(ut.TestCase): system = espressomd.System(box_l=[10., 10., 10.]) + n_nodes = system.cell_system.get_state()["n_nodes"] def setUp(self): self.system.box_l = [10., 10., 10.] self.system.periodicity = [True, True, True] - self.system.part.add(pos=(0.0, 0.0, 0.0), dip=(1.3, 2.1, -6)) - self.system.part.add(pos=(0.1, 0.1, 0.1), dip=(7.3, 6.1, -4)) + self.system.part.add(pos=(0.1, 0.1, 0.1), dip=(1.3, 2.1, -6.0)) + self.system.part.add(pos=(0.2, 0.2, 0.2), dip=(7.3, 6.1, -4.0)) def tearDown(self): self.system.part.clear() self.system.actors.clear() - if espressomd.has_features("DIPOLES"): - test_dds_cpu = tests_common.generate_test_for_class( + if espressomd.has_features("DIPOLES") and n_nodes == 1: + test_dds_cpu = tests_common.generate_test_for_actor_class( system, espressomd.magnetostatics.DipolarDirectSumCpu, dict(prefactor=3.4)) + if espressomd.has_features("DIPOLES") and n_nodes == 1: + test_dds_cpu = tests_common.generate_test_for_actor_class( + system, espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu, + dict(prefactor=3.4, n_replica=3)) + if espressomd.has_features( "DIPOLAR_DIRECT_SUM") and espressomd.gpu_available(): - test_dds_gpu = tests_common.generate_test_for_class( + test_dds_gpu = tests_common.generate_test_for_actor_class( system, espressomd.magnetostatics.DipolarDirectSumGpu, dict(prefactor=3.4)) - if espressomd.has_features("DIPOLES"): - test_dds_replica = tests_common.generate_test_for_class( + if espressomd.has_features( + "DIPOLAR_BARNES_HUT") and espressomd.gpu_available(): + test_dds_gpu = tests_common.generate_test_for_actor_class( + system, espressomd.magnetostatics.DipolarBarnesHutGpu, + dict(prefactor=3.4, epssq=200.0, itolsq=8.0)) + + if espressomd.has_features("DIPOLES") and n_nodes == 1: + test_dds_replica = tests_common.generate_test_for_actor_class( system, espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu, dict(prefactor=3.4, n_replica=2)) - def test_exceptions(self): - actor = espressomd.magnetostatics.DipolarDirectSumCpu(prefactor=-1) - with self.assertRaises(ValueError): - self.system.actors.add(actor) - actor = self.system.actors[0] - actor.set_params(prefactor=1) - with self.assertRaises(ValueError): - actor.set_params(prefactor=-1) - with self.assertRaises(ValueError): - actor.set_magnetostatics_prefactor() - self.system.actors.clear() - actor_dawaanr = espressomd.magnetostatics.DipolarDirectSumCpu( - prefactor=1) - actor_mdlc = espressomd.magnetostatic_extensions.DLC( - gap_size=2, maxPWerror=1e-5) - self.system.actors.add(actor_dawaanr) - with self.assertRaisesRegex(RuntimeError, 'MDLC cannot extend the currently active magnetostatics solver'): - self.system.actors.add(actor_mdlc) - self.system.actors.clear() - actor = espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu( - prefactor=1, n_replica=-2) - with self.assertRaisesRegex(RuntimeError, 'requires n_replica >= 0'): - self.system.actors.add(actor) - self.system.actors.clear() + if espressomd.has_features("DP3M"): + test_dp3m_metallic = tests_common.generate_test_for_actor_class( + system, espressomd.magnetostatics.DipolarP3M, + dict(prefactor=2., epsilon=0., mesh_off=[0.6, 0.7, 0.8], r_cut=1.4, + cao=2, mesh=[8, 8, 8], alpha=12., accuracy=0.01, tune=False)) + test_dp3m_non_metallic = tests_common.generate_test_for_actor_class( + system, espressomd.magnetostatics.DipolarP3M, + dict(prefactor=3., epsilon=3., mesh_off=[0.6, 0.7, 0.8], r_cut=1.6, + cao=3, mesh=[8, 8, 8], alpha=14., accuracy=0.01, tune=False)) + test_dp3m_dlc = tests_common.generate_test_for_actor_class( + system, espressomd.magnetostatics.DLC, + dict(gap_size=2., maxPWerror=0.1, far_cut=1., + actor=espressomd.magnetostatics.DipolarP3M( + cao=2, tune=False, mesh=8, prefactor=2., + r_cut=1.4, alpha=12., accuracy=0.01))) + + @ut.skipIf(n_nodes == 1, "only runs for 2 or more MPI ranks") + def test_mpi_sanity_checks(self): + DDSR = espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu + DDSC = espressomd.magnetostatics.DipolarDirectSumCpu + # some actors don't support parallelization + with self.assertRaisesRegex(RuntimeError, 'MPI parallelization not supported'): + DDSC(prefactor=1.) + with self.assertRaisesRegex(RuntimeError, 'MPI parallelization not supported'): + DDSR(prefactor=1., n_replica=2) + + @ut.skipIf(n_nodes != 1, "only runs for 1 MPI rank") + def test_ddswr_mixed_particles(self): + # check that non-magnetic particles don't influence the DDS kernels actor = espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu( - prefactor=1, n_replica=0) + prefactor=1., n_replica=2) + self.system.actors.add(actor) + energy1 = self.system.analysis.energy()["dipolar"] + self.system.part.add(pos=(0.4, 0.2, 0.2), dip=(0.0, 0.0, 0.0)) + energy2 = self.system.analysis.energy()["dipolar"] + self.assertAlmostEqual(energy1, energy2, delta=1e-12) + + @ut.skipIf(n_nodes != 1, "only runs for 1 MPI rank") + def test_dds_mixed_particles(self): + # check that non-magnetic particles don't influence the DDS kernels + actor = espressomd.magnetostatics.DipolarDirectSumCpu(prefactor=1.) + self.system.actors.add(actor) + energy1 = self.system.analysis.energy()["dipolar"] + self.system.part.add(pos=(0.4, 0.2, 0.2), dip=(0.0, 0.0, 0.0)) + energy2 = self.system.analysis.energy()["dipolar"] + self.assertAlmostEqual(energy1, energy2, delta=1e-12) + + @ut.skipIf(n_nodes != 1, "only runs for 1 MPI rank") + def test_exceptions_serial(self): + DDSR = espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu + DDSC = espressomd.magnetostatics.DipolarDirectSumCpu + MDLC = espressomd.magnetostatics.DLC + # check invalid prefactors + with self.assertRaisesRegex(ValueError, "Parameter 'prefactor' must be > 0"): + DDSC(prefactor=-1.) + # check runtime errors and input parameters + dds = DDSC(prefactor=1.) + with self.assertRaisesRegex(RuntimeError, "Parameter 'actor' is missing"): + MDLC(gap_size=2., maxPWerror=0.1) + with self.assertRaisesRegex(ValueError, "Parameter 'actor' of type Dipoles::DipolarDirectSum isn't supported by DLC"): + MDLC(gap_size=2., maxPWerror=0.1, actor=dds) + with self.assertRaisesRegex(ValueError, "Parameter 'n_replica' must be >= 0"): + DDSR(prefactor=1., n_replica=-2) + with self.assertRaisesRegex(ValueError, "Parameter 'prefactor' must be > 0"): + DDSR(prefactor=-2., n_replica=1) with self.assertRaisesRegex(RuntimeError, 'with replica does not support a periodic system with zero replica'): - self.system.actors.add(actor) - self.system.actors.clear() + DDSR(prefactor=1., n_replica=0) + # run sanity checks self.system.periodicity = [True, True, False] - actor = espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu( - prefactor=1, n_replica=1) - solver_mdlc = espressomd.magnetostatic_extensions.DLC( - gap_size=1, maxPWerror=1e-5) - self.system.actors.add(actor) - with self.assertRaisesRegex(RuntimeError, "MDLC requires periodicity 1 1 1"): - self.system.actors.add(solver_mdlc) - self.system.actors.remove(solver_mdlc) + ddsr = DDSR(prefactor=1., n_replica=1) + with self.assertRaisesRegex(Exception, r"DLC: requires periodicity \(1 1 1\)"): + mdlc = MDLC(gap_size=1., maxPWerror=1e-5, actor=ddsr) + self.system.actors.add(mdlc) + self.assertEqual(len(self.system.actors), 0) self.system.periodicity = [True, True, True] self.system.box_l = [10., 10. + 2e-3, 10.] - with self.assertRaisesRegex(RuntimeError, "box size in x direction is different from y direction"): - self.system.actors.add(solver_mdlc) + with self.assertRaisesRegex(Exception, "box size in x direction is different from y direction"): + mdlc = MDLC(gap_size=1., maxPWerror=1e-5, actor=ddsr) + self.system.actors.add(mdlc) + self.assertEqual(len(self.system.actors), 0) + # check it's safe to resize the box, i.e. there are no currently + # active sanity check in the core + self.system.box_l = [10., 10., 10.] + with self.assertRaisesRegex(Exception, "box size in x direction is different from y direction"): + ddsr = DDSR(prefactor=1., n_replica=1) + mdlc = MDLC(gap_size=1., maxPWerror=1e-5, actor=ddsr) + self.system.actors.add(mdlc) + self.system.box_l = [9., 10., 10.] self.system.actors.clear() self.system.box_l = [10., 10., 10.] + @utx.skipIfMissingFeatures(["DP3M"]) + def test_exceptions_parallel(self): + DP3M = espressomd.magnetostatics.DipolarP3M + MDLC = espressomd.magnetostatics.DLC + dp3m_params = dict(prefactor=1., epsilon=0.1, accuracy=1e-6, + mesh=[49, 49, 49], cao=7, r_cut=4.5, alpha=0.9) + with self.assertRaisesRegex(ValueError, "Parameter 'prefactor' must be > 0"): + espressomd.magnetostatics.DipolarP3M( + **{**dp3m_params, 'prefactor': -2.}) + with self.assertRaisesRegex(ValueError, "DipolarP3M mesh has to be an integer or integer list of length 3"): + espressomd.magnetostatics.DipolarP3M( + **{**dp3m_params, 'mesh': [49, 49]}) + dp3m = DP3M(**dp3m_params) + mdlc = MDLC(gap_size=2., maxPWerror=0.1, actor=dp3m) + with self.assertRaisesRegex(ValueError, "Parameter 'actor' of type Dipoles::DipolarLayerCorrection isn't supported by DLC"): + MDLC(gap_size=2., maxPWerror=0.1, actor=mdlc) + if __name__ == "__main__": ut.main() diff --git a/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py b/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py index c70460dce7d..a8d0fc017b6 100644 --- a/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py +++ b/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py @@ -22,7 +22,6 @@ import espressomd import espressomd.magnetostatics -import espressomd.magnetostatic_extensions import numpy as np import unittest as ut import unittest_decorators as utx @@ -73,12 +72,11 @@ def test_mdlc(self): partcls = s.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) partcls.rotation = 3 * [True] - p3m = espressomd.magnetostatics.DipolarP3M( + dp3m = espressomd.magnetostatics.DipolarP3M( prefactor=DIPOLAR_PREFACTOR, mesh=32, accuracy=1E-4) - dlc = espressomd.magnetostatic_extensions.DLC( - maxPWerror=1E-5, gap_size=gap_size) - s.actors.add(p3m) - s.actors.add(dlc) + mdlc = espressomd.magnetostatics.DLC( + maxPWerror=1E-5, gap_size=gap_size, actor=dp3m) + s.actors.add(mdlc) s.integrator.run(0) err_f = self.vector_error( partcls.f, data[:, 7:10] * DIPOLAR_PREFACTOR) @@ -130,9 +128,9 @@ def test_p3m(self): partcls = s.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) partcls.rotation = 3 * [True] - p3m = espressomd.magnetostatics.DipolarP3M( + dp3m = espressomd.magnetostatics.DipolarP3M( prefactor=DIPOLAR_PREFACTOR, mesh=32, accuracy=1E-6, epsilon="metallic") - s.actors.add(p3m) + s.actors.add(dp3m) s.integrator.run(0) expected = np.genfromtxt( tests_common.data_path("p3m_magnetostatics_expected.data"))[:, 1:] diff --git a/testsuite/python/dipolar_mpi_exceptions.py b/testsuite/python/dipolar_mpi_exceptions.py deleted file mode 100644 index abd38219d38..00000000000 --- a/testsuite/python/dipolar_mpi_exceptions.py +++ /dev/null @@ -1,47 +0,0 @@ -# -# Copyright (C) 2021 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import unittest as ut -import unittest_decorators as utx - -import espressomd.magnetostatics - - -@utx.skipIfMissingFeatures(["DIPOLES"]) -class MagnetostaticsInterface(ut.TestCase): - system = espressomd.System(box_l=[10., 10., 10.]) - n_nodes = system.cell_system.get_state()["n_nodes"] - - def tearDown(self): - self.system.actors.clear() - - @ut.skipIf(n_nodes == 1, "only runs for 2+ MPI ranks") - def test_exceptions_mpi(self): - actor = espressomd.magnetostatics.DipolarDirectSumCpu(prefactor=1) - with self.assertRaisesRegex(RuntimeError, 'MPI parallelization not supported'): - self.system.actors.add(actor) - self.system.actors.clear() - actor = espressomd.magnetostatics.DipolarDirectSumWithReplicaCpu( - prefactor=1, n_replica=2) - with self.assertRaisesRegex(RuntimeError, 'MPI parallelization not supported'): - self.system.actors.add(actor) - - -if __name__ == "__main__": - ut.main() diff --git a/testsuite/python/dipolar_p3m.py b/testsuite/python/dipolar_p3m.py index bca0e09c5a3..7002823116b 100644 --- a/testsuite/python/dipolar_p3m.py +++ b/testsuite/python/dipolar_p3m.py @@ -18,36 +18,29 @@ # import unittest as ut import unittest_decorators as utx -import tests_common import numpy as np import espressomd.magnetostatics -import espressomd.magnetostatic_extensions @utx.skipIfMissingFeatures(["DP3M"]) class MagnetostaticsP3M(ut.TestCase): system = espressomd.System(box_l=3 * [10.]) - - def setUp(self): - self.partcls = self.system.part.add(pos=[[4.0, 2.0, 2.0], [6.0, 2.0, 2.0]], - dip=[(1.3, 2.1, -6.0), (7.3, 6.1, -4.0)]) + system.cell_system.skin = 0. def tearDown(self): self.system.part.clear() self.system.actors.clear() - if espressomd.has_features("DP3M"): - test_DP3M = tests_common.generate_test_for_class( - system, espressomd.magnetostatics.DipolarP3M, - dict(prefactor=1., epsilon=0., mesh_off=[0.5, 0.5, 0.5], r_cut=2.4, - cao=1, mesh=[8, 8, 8], alpha=12, accuracy=0.01, tune=False)) - def test_dp3m(self): self.system.time_step = 0.01 prefactor = 1.1 box_vol = self.system.volume() - p1, p2 = self.partcls + self.system.part.add(pos=[1., 2., 3.]) # non-magnetic particle + partcls = self.system.part.add( + pos=[(4.0, 2.0, 2.0), (6.0, 2.0, 2.0)], + dip=[(1.3, 2.1, -6.0), (7.3, 6.1, -4.0)]) + p1, p2 = partcls dip = np.copy(p1.dip + p2.dip) dp3m_params = {'accuracy': 1e-6, 'mesh': [49, 49, 49], @@ -81,27 +74,45 @@ def test_dp3m(self): # keep current values as reference to check for DP3M dipole correction ref_dp3m_energy_metallic = self.system.analysis.energy()['dipolar'] - ref_dp3m_forces_metallic = np.copy(self.partcls.f) + ref_dp3m_forces_metallic = np.copy(partcls.f) ref_dp3m_torque_metallic = np.array([ p1.convert_vector_space_to_body(p1.torque_lab), p2.convert_vector_space_to_body(p2.torque_lab)]) # MDLC cancels out dipole correction - mdlc = espressomd.magnetostatic_extensions.DLC(**mdlc_params) + self.system.actors.remove(dp3m) + mdlc = espressomd.magnetostatics.DLC(actor=dp3m, **mdlc_params) self.system.actors.add(mdlc) + self.assertAlmostEqual(mdlc.prefactor, 1.1, delta=1e-12) # keep current values as reference to check for MDLC dipole correction self.system.integrator.run(0, recalc_forces=True) ref_mdlc_energy_metallic = self.system.analysis.energy()['dipolar'] - ref_mdlc_forces_metallic = np.copy(self.partcls.f) - ref_mdlc_torque_metallic = np.copy(self.partcls.torque_lab) + ref_mdlc_forces_metallic = np.copy(partcls.f) + ref_mdlc_torque_metallic = np.copy(partcls.torque_lab) + + # actors should remain in a valid state after a cell system reset + self.system.box_l = self.system.box_l + self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid + self.system.integrator.run(0, recalc_forces=True) + mdlc_forces = np.copy(partcls.f) + mdlc_torque = np.copy(partcls.torque_lab) + mdlc_energy = self.system.analysis.energy()['dipolar'] + np.testing.assert_allclose(mdlc_forces, ref_mdlc_forces_metallic, + atol=1e-12) + np.testing.assert_allclose(mdlc_torque, ref_mdlc_torque_metallic, + atol=1e-12) + np.testing.assert_allclose(mdlc_energy, ref_mdlc_energy_metallic, + atol=1e-12) + self.system.actors.clear() # check non-metallic case tol = 1e-10 - for epsilon in np.power(10., np.arange(-4, 5)): - dipole_correction = 4 * np.pi / box_vol / (1 + 2 * epsilon) - e_correction = dipole_correction / 2 * np.linalg.norm(dip)**2 + for epsilon in np.power(10., np.arange(-4, 5, 2)): + dipole_correction = 4. * np.pi / box_vol / (1. + 2. * epsilon) + e_correction = dipole_correction / 2. * np.linalg.norm(dip)**2 t_correction = np.cross([p1.dip, p2.dip], dipole_correction * dip) ref_dp3m_energy = ref_dp3m_energy_metallic + prefactor * e_correction ref_dp3m_forces = ref_dp3m_forces_metallic @@ -110,7 +121,7 @@ def test_dp3m(self): prefactor=prefactor, epsilon=epsilon, tune=False, **dp3m_params) self.system.actors.add(dp3m) self.system.integrator.run(0, recalc_forces=True) - dp3m_forces = np.copy(self.partcls.f) + dp3m_forces = np.copy(partcls.f) dp3m_torque = np.array([ p1.convert_vector_space_to_body(p1.torque_lab), p2.convert_vector_space_to_body(p2.torque_lab)]) @@ -123,11 +134,12 @@ def test_dp3m(self): ref_mdlc_energy = ref_mdlc_energy_metallic ref_mdlc_forces = ref_mdlc_forces_metallic ref_mdlc_torque = ref_mdlc_torque_metallic - mdlc = espressomd.magnetostatic_extensions.DLC(**mdlc_params) + self.system.actors.remove(dp3m) + mdlc = espressomd.magnetostatics.DLC(actor=dp3m, **mdlc_params) self.system.actors.add(mdlc) self.system.integrator.run(0, recalc_forces=True) - mdlc_forces = np.copy(self.partcls.f) - mdlc_torque = np.copy(self.partcls.torque_lab) + mdlc_forces = np.copy(partcls.f) + mdlc_torque = np.copy(partcls.torque_lab) mdlc_energy = self.system.analysis.energy()['dipolar'] np.testing.assert_allclose(mdlc_forces, ref_mdlc_forces, atol=tol) np.testing.assert_allclose(mdlc_torque, ref_mdlc_torque, atol=tol) diff --git a/testsuite/python/elc.py b/testsuite/python/elc.py index 32fe7754935..fb068e7dd50 100644 --- a/testsuite/python/elc.py +++ b/testsuite/python/elc.py @@ -20,24 +20,27 @@ import numpy as np -GAP = np.array([0, 0, 3.]) -BOX_L = np.array(3 * [10]) + GAP +GAP = np.array([0., 0., 3.]) +BOX_L = np.array(3 * [10.]) + GAP TIME_STEP = 1e-100 POTENTIAL_DIFFERENCE = -3. -@utx.skipIfMissingFeatures(["P3M"]) -class ElcTest(ut.TestCase): +class ElcTest: system = espressomd.System(box_l=BOX_L, time_step=TIME_STEP) system.cell_system.skin = 0.0 + def tearDown(self): + self.system.part.clear() + self.system.actors.clear() + def test_finite_potential_drop(self): system = self.system p1 = system.part.add(pos=[0, 0, 1], q=+1) p2 = system.part.add(pos=[0, 0, 9], q=-1) - p3m = espressomd.electrostatics.P3M( + p3m = self.p3m_class( # zero is not allowed prefactor=1e-100, mesh=32, @@ -45,14 +48,15 @@ def test_finite_potential_drop(self): accuracy=1e-3, ) elc = espressomd.electrostatics.ELC( - p3m_actor=p3m, + actor=p3m, gap_size=GAP[2], maxPWerror=1e-3, delta_mid_top=-1, delta_mid_bot=-1, - const_pot=1, + const_pot=True, pot_diff=POTENTIAL_DIFFERENCE, ) + system.actors.add(elc) # Calculated energy @@ -63,11 +67,11 @@ def test_finite_potential_drop(self): # Expected potential is -E_expected * z, so U_expected = -E_expected * (p1.pos[2] * p1.q + p2.pos[2] * p2.q) - self.assertAlmostEqual(U_elc, U_expected) - system.integrator.run(0) - self.assertAlmostEqual(E_expected, p1.f[2] / p1.q) - self.assertAlmostEqual(E_expected, p2.f[2] / p2.q) + + self.assertAlmostEqual(U_elc, U_expected) + self.assertAlmostEqual(p1.f[2] / p1.q, E_expected) + self.assertAlmostEqual(p2.f[2] / p2.q, E_expected) # Check if error is thrown when particles enter the ELC gap # positive direction @@ -84,5 +88,20 @@ def test_finite_potential_drop(self): self.system.integrator.run(2) +@utx.skipIfMissingFeatures(["P3M"]) +class ElcTestCPU(ElcTest, ut.TestCase): + + p3m_class = espressomd.electrostatics.P3M + rtol = 1e-7 + + +@utx.skipIfMissingGPU() +@utx.skipIfMissingFeatures(["P3M"]) +class ElcTestGPU(ElcTest, ut.TestCase): + + p3m_class = espressomd.electrostatics.P3MGPU + rtol = 4e-6 + + if __name__ == "__main__": ut.main() diff --git a/testsuite/python/elc_vs_analytic.py b/testsuite/python/elc_vs_analytic.py index 333061ba6ad..ad76c8b3ccc 100644 --- a/testsuite/python/elc_vs_analytic.py +++ b/testsuite/python/elc_vs_analytic.py @@ -19,11 +19,10 @@ import espressomd import numpy as np import espressomd.electrostatics +import espressomd.code_info -@utx.skipIfMissingFeatures(["P3M"]) -class ELC_vs_analytic(ut.TestCase): - # Handle to espresso system +class Test: box_l = 200. system = espressomd.System(box_l=[box_l, box_l, box_l]) accuracy = 1e-7 @@ -34,14 +33,16 @@ class ELC_vs_analytic(ut.TestCase): delta_mid_bot = 39. / 41. distance = 1. - number_samples = 6 if '@WITH_COVERAGE@' == 'ON' else 12 minimum_distance_to_wall = 0.1 zPos = np.linspace( minimum_distance_to_wall, box_l - minimum_distance_to_wall - distance, - number_samples) + 6 if espressomd.code_info.build_type() == "Coverage" else 12) q = np.arange(-5.0, 5.1, 2.5) - prefactor = 2.0 + + def tearDown(self): + self.system.part.clear() + self.system.actors.clear() def test_elc(self): """ @@ -57,34 +58,33 @@ def test_elc(self): self.system.cell_system.set_regular_decomposition( use_verlet_lists=True) self.system.periodicity = [1, 1, 1] - p3m = espressomd.electrostatics.P3M(prefactor=self.prefactor, - accuracy=self.accuracy, - mesh=[58, 58, 70], - cao=4) - elc = espressomd.electrostatics.ELC(p3m_actor=p3m, + prefactor = 2.0 + p3m = self.p3m_class(prefactor=prefactor, accuracy=self.accuracy, + mesh=[58, 58, 70], cao=4) + elc = espressomd.electrostatics.ELC(actor=p3m, gap_size=self.elc_gap, maxPWerror=self.accuracy, delta_mid_bot=self.delta_mid_bot, delta_mid_top=self.delta_mid_top) self.system.actors.add(elc) - elc_results = self.scan() + elc_forces, elc_energy = self.scan() # ANALYTIC SOLUTION - charge_reshaped = self.prefactor * np.square(self.q.reshape(-1, 1)) - analytic_force = charge_reshaped * (1 / self.distance ** 2 + self.delta_mid_bot * ( + charge_reshaped = prefactor * np.square(self.q.reshape(-1, 1)) + analytic_forces = charge_reshaped * (1 / self.distance ** 2 + self.delta_mid_bot * ( 1 / np.square(2 * self.zPos) - 1 / np.square(2 * self.zPos + self.distance))) analytic_energy = charge_reshaped * (-1 / self.distance + self.delta_mid_bot * (1 / ( 4 * self.zPos) - 1 / (2 * self.zPos + self.distance) + 1 / (4 * (self.zPos + self.distance)))) - analytic_results = np.dstack((analytic_force, analytic_energy)) - - np.testing.assert_allclose( - elc_results, analytic_results, rtol=0, atol=self.check_accuracy) + np.testing.assert_allclose(elc_energy, analytic_energy, atol=1e-4) + np.testing.assert_allclose(elc_forces, analytic_forces, atol=1e-4, + rtol=self.rtol) def scan(self): p1, p2 = self.system.part.all() - result_array = np.empty((len(self.q), len(self.zPos), 2)) + elc_forces = np.empty((len(self.q), len(self.zPos))) + elc_energy = np.empty(elc_forces.shape) for chargeIndex, charge in enumerate(self.q): p1.q = charge p2.q = -charge @@ -94,10 +94,25 @@ def scan(self): p2.pos = [pos[0], pos[1], z + self.distance] self.system.integrator.run(0) - result_array[chargeIndex, i, 0] = p1.f[2] - result_array[chargeIndex, i, 1] = self.system.analysis.energy()[ + elc_forces[chargeIndex, i] = p1.f[2] + elc_energy[chargeIndex, i] = self.system.analysis.energy()[ "total"] - return result_array + return elc_forces, elc_energy + + +@utx.skipIfMissingFeatures(["P3M"]) +class TestCPU(Test, ut.TestCase): + + p3m_class = espressomd.electrostatics.P3M + rtol = 1e-7 + + +@utx.skipIfMissingGPU() +@utx.skipIfMissingFeatures(["P3M"]) +class TestGPU(Test, ut.TestCase): + + p3m_class = espressomd.electrostatics.P3MGPU + rtol = 4e-6 if __name__ == "__main__": diff --git a/testsuite/python/electrostaticInteractions.py b/testsuite/python/electrostatic_interactions.py similarity index 84% rename from testsuite/python/electrostaticInteractions.py rename to testsuite/python/electrostatic_interactions.py index a9054f7ee3b..9847d60260c 100644 --- a/testsuite/python/electrostaticInteractions.py +++ b/testsuite/python/electrostatic_interactions.py @@ -81,7 +81,7 @@ def test_p3m(self): p3m_params = {'accuracy': 1e-7, 'mesh': [22, 22, 22], 'cao': 7, - 'r_cut': 8.906249999999998, + 'r_cut': 8.90625, 'alpha': 0.387611049779351} # reference values for energy and force calculated for prefactor = 1 @@ -132,6 +132,11 @@ def test_dh(self): r_cut=dh_params['r_cut']) self.system.actors.add(dh) + # actor should remain in a valid state after a cell system reset + self.system.box_l = self.system.box_l + self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid + dr = 0.001 r = np.arange(.5, 1.01 * dh_params['r_cut'], dr) u_dh = self.calc_dh_potential(r, dh_params) @@ -180,17 +185,11 @@ def test_dh_pure_coulomb(self): np.testing.assert_allclose(f_dh_core, -f_dh, atol=1e-7) def test_dh_exceptions(self): - dh = espressomd.electrostatics.DH(prefactor=-1.0, kappa=1.0, r_cut=1.0) - with self.assertRaisesRegex(ValueError, 'Coulomb prefactor has to be >= 0'): - self.system.actors.add(dh) - self.system.actors.clear() - dh = espressomd.electrostatics.DH(prefactor=1.0, kappa=-1.0, r_cut=1.0) - with self.assertRaisesRegex(ValueError, 'kappa should be a non-negative number'): - self.system.actors.add(dh) - self.system.actors.clear() - dh = espressomd.electrostatics.DH(prefactor=1.0, kappa=1.0, r_cut=-1.0) - with self.assertRaisesRegex(ValueError, 'r_cut should be a non-negative number'): - self.system.actors.add(dh) + params = dict(prefactor=1.0, kappa=1.0, r_cut=1.0) + for key in params: + invalid_params = {**params, key: -1.0} + with self.assertRaisesRegex(ValueError, f"Parameter '{key}' must be >=? 0"): + espressomd.electrostatics.DH(**invalid_params) def test_rf(self): """Tests the ReactionField coulomb interaction by comparing the @@ -207,7 +206,12 @@ def test_rf(self): epsilon1=rf_params['epsilon1'], epsilon2=rf_params['epsilon2'], r_cut=rf_params['r_cut']) + self.system.actors.add(rf) + # actor should remain in a valid state after a cell system reset + self.system.box_l = self.system.box_l + self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid dr = 0.001 r = np.arange(.5, 1.01 * rf_params['r_cut'], dr) @@ -234,21 +238,16 @@ def test_rf(self): np.testing.assert_allclose(f_rf_core, -f_rf, atol=1e-2) def test_rf_exceptions(self): - params = dict(kappa=1.0, epsilon1=1.0, epsilon2=2.0, r_cut=1.0) + params = dict( + prefactor=1.0, + kappa=1.0, + epsilon1=1.0, + epsilon2=2.0, + r_cut=1.0) for key in params: - invalid_params = {**params, 'prefactor': 1.0, key: -1.0} - rf = espressomd.electrostatics.ReactionField(**invalid_params) - with self.assertRaisesRegex(ValueError, f'{key} should be a non-negative number'): - self.system.actors.add(rf) - self.system.actors.clear() - - valid_actor = espressomd.electrostatics.ReactionField( - **params, prefactor=1.0) - with self.assertRaisesRegex(Exception, "chosen method does not support tuning"): - valid_actor.tune() - with self.assertRaisesRegex(ValueError, r"Only the following keys can be given as keyword arguments: " - r"\[.+\], got \[.+\] \(unknown \['coulomb_prefactor'\]\)"): - valid_actor.tune(coulomb_prefactor=1.0) + invalid_params = {**params, key: -1.0} + with self.assertRaisesRegex(ValueError, f"'{key}' must be >=? 0"): + espressomd.electrostatics.ReactionField(**invalid_params) if __name__ == "__main__": diff --git a/testsuite/python/icc.py b/testsuite/python/icc.py index 31827815690..a5f06d20cbd 100644 --- a/testsuite/python/icc.py +++ b/testsuite/python/icc.py @@ -21,10 +21,15 @@ import espressomd.electrostatic_extensions import numpy as np +BOX_L = 20. +BOX_SPACE = 5. + @utx.skipIfMissingFeatures(["ELECTROSTATICS", "EXTERNAL_FORCES"]) -class test_icc(ut.TestCase): - system = espressomd.System(box_l=[10, 10, 10]) +class TestICC(ut.TestCase): + system = espressomd.System(box_l=[BOX_L, BOX_L, BOX_L + BOX_SPACE]) + system.cell_system.skin = 0.4 + system.time_step = 0.01 def tearDown(self): self.system.actors.clear() @@ -59,62 +64,8 @@ def add_icc_particles(self, side_num_particles, return self.system.part.add( pos=positions, q=charges, fix=fix), normals, areas - def common_setup(self, kwargs, error): - part_slice, normals, areas = self.add_icc_particles(2, 0.01, 0) - - params = {"n_icc": len(part_slice), - "normals": normals, - "areas": areas, - "epsilons": np.ones_like(areas), - "first_id": part_slice.id[0], - "check_neutrality": False} - - params.update(kwargs) - - icc = espressomd.electrostatic_extensions.ICC(**params) - with self.assertRaisesRegex(Exception, error): - self.system.actors.add(icc) - - def test_params(self): - params = [({"n_icc": -1}, 'ICC: invalid number of particles'), - ({"first_id": -1}, 'ICC: invalid first_id'), - ({"max_iterations": -1}, 'ICC: invalid max_iterations'), - ({"convergence": -1}, 'ICC: invalid convergence value'), - ({"relaxation": -1}, 'ICC: invalid relaxation value'), - ({"relaxation": 2.1}, 'ICC: invalid relaxation value'), - ({"eps_out": -1}, 'ICC: invalid eps_out'), - ({"ext_field": 0}, 'A single value was given but 3 were expected'), ] - - for kwargs, error in params: - self.common_setup(kwargs, error) - self.tearDown() - - def test_core_params(self): - part_slice, normals, areas = self.add_icc_particles(5, 0.01, 0) - - params = {"n_icc": len(part_slice), - "normals": normals, - "areas": areas, - "epsilons": np.ones_like(areas), - "first_id": part_slice.id[0], - "check_neutrality": False} - - icc = espressomd.electrostatic_extensions.ICC(**params) - self.system.actors.add(icc) - - icc_params = icc.get_params() - for key, value in params.items(): - np.testing.assert_allclose(value, np.copy(icc_params[key])) - @utx.skipIfMissingFeatures(["P3M"]) def test_dipole_system(self): - BOX_L = 20. - BOX_SPACE = 5. - - self.system.box_l = [BOX_L, BOX_L, BOX_L + BOX_SPACE] - self.system.cell_system.skin = 0.4 - self.system.time_step = 0.01 - N_ICC_SIDE_LENGTH = 10 DIPOLE_DISTANCE = 5.0 DIPOLE_CHARGE = 10.0 @@ -154,11 +105,11 @@ def test_dipole_system(self): q=-DIPOLE_CHARGE, fix=[True, True, True]) p3m = espressomd.electrostatics.P3M( - prefactor=1, mesh=32, cao=7, accuracy=1e-5) + prefactor=1., mesh=32, cao=7, accuracy=1e-5) + p3m.charge_neutrality_tolerance = 1e-11 self.system.actors.add(p3m) self.system.actors.add(icc) - self.system.integrator.run(0) charge_lower = sum(part_slice_lower.q) diff --git a/testsuite/python/icc_interface.py b/testsuite/python/icc_interface.py new file mode 100644 index 00000000000..2b968d2432b --- /dev/null +++ b/testsuite/python/icc_interface.py @@ -0,0 +1,218 @@ +# +# Copyright (C) 2021-2022 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import unittest as ut +import unittest_decorators as utx +import espressomd +import espressomd.electrostatics +import espressomd.electrostatic_extensions +import numpy as np + + +@utx.skipIfMissingFeatures(["ELECTROSTATICS"]) +class Test(ut.TestCase): + system = espressomd.System(box_l=[20., 20., 20.]) + system.cell_system.skin = 0.4 + system.time_step = 0.01 + + def tearDown(self): + self.system.part.clear() + self.system.actors.clear() + self.system.thermostat.turn_off() + self.system.integrator.set_vv() + + def add_icc_particles(self): + pos = [[0., 0., 0.], [1., 0., 0.]] + q = [0.0001, -0.0001] + p_slice = self.system.part.add(pos=pos, q=q) + areas = self.system.box_l[0] * self.system.box_l[1] / 2. * np.ones(2) + normals = 2 * [(0., 0., 1.)] + return p_slice, normals, areas + + def setup_icc_particles_and_solver(self, **icc_params): + part_slice, normals, areas = self.add_icc_particles() + icc = espressomd.electrostatic_extensions.ICC( + n_icc=len(part_slice), + normals=normals, + areas=areas, + epsilons=np.ones_like(areas), + first_id=part_slice.id[0], + **icc_params + ) + return icc, part_slice + + def valid_p3m_parameters(self): + return {"prefactor": 1., "mesh": 32, "cao": 7, "accuracy": 1e-5, + "r_cut": 1.25625, "alpha": 1.50505, "tune": False, + "check_neutrality": False} + + def test_getters_and_setters(self): + part_slice, normals, areas = self.add_icc_particles() + + params = {"n_icc": len(part_slice), + "normals": normals, + "areas": areas, + "epsilons": np.ones_like(areas), + "first_id": part_slice.id[0]} + + icc = espressomd.electrostatic_extensions.ICC(**params) + icc_params = icc.get_params() + for key, value in params.items(): + np.testing.assert_allclose(value, np.copy(icc_params[key])) + with self.assertRaisesRegex(RuntimeError, f"Parameter '{key}' is read-only"): + setattr(icc, key, 5) + + def test_invalid_parameters(self): + part_slice, normals, areas = self.add_icc_particles() + + valid_params = {"n_icc": len(part_slice), + "normals": normals, + "areas": areas, + "epsilons": np.ones_like(areas), + "first_id": part_slice.id[0]} + + invalid_params = [({"n_icc": -1}, "Parameter 'n_icc' must be >= 1"), + ({"n_icc": 0}, "Parameter 'n_icc' must be >= 1"), + ({"first_id": -1}, "Parameter 'first_id' must be >= 0"), + ({"max_iterations": -1}, + "Parameter 'max_iterations' must be > 0"), + ({"convergence": -1.}, + "Parameter 'convergence' must be > 0"), + ({"relaxation": -0.1}, + "Parameter 'relaxation' must be >= 0 and <= 2"), + ({"relaxation": 2.1}, + "Parameter 'relaxation' must be >= 0 and <= 2"), + ({"eps_out": -1.}, "Parameter 'eps_out' must be > 0"), + ({"ext_field": 0.}, 'A single value was given but 3 were expected'), ] + + for kwargs, error in invalid_params: + params = valid_params.copy() + params.update(kwargs) + with self.assertRaisesRegex(ValueError, error): + espressomd.electrostatic_extensions.ICC(**params) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_exceptions_small_r_cut(self): + icc, _ = self.setup_icc_particles_and_solver(max_iterations=1) + p3m = espressomd.electrostatics.P3M( + prefactor=1., mesh=32, cao=7, accuracy=1e-5, r_cut=0.01875, + alpha=0.005, tune=False, check_neutrality=False) + self.system.actors.add(p3m) + self.system.actors.add(icc) + + with self.assertRaisesRegex(Exception, "ICC found zero electric field on a charge"): + self.system.integrator.run(0) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_exceptions_large_r_cut(self): + icc, (_, p) = self.setup_icc_particles_and_solver(max_iterations=1) + p3m = espressomd.electrostatics.P3M(**self.valid_p3m_parameters()) + + self.system.actors.add(p3m) + self.system.actors.add(icc) + + with self.assertRaisesRegex(Exception, f"Particle with id {p.id} has a charge .+ that is too large for the ICC algorithm"): + p.q = 1e9 + self.system.integrator.run(0) + with self.assertRaisesRegex(Exception, "ICC failed to converge in the given number of maximal steps"): + p.q = 0. + self.system.integrator.run(0) + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["P3M"]) + def test_exceptions_gpu(self): + icc, _ = self.setup_icc_particles_and_solver() + p3m = espressomd.electrostatics.P3MGPU(**self.valid_p3m_parameters()) + + self.system.actors.add(p3m) + with self.assertRaisesRegex(RuntimeError, "ICC does not work with P3MGPU"): + self.system.actors.add(icc) + self.assertEqual(len(self.system.actors), 1) + self.system.integrator.run(0) + + self.system.actors.clear() + elc = espressomd.electrostatics.ELC( + actor=p3m, gap_size=5., maxPWerror=1e-3) + self.system.actors.add(elc) + with self.assertRaisesRegex(RuntimeError, "ICC does not work with P3MGPU"): + self.system.actors.add(icc) + self.assertEqual(len(self.system.actors), 1) + self.system.integrator.run(0) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_exceptions_elc(self): + icc, _ = self.setup_icc_particles_and_solver() + p3m = espressomd.electrostatics.P3M(**self.valid_p3m_parameters()) + elc = espressomd.electrostatics.ELC( + actor=p3m, gap_size=5., maxPWerror=1e-3, pot_diff=-3., + delta_mid_top=-1., delta_mid_bot=-1., const_pot=True) + + self.system.actors.add(elc) + with self.assertRaisesRegex(RuntimeError, "ICC conflicts with ELC dielectric contrast"): + self.system.actors.add(icc) + self.assertEqual(len(self.system.actors), 1) + self.system.integrator.run(0) + self.system.actors.clear() + + # valid ELC actor should pass sanity checks + elc = espressomd.electrostatics.ELC( + actor=p3m, gap_size=5., maxPWerror=1e-3) + self.system.actors.add(elc) + self.system.actors.add(icc) + self.system.part.clear() + self.system.integrator.run(0) + + def test_exceptions_dh(self): + icc, _ = self.setup_icc_particles_and_solver() + solver = espressomd.electrostatics.DH( + prefactor=2., kappa=0.8, r_cut=1.2) + + self.system.actors.add(solver) + with self.assertRaisesRegex(RuntimeError, "ICC does not work with DebyeHueckel"): + self.system.actors.add(icc) + self.assertEqual(len(self.system.actors), 1) + self.system.integrator.run(0) + + def test_exceptions_rf(self): + icc, _ = self.setup_icc_particles_and_solver() + solver = espressomd.electrostatics.ReactionField( + prefactor=1., kappa=2., epsilon1=1., epsilon2=2., r_cut=2.) + + self.system.actors.add(solver) + with self.assertRaisesRegex(RuntimeError, "ICC does not work with ReactionField"): + self.system.actors.add(icc) + self.assertEqual(len(self.system.actors), 1) + self.system.integrator.run(0) + + @utx.skipIfMissingFeatures(["NPT", "P3M"]) + def test_exceptions_npt(self): + icc, _ = self.setup_icc_particles_and_solver() + p3m = espressomd.electrostatics.P3M(**self.valid_p3m_parameters()) + + self.system.actors.add(p3m) + self.system.thermostat.set_npt(kT=1., gamma0=2., gammav=0.004, seed=42) + self.system.integrator.set_isotropic_npt(ext_pressure=2., piston=0.001) + with self.assertRaisesRegex(RuntimeError, "ICC does not work in the NPT ensemble"): + self.system.actors.add(icc) + self.assertEqual(len(self.system.actors), 1) + self.system.integrator.run(0) + + +if __name__ == "__main__": + ut.main() diff --git a/testsuite/python/integrator_npt.py b/testsuite/python/integrator_npt.py index 3e9eaa44427..bab6aab1746 100644 --- a/testsuite/python/integrator_npt.py +++ b/testsuite/python/integrator_npt.py @@ -56,11 +56,11 @@ def test_integrator_exceptions(self): self.system.integrator.set_isotropic_npt( ext_pressure=1, piston=1, direction=[True, False]) - def test_integrator_recovery(self): + def test_00_integrator_recovery(self): # the system is still in a valid state after a failure system = self.system np.random.seed(42) - npt_params = {'ext_pressure': 0.001, 'piston': 0.001} + npt_params = {'ext_pressure': 0.01, 'piston': 0.001} system.box_l = [6] * 3 system.part.add(pos=np.random.uniform(0, system.box_l[0], (11, 3))) system.non_bonded_inter[0, 0].lennard_jones.set_params( @@ -69,7 +69,10 @@ def test_integrator_recovery(self): system.integrator.set_isotropic_npt(**npt_params) # get the equilibrium box length for the chosen NpT parameters - system.integrator.run(2000) + system.integrator.run(500) + # catch unstable simulation early (when the DP3M test case ran first) + assert system.box_l[0] < 20. + system.integrator.run(1500) box_l_ref = system.box_l[0] # resetting the NpT integrator with incorrect values doesn't leave the @@ -123,8 +126,7 @@ def run_with_p3m(self, p3m, **npt_kwargs): # set up particles system.box_l = [6] * 3 partcl = system.part.add( - pos=np.random.uniform( - 0, system.box_l[0], (11, 3))) + pos=np.random.uniform(0, system.box_l[0], (11, 3))) if espressomd.has_features("P3M"): partcl.q = np.sign(np.arange(-5, 6)) if espressomd.has_features("DP3M"): @@ -137,15 +139,14 @@ def run_with_p3m(self, p3m, **npt_kwargs): system.integrator.set_vv() # combine NpT with a P3M algorithm system.actors.add(p3m) - system.integrator.run(20) + system.integrator.run(10) system.integrator.set_isotropic_npt(ext_pressure=0.001, piston=0.001, **npt_kwargs) system.thermostat.set_npt(kT=1.0, gamma0=2, gammav=0.04, seed=42) - system.integrator.run(20) + system.integrator.run(10) @utx.skipIfMissingFeatures(["DP3M"]) - def test_dp3m_exception(self): - # NpT is compatible with DP3M CPU (only cubic box) + def test_npt_dp3m_cpu(self): import espressomd.magnetostatics dp3m = espressomd.magnetostatics.DipolarP3M( prefactor=1.0, accuracy=1e-2, mesh=3 * [36], cao=7, r_cut=1.0, @@ -158,8 +159,7 @@ def test_dp3m_exception(self): self.run_with_p3m(dp3m) @utx.skipIfMissingFeatures(["P3M"]) - def test_p3m_exception(self): - # NpT is compatible with P3M CPU (only cubic box) + def test_npt_p3m_cpu(self): import espressomd.electrostatics p3m = espressomd.electrostatics.P3M( prefactor=1.0, accuracy=1e-2, mesh=3 * [8], cao=3, r_cut=0.36, @@ -173,14 +173,17 @@ def test_p3m_exception(self): @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures(["P3M"]) - def test_p3mgpu_exception(self): - # NpT is not compatible with P3M GPU (no energies) + def test_npt_p3m_gpu(self): import espressomd.electrostatics p3m = espressomd.electrostatics.P3MGPU( - prefactor=1.0, accuracy=1e-2, mesh=3 * [24], cao=2, r_cut=0.24, - alpha=8.26, tune=False) - with self.assertRaisesRegex(RuntimeError, 'NpT virial cannot be calculated on P3M GPU'): - self.run_with_p3m(p3m) + prefactor=1.0, accuracy=1e-2, mesh=3 * [8], cao=3, r_cut=0.36, + alpha=5.35, tune=False) + with self.assertRaisesRegex(RuntimeError, 'If electrostatics is being used you must use the cubic box NpT'): + self.run_with_p3m( + p3m, cubic_box=False, direction=(False, True, True)) + self.tearDown() + # should not raise an exception + self.run_with_p3m(p3m) if __name__ == "__main__": diff --git a/testsuite/python/long_range_actors.py b/testsuite/python/long_range_actors.py new file mode 100644 index 00000000000..1682c16059b --- /dev/null +++ b/testsuite/python/long_range_actors.py @@ -0,0 +1,358 @@ +# +# Copyright (C) 2021-2022 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import espressomd +import espressomd.magnetostatics +import espressomd.electrostatics +import espressomd.electrostatic_extensions + +import numpy as np +import itertools +import unittest as ut +import unittest_decorators as utx + + +class Test(ut.TestCase): + system = espressomd.System(box_l=[20., 20., 20.]) + system.cell_system.skin = 0.4 + system.time_step = 0.01 + n_nodes = system.cell_system.get_state()["n_nodes"] + original_node_grid = tuple(system.cell_system.node_grid) + + def setUp(self): + self.system.box_l = [20., 20., 20.] + self.system.cell_system.node_grid = self.original_node_grid + + def tearDown(self): + self.system.part.clear() + self.system.actors.clear() + self.system.thermostat.turn_off() + self.system.integrator.set_vv() + self.system.periodicity = [1, 1, 1] + self.system.cell_system.set_regular_decomposition() + + def add_charged_particles(self): + self.system.part.add(pos=[[0., 0., 0.], [0.5, 0.5, 0.5]], q=[-1., 1.]) + + def add_magnetic_particles(self): + self.system.part.add(pos=[[0.01, 0.01, 0.01], [0.5, 0.5, 0.5]], + dip=[(1., 0., 0.), (-1., 0., 0.)], + rotation=2 * [(True, True, True)]) + + def add_icc_particles(self): + pos = [[0., 0., 0.], [1., 0., 0.]] + q = [0.0001, -0.0001] + p_slice = self.system.part.add(pos=pos, q=q) + areas = self.system.box_l[0] * self.system.box_l[1] / 2. * np.ones(2) + normals = 2 * [(0., 0., 1.)] + return p_slice, normals, areas + + def setup_icc_particles_and_solver(self): + part_slice, normals, areas = self.add_icc_particles() + icc = espressomd.electrostatic_extensions.ICC( + n_icc=len(part_slice), + normals=normals, + areas=areas, + epsilons=np.ones_like(areas), + first_id=part_slice.id[0], + max_iterations=100, + check_neutrality=False, + ) + return icc, part_slice + + def valid_p3m_parameters(self): + return {"prefactor": 1., "mesh": 32, "cao": 7, "accuracy": 1e-5, + "r_cut": 1.25625, "alpha": 1.50505, "tune": False, + "check_neutrality": False} + + def valid_dp3m_parameters(self): + return {"prefactor": 1., "mesh": 26, "cao": 7, "accuracy": 1e-4, + "r_cut": 4.80, "alpha": 0.62626, "tune": False} + + @utx.skipIfMissingFeatures(["P3M"]) + def test_electrostatics_registration(self): + import espressomd.highlander + icc, _ = self.setup_icc_particles_and_solver() + p3m = espressomd.electrostatics.P3M(**self.valid_p3m_parameters()) + dh = espressomd.electrostatics.DH( + prefactor=2., kappa=0.8, r_cut=1.2) + + self.assertIsNone(p3m.call_method("unknown")) + + with self.assertRaisesRegex(RuntimeError, "The given electrostatics solver is not currently active"): + p3m._deactivate() + with self.assertRaisesRegex(RuntimeError, "The given electrostatics extension is not currently active"): + icc._deactivate() + + with self.assertRaisesRegex(RuntimeError, "An electrostatics solver is needed by ICC"): + self.system.actors.add(icc) + + self.system.actors.add(p3m) + self.system.actors.add(icc) + + with self.assertRaises(espressomd.highlander.ThereCanOnlyBeOne): + self.system.actors.add(p3m) + with self.assertRaisesRegex(RuntimeError, r"An electrostatics solver is already active \(CoulombP3M\)"): + self.system.actors.add(dh) + + with self.assertRaises(espressomd.highlander.ThereCanOnlyBeOne): + self.system.actors.add(icc) + with self.assertRaisesRegex(RuntimeError, r"An electrostatics extension is already active \(ICCStar\)"): + icc_new, _ = self.setup_icc_particles_and_solver() + self.system.actors.add(icc_new) + + with self.assertRaisesRegex(Exception, r"An electrostatics solver is needed by ICC"): + self.system.actors.remove(p3m) + self.system.integrator.run(0) + + @utx.skipIfMissingFeatures(["DP3M"]) + def test_magnetostatics_registration(self): + import espressomd.highlander + dp3m = espressomd.magnetostatics.DipolarP3M( + **self.valid_dp3m_parameters()) + + self.assertIsNone(dp3m.call_method("unknown")) + + with self.assertRaisesRegex(RuntimeError, "The given magnetostatics solver is not currently active"): + dp3m._deactivate() + + self.system.actors.add(dp3m) + + with self.assertRaises(espressomd.highlander.ThereCanOnlyBeOne): + self.system.actors.add(dp3m) + with self.assertRaisesRegex(RuntimeError, r"A magnetostatics solver is already active \(DipolarP3M\)"): + dp3m_new = espressomd.magnetostatics.DipolarP3M( + **self.valid_dp3m_parameters()) + self.system.actors.add(dp3m_new) + + def check_obs_stats(self, key): + # check observable statistics have the correct shape + pressure_tensor = self.system.analysis.pressure_tensor() + pressure_scalar = self.system.analysis.pressure() + energies_scalar = self.system.analysis.energy() + for key in [key, (key, 0), (key, 1)]: + self.assertEqual(np.shape(energies_scalar[key]), ()) + self.assertEqual(np.shape(pressure_scalar[key]), ()) + self.assertEqual(np.shape(pressure_tensor[key]), (3, 3)) + self.assertAlmostEqual(pressure_scalar[key], + np.trace(pressure_tensor[key]) / 3., + delta=1e-12) + return pressure_tensor, pressure_scalar + + @utx.skipIfMissingFeatures(["DP3M"]) + def test_dp3m_cpu_pressure(self): + dp3m = espressomd.magnetostatics.DipolarP3M( + **self.valid_dp3m_parameters()) + self.system.actors.add(dp3m) + pressure_tensor, pressure_scalar = self.check_obs_stats("dipolar") + # DP3M doesn't contribute to the pressure + np.testing.assert_allclose(pressure_tensor["dipolar"], 0., atol=1e-12) + np.testing.assert_allclose(pressure_scalar["dipolar"], 0., atol=1e-12) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_p3m_cpu_pressure(self): + self.add_charged_particles() + p3m = espressomd.electrostatics.P3M(**self.valid_p3m_parameters()) + self.system.actors.add(p3m) + self.check_obs_stats("coulomb") + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["P3M"]) + def test_p3m_gpu_pressure(self): + self.add_charged_particles() + p3m = espressomd.electrostatics.P3MGPU(**self.valid_p3m_parameters()) + self.system.actors.add(p3m) + self.check_obs_stats("coulomb") + + @utx.skipIfMissingFeatures(["P3M"]) + def test_elc_cpu_pressure(self): + self.add_charged_particles() + p3m = espressomd.electrostatics.P3M(**self.valid_p3m_parameters()) + elc = espressomd.electrostatics.ELC( + actor=p3m, gap_size=2., maxPWerror=1e-3, check_neutrality=False) + self.system.actors.add(elc) + pressure_tensor, pressure_scalar = self.check_obs_stats("coulomb") + # ELC doesn't contribute to the pressure + pressure_tensor_far_field = pressure_tensor[("coulomb", 1)] + pressure_scalar_far_field = pressure_scalar[("coulomb", 1)] + pressure_tensor_near_field = pressure_tensor[("coulomb", 0)] + pressure_scalar_near_field = pressure_scalar[("coulomb", 0)] + np.testing.assert_allclose(pressure_tensor_far_field, 0., atol=1e-12) + np.testing.assert_allclose(pressure_scalar_far_field, 0., atol=1e-12) + np.testing.assert_allclose(pressure_tensor_near_field, 0., atol=1e-12) + np.testing.assert_allclose(pressure_scalar_near_field, 0., atol=1e-12) + + @utx.skipIfMissingFeatures(["ELECTROSTATICS"]) + def test_rf_pressure(self): + self.add_charged_particles() + actor = espressomd.electrostatics.ReactionField( + prefactor=1., kappa=2., epsilon1=1., epsilon2=2., r_cut=2.) + self.system.actors.add(actor) + pressure_tensor, pressure_scalar = self.check_obs_stats("coulomb") + # actor doesn't contribute to the pressure in the far field + pressure_tensor_far_field = pressure_tensor[("coulomb", 1)] + pressure_scalar_far_field = pressure_scalar[("coulomb", 1)] + np.testing.assert_allclose(pressure_tensor_far_field, 0., atol=1e-12) + np.testing.assert_allclose(pressure_scalar_far_field, 0., atol=1e-12) + + @utx.skipIfMissingFeatures(["ELECTROSTATICS"]) + def test_dh_pressure(self): + self.add_charged_particles() + actor = espressomd.electrostatics.DH(prefactor=1., kappa=1., r_cut=1.) + self.system.actors.add(actor) + pressure_tensor, pressure_scalar = self.check_obs_stats("coulomb") + # actor doesn't contribute to the pressure in the far field + pressure_tensor_far_field = pressure_tensor[("coulomb", 1)] + pressure_scalar_far_field = pressure_scalar[("coulomb", 1)] + np.testing.assert_allclose(pressure_tensor_far_field, 0., atol=1e-12) + np.testing.assert_allclose(pressure_scalar_far_field, 0., atol=1e-12) + + @utx.skipIfMissingFeatures(["DIPOLES"]) + @ut.skipIf(n_nodes > 1, "only runs for 1 MPI rank") + def test_mdds_cpu_no_magnetic_particles(self): + self.system.part.add(pos=2 * [[1., 1., 1.]], dip=2 * [[0., 0., 0.]]) + mdds = espressomd.magnetostatics.DipolarDirectSumCpu(prefactor=2.) + self.system.actors.add(mdds) + energy = self.system.analysis.energy() + self.assertAlmostEqual(energy["dipolar"], 0., delta=1e-12) + + def check_p3m_pre_conditions(self, class_p3m): + params = {"prefactor": 1., "accuracy": 1., "r_cut": 1., "alpha": 1.} + + # P3M pre-condition: cao / mesh[i] < 1 + with self.assertRaisesRegex(RuntimeError, "k-space cutoff .+ is larger than half of box dimension"): + self.system.actors.add( + class_p3m(cao=6, mesh=6, tune=False, **params)) + + # P3M pre-condition: cao / mesh[i] < 2 / n_nodes[i] + with self.assertRaisesRegex(RuntimeError, "k-space cutoff .+ is larger than local box dimension"): + self.system.cell_system.node_grid = [self.n_nodes, 1, 1] + self.system.actors.add( + class_p3m(cao=7, mesh=8, tune=False, **params)) + + @utx.skipIfMissingFeatures(["P3M"]) + @ut.skipIf(n_nodes < 3, "only runs for 3+ MPI ranks") + def test_p3m_cpu_pre_condition_exceptions(self): + self.add_charged_particles() + self.check_p3m_pre_conditions(espressomd.electrostatics.P3M) + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["P3M"]) + @ut.skipIf(n_nodes < 3, "only runs for 3+ MPI ranks") + def test_p3m_gpu_pre_condition_exceptions(self): + self.add_charged_particles() + self.check_p3m_pre_conditions(espressomd.electrostatics.P3MGPU) + + @utx.skipIfMissingFeatures(["DP3M"]) + @ut.skipIf(n_nodes < 3, "only runs for 3+ MPI ranks") + def test_dp3m_cpu_pre_condition_exceptions(self): + self.add_magnetic_particles() + self.check_p3m_pre_conditions(espressomd.magnetostatics.DipolarP3M) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_p3m_cpu_tuning_accuracy_exception(self): + self.add_charged_particles() + p3m = espressomd.electrostatics.P3M(prefactor=1., mesh=8, alpha=60., + r_cut=1.25625, accuracy=1e-5) + with self.assertRaisesRegex(RuntimeError, "failed to reach requested accuracy"): + self.system.actors.add(p3m) + self.assertFalse(p3m.is_tuned) + self.assertEqual(len(self.system.actors), 0) + + def check_p3m_tuning_errors(self, p3m): + # set an incompatible combination of thermostat and integrators + self.system.integrator.set_isotropic_npt(ext_pressure=2., piston=0.01) + self.system.thermostat.set_brownian(kT=1.0, gamma=1.0, seed=42) + with self.assertRaisesRegex(RuntimeError, r"tuning failed: an exception was thrown while benchmarking the integration loop"): + self.system.actors.add(p3m) + self.assertFalse(p3m.is_tuned) + self.assertEqual(len(self.system.actors), 0) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_p3m_cpu_tuning_errors(self): + self.add_charged_particles() + p3m = espressomd.electrostatics.P3M(prefactor=1., accuracy=1e-3) + self.check_p3m_tuning_errors(p3m) + + @utx.skipIfMissingFeatures(["DP3M"]) + def test_dp3m_cpu_tuning_errors(self): + self.add_magnetic_particles() + dp3m = espressomd.magnetostatics.DipolarP3M( + prefactor=1., accuracy=1e-3) + self.check_p3m_tuning_errors(dp3m) + + def check_mmm1d_exceptions(self, mmm1d_class): + mmm1d = mmm1d_class(prefactor=1., maxPWerror=1e-2) + + # check cell system exceptions + with self.assertRaisesRegex(Exception, "MMM1D requires the N-square cellsystem"): + self.system.cell_system.set_regular_decomposition() + self.system.actors.add(mmm1d) + self.assertEqual(len(self.system.actors), 0) + self.assertFalse(mmm1d.is_tuned) + self.system.cell_system.set_n_square() + + # check periodicity exceptions + for periodicity in itertools.product(range(2), range(2), range(2)): + if periodicity == (0, 0, 1): + continue + self.system.periodicity = periodicity + with self.assertRaisesRegex(Exception, r"MMM1D requires periodicity \(0, 0, 1\)"): + mmm1d = mmm1d_class(prefactor=1., maxPWerror=1e-2) + self.system.actors.add(mmm1d) + self.assertEqual(len(self.system.actors), 0) + self.assertFalse(mmm1d.is_tuned) + self.system.periodicity = (0, 0, 1) + + @utx.skipIfMissingFeatures(["ELECTROSTATICS"]) + def test_mmm1d_cpu_exceptions(self): + self.system.periodicity = (0, 0, 1) + self.check_mmm1d_exceptions(espressomd.electrostatics.MMM1D) + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures(["MMM1D_GPU"]) + def test_mmm1d_gpu_exceptions(self): + self.system.periodicity = (0, 0, 1) + self.check_mmm1d_exceptions(espressomd.electrostatics.MMM1DGPU) + + with self.assertRaisesRegex(ValueError, "switching radius must not be larger than box length"): + espressomd.electrostatics.MMM1DGPU( + prefactor=1., maxPWerror=1e-2, + far_switch_radius=2. * self.system.box_l[2]) + + @utx.skipIfMissingFeatures(["P3M"]) + def test_elc_tuning_exceptions(self): + p3m = espressomd.electrostatics.P3M(**self.valid_p3m_parameters()) + elc = espressomd.electrostatics.ELC( + actor=p3m, + gap_size=2., + maxPWerror=1e-3, + delta_mid_top=-1., + delta_mid_bot=-1., + const_pot=True, + pot_diff=-3, + check_neutrality=False, + ) + self.system.part.add(pos=[0., 0., 0.], q=1.) + with self.assertRaisesRegex(RuntimeError, "ELC does not currently support non-neutral systems"): + self.system.actors.add(elc) + + +if __name__ == "__main__": + ut.main() diff --git a/testsuite/python/mmm1d.py b/testsuite/python/mmm1d.py index c0c7f6f624b..68b33174fc2 100644 --- a/testsuite/python/mmm1d.py +++ b/testsuite/python/mmm1d.py @@ -17,7 +17,6 @@ # along with this program. If not, see . # import numpy as np -import itertools import unittest as ut import unittest_decorators as utx import tests_common @@ -25,7 +24,6 @@ class ElectrostaticInteractionsTests: - MMM1D = None # Handle to espresso system system = espressomd.System(box_l=[10.0] * 3) @@ -41,9 +39,8 @@ class ElectrostaticInteractionsTests: forces_target = data[:, 5:8] energy_target = -7.156365298205383 - allowed_error = 2e-5 - def setUp(self): + self.system.box_l = [10.0] * 3 self.system.periodicity = [0, 0, 1] self.system.cell_system.set_n_square() @@ -89,7 +86,8 @@ def test_with_analytical_result(self): self.system.part.add(pos=[0, 0, 1], q=-1) mmm1d = self.MMM1D(prefactor=1.0, maxPWerror=1e-20) self.system.actors.add(mmm1d) - self.system.integrator.run(steps=0) + self.assertTrue(mmm1d.is_tuned) + self.system.integrator.run(steps=0, recalc_forces=True) self.check_with_analytical_result(prefactor=1.0, accuracy=0.0004) def test_bjerrum_length_change(self): @@ -97,35 +95,62 @@ def test_bjerrum_length_change(self): self.system.part.add(pos=[0, 0, 1], q=-1) mmm1d = self.MMM1D(prefactor=2.0, maxPWerror=1e-20) self.system.actors.add(mmm1d) - self.system.integrator.run(steps=0) + self.assertTrue(mmm1d.is_tuned) + self.system.integrator.run(steps=0, recalc_forces=True) self.check_with_analytical_result(prefactor=2.0, accuracy=0.0017) - def test_exceptions(self): - # check periodicity exceptions - for periodicity in itertools.product(range(2), range(2), range(2)): - if periodicity == (0, 0, 1): - continue - self.system.periodicity = periodicity - with self.assertRaisesRegex(Exception, r"MMM1D requires periodicity \(0, 0, 1\)"): - mmm1d = self.MMM1D(prefactor=1.0, maxPWerror=1e-2) - self.system.actors.add(mmm1d) - self.system.periodicity = (0, 0, 1) - self.system.actors.clear() - if self.MMM1D is espressomd.electrostatics.MMM1D: - with self.assertRaisesRegex(Exception, "MMM1D requires the N-square cellsystem"): - mmm1d = self.MMM1D(prefactor=1.0, maxPWerror=1e-2) - self.system.cell_system.set_regular_decomposition() - self.system.actors.add(mmm1d) - self.system.cell_system.set_n_square() - self.system.actors.clear() + # actor should remain in a valid state after a cell system reset + forces1 = np.copy(self.system.part.all().f) + self.system.box_l = self.system.box_l + self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid + self.system.integrator.run(steps=0, recalc_forces=True) + forces2 = np.copy(self.system.part.all().f) + np.testing.assert_allclose(forces1, forces2, atol=1e-12, rtol=0.) + + def test_infinite_wire(self): + """ + For an infinite wire, the energy per ion is :math:`MC\\frac{q}{a}` + with :math:`M = - \\ln{2}` the 1D Madelung constant, :math:`C` + the electrostatics prefactor, :math:`q` the ion charge and + :math:`a` the lattice constant. Likewise, the pressure for + one ion can be derived as :math:`MC\\frac{q}{aV}` with + :math:`V` the simulation box volume. For more details, see + Orion Ciftja, "Equivalence of an infinite one-dimensional ionic + crystal to a simple electrostatic model", Results in Physics, + Volume 13, 2019, 102325, doi:10.1016/j.rinp.2019.102325 + """ + n_pairs = 128 + n_part = 2 * n_pairs + self.system.box_l = 3 * [n_part] + for i in range(n_pairs): + self.system.part.add(pos=[0., 0., 2. * i + 0.], q=+1.) + self.system.part.add(pos=[0., 0., 2. * i + 1.], q=-1.) + self.system.actors.add(self.MMM1D( + prefactor=1., maxPWerror=1e-20, far_switch_radius=n_pairs / 2.)) + energy = self.system.analysis.energy()["coulomb"] + p_scalar = self.system.analysis.pressure()["coulomb"] + p_tensor = self.system.analysis.pressure_tensor()["coulomb"] + ref_energy = -np.log(2.) + np.testing.assert_allclose(energy / n_part, ref_energy, + atol=0., rtol=5e-7) + np.testing.assert_allclose(p_scalar, 0., atol=1e-12) + np.testing.assert_allclose(p_tensor, 0., atol=1e-12) @utx.skipIfMissingFeatures(["ELECTROSTATICS"]) class MMM1D_Test(ElectrostaticInteractionsTests, ut.TestCase): - def setUp(self): - self.MMM1D = espressomd.electrostatics.MMM1D - super().setUp() + allowed_error = 2e-5 + MMM1D = espressomd.electrostatics.MMM1D + + +@utx.skipIfMissingFeatures(["ELECTROSTATICS", "MMM1D_GPU"]) +@utx.skipIfMissingGPU() +class MMM1D_GPU_Test(ElectrostaticInteractionsTests, ut.TestCase): + + allowed_error = 1e-4 + MMM1D = espressomd.electrostatics.MMM1DGPU if __name__ == "__main__": diff --git a/testsuite/python/p3m_electrostatic_pressure.py b/testsuite/python/p3m_electrostatic_pressure.py index 0e0cc065660..a48fddc527a 100644 --- a/testsuite/python/p3m_electrostatic_pressure.py +++ b/testsuite/python/p3m_electrostatic_pressure.py @@ -59,7 +59,6 @@ def measure_pressure_via_volume_scaling(self): def get_result(self): average_value = np.mean(self.list_of_previous_values) - pressure = self.kbT / self.dV * np.log(average_value) return pressure @@ -116,19 +115,20 @@ def setUp(self): self.system.integrator.set_vv() self.system.thermostat.set_langevin(kT=self.kT, gamma=1.0, seed=41) - def test_p3m_pressure(self): - pressures_via_virial = [] - p3m = espressomd.electrostatics.P3M( - prefactor=2.0, - accuracy=1e-3, - mesh=16, - cao=6, - r_cut=1.4941e-01 * self.system.box_l[0]) + def tearDown(self): + self.system.part.clear() + self.system.actors.clear() + self.system.thermostat.turn_off() + self.system.integrator.set_vv() + + def check_p3m_pressure(self, class_p3m): + p3m = class_p3m(prefactor=2., accuracy=1e-3, mesh=16, cao=6, r_cut=7.5) self.system.actors.add(p3m) skin = self.system.cell_system.tune_skin( min_skin=0.0, max_skin=2.5, tol=0.05, int_steps=100) print(f"Tuned skin: {skin}") + pressures_via_virial = [] num_samples = 25 pressure_via_volume_scaling = pressureViaVolumeScaling( self.system, self.kT) @@ -143,6 +143,13 @@ def test_p3m_pressure(self): pressure_virial / pressure_via_volume_scaling.get_result() - 1.0) np.testing.assert_array_less(abs_deviation_in_percent, 5.0) + def test_pressure_p3m_cpu(self): + self.check_p3m_pressure(espressomd.electrostatics.P3M) + + @utx.skipIfMissingGPU() + def test_pressure_p3m_gpu(self): + self.check_p3m_pressure(espressomd.electrostatics.P3MGPU) + if __name__ == "__main__": ut.main() diff --git a/testsuite/python/p3m_fft.py b/testsuite/python/p3m_fft.py index 872a4ec59b8..79f4e0e2f63 100644 --- a/testsuite/python/p3m_fft.py +++ b/testsuite/python/p3m_fft.py @@ -108,7 +108,7 @@ def test_unsorted_node_grid_exception_p3m(self): unsorted_node_grid = self.system.cell_system.node_grid[::-1] self.system.cell_system.node_grid = unsorted_node_grid solver = espressomd.electrostatics.P3M(prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: P3M_init: node grid must be sorted, largest first'): + with self.assertRaisesRegex(Exception, 'node grid must be sorted, largest first'): self.system.actors.add(solver) @utx.skipIfMissingFeatures("DP3M") @@ -121,7 +121,7 @@ def test_unsorted_node_grid_exception_dp3m(self): self.system.cell_system.node_grid = unsorted_node_grid solver = espressomd.magnetostatics.DipolarP3M( prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: dipolar P3M_init: node grid must be sorted, largest first'): + with self.assertRaisesRegex(Exception, 'node grid must be sorted, largest first'): self.system.actors.add(solver) diff --git a/testsuite/python/p3m_gpu.py b/testsuite/python/p3m_gpu.py deleted file mode 100644 index d0c8b466370..00000000000 --- a/testsuite/python/p3m_gpu.py +++ /dev/null @@ -1,48 +0,0 @@ -# -# Copyright (C) 2013-2019 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -import espressomd -import espressomd.electrostatics -import unittest as ut -import unittest_decorators as utx -import tests_common - - -@utx.skipIfMissingGPU() -@utx.skipIfMissingFeatures("ELECTROSTATICS") -class P3MGPU_test(ut.TestCase): - - def test(self): - system = espressomd.System(box_l=[10.0, 10.0, 10.0]) - test_params = {} - test_params["prefactor"] = 2 - test_params["cao"] = 2 - test_params["r_cut"] = 0.9 - test_params["accuracy"] = 1e-1 - test_params["mesh"] = [10, 10, 10] - test_params["epsilon"] = 20.0 - test_params["alpha"] = 1.1 - test_params["tune"] = False - - p3m = espressomd.electrostatics.P3MGPU(**test_params) - system.actors.add(p3m) - tests_common.assert_params_match(self, test_params, p3m.get_params()) - - -if __name__ == "__main__": - ut.main() diff --git a/testsuite/python/p3m_tuning_exceptions.py b/testsuite/python/p3m_tuning_exceptions.py index 85e9c2b7a3b..9a9d2962cb5 100644 --- a/testsuite/python/p3m_tuning_exceptions.py +++ b/testsuite/python/p3m_tuning_exceptions.py @@ -17,15 +17,37 @@ # along with this program. If not, see . # import espressomd +import espressomd.utils +import espressomd.electrostatics +import espressomd.magnetostatics import unittest as ut import unittest_decorators as utx import itertools +import numpy as np -class P3M_tuning_test(ut.TestCase): +class Test(ut.TestCase): system = espressomd.System(box_l=[10., 10., 10.]) + # tune parameters that are valid for all CPU methods + valid_params = { + 'P3MGPU': + {'cao': 2, 'r_cut': 3.1836, 'accuracy': 0.01, 'mesh': [8, 8, 8], + 'mesh_off': [0.5, 0.5, 0.5], 'prefactor': 2.0, 'alpha': 0.5115}, + 'P3M': + {'cao': 2, 'r_cut': 3.1836, 'accuracy': 0.01, 'mesh': [8, 8, 8], + 'mesh_off': [0.5, 0.5, 0.5], 'prefactor': 2.0, 'alpha': 0.5115}, + 'DP3M': + {'cao': 1, 'r_cut': 3.28125, 'accuracy': 0.01, 'mesh': [5, 5, 5], + 'mesh_off': [0.5, 0.5, 0.5], 'prefactor': 2.0, 'alpha': 0.455}, + } + + def get_valid_params(self, key, **custom_params): + params = self.valid_params[key].copy() + params.update(custom_params) + return params + def setUp(self): self.system.box_l = [10., 10., 10.] self.system.periodicity = [True, True, True] @@ -33,13 +55,16 @@ def setUp(self): def tearDown(self): self.system.actors.clear() self.system.part.clear() + # assertion: there should be no pending runtime error + espressomd.utils.handle_errors("tearDown") def add_charged_particles(self): - self.system.part.add(pos=[[0, 0, 0], [.5, .5, .5]], q=[-1, 1]) + self.system.part.add(pos=[[0., 0., 0.], [0.5, 0.5, 0.5]], q=[-1., 1.]) def add_magnetic_particles(self): - self.system.part.add(pos=[[0.01, 0.01, 0.01], [.5, .5, .5]], - rotation=2 * [(1, 1, 1)], dip=2 * [(1, 0, 0)]) + self.system.part.add(pos=[[0.01, 0.01, 0.01], [0.5, 0.5, 0.5]], + dip=[(1., 0., 0.), (-1., 0., 0.)], + rotation=2 * [(True, True, True)]) ################################################## # block of tests where the time_step is negative # @@ -48,33 +73,27 @@ def add_magnetic_particles(self): @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures("P3M") def test_01_time_not_set_p3m_gpu(self): - import espressomd.electrostatics - self.add_charged_particles() solver = espressomd.electrostatics.P3MGPU(prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: time_step not set'): + with self.assertRaisesRegex(Exception, 'time_step not set'): self.system.actors.add(solver) @utx.skipIfMissingFeatures("P3M") def test_01_time_not_set_p3m_cpu(self): - import espressomd.electrostatics - self.add_charged_particles() solver = espressomd.electrostatics.P3M(prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: time_step not set'): + with self.assertRaisesRegex(Exception, 'time_step not set'): self.system.actors.add(solver) @utx.skipIfMissingFeatures("DP3M") def test_01_time_not_set_dp3m_cpu(self): - import espressomd.magnetostatics - self.add_magnetic_particles() solver = espressomd.magnetostatics.DipolarP3M( prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: time_step not set'): + with self.assertRaisesRegex(Exception, 'time_step not set'): self.system.actors.add(solver) ############################################## @@ -84,33 +103,37 @@ def test_01_time_not_set_dp3m_cpu(self): @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures("P3M") def test_02_no_particles_p3m_gpu(self): - import espressomd.electrostatics - self.system.time_step = 0.01 solver = espressomd.electrostatics.P3MGPU(prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: no charged particles in the system'): + with self.assertRaisesRegex(RuntimeError, 'no charged particles in the system'): self.system.actors.add(solver) @utx.skipIfMissingFeatures("P3M") def test_02_no_particles_p3m_cpu(self): - import espressomd.electrostatics - self.system.time_step = 0.01 solver = espressomd.electrostatics.P3M(prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: no charged particles in the system'): + with self.assertRaisesRegex(RuntimeError, 'no charged particles in the system'): self.system.actors.add(solver) @utx.skipIfMissingFeatures("DP3M") def test_02_no_particles_dp3m_cpu(self): - import espressomd.magnetostatics + self.system.time_step = 0.01 + + solver = espressomd.magnetostatics.DipolarP3M( + **self.get_valid_params('DP3M')) + with self.assertRaisesRegex(RuntimeError, 'DipolarP3M: no dipolar particles in the system'): + self.system.actors.add(solver) + @utx.skipIfMissingFeatures("DP3M") + def test_02_accuracy_dp3m_cpu(self): self.system.time_step = 0.01 + self.add_magnetic_particles() solver = espressomd.magnetostatics.DipolarP3M( - prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: no dipolar particles in the system'): + **self.get_valid_params('DP3M', accuracy=1e-20)) + with self.assertRaisesRegex(Exception, 'DipolarP3M: failed to reach requested accuracy'): self.system.actors.add(solver) ####################################### @@ -120,107 +143,115 @@ def test_02_no_particles_dp3m_cpu(self): @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures("P3M") def test_03_non_cubic_box_p3m_gpu(self): - import espressomd.electrostatics - self.system.box_l = [10., 10., 20.] self.system.time_step = 0.01 self.add_charged_particles() solver = espressomd.electrostatics.P3MGPU( prefactor=2, accuracy=1e-2, epsilon=1) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: non-metallic epsilon requires cubic box'): + with self.assertRaisesRegex(RuntimeError, 'P3M: non-metallic epsilon requires cubic box'): self.system.actors.add(solver) @utx.skipIfMissingFeatures("P3M") def test_03_non_cubic_box_p3m_cpu(self): - import espressomd.electrostatics - self.system.box_l = [10., 10., 20.] self.system.time_step = 0.01 self.add_charged_particles() solver = espressomd.electrostatics.P3M( - prefactor=2, accuracy=1e-2, epsilon=1) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: non-metallic epsilon requires cubic box'): + prefactor=2, accuracy=1e-2, epsilon=1, mesh=[8, 8, 8]) + with self.assertRaisesRegex(RuntimeError, 'P3M: non-metallic epsilon requires cubic box'): + self.system.actors.add(solver) + + self.system.box_l = [10., 10., 10.] + solver = espressomd.electrostatics.P3M( + prefactor=2, accuracy=1e-2, epsilon=1, mesh=[4, 8, 8]) + with self.assertRaisesRegex(RuntimeError, 'P3M: non-metallic epsilon requires cubic box'): self.system.actors.add(solver) @utx.skipIfMissingFeatures("DP3M") def test_03_non_cubic_box_dp3m_cpu(self): - import espressomd.magnetostatics - self.system.box_l = [10., 10., 20.] self.system.time_step = 0.01 self.add_magnetic_particles() - solver = espressomd.magnetostatics.DipolarP3M( - prefactor=2, accuracy=1e-2) - with self.assertRaisesRegex(Exception, 'P3M: tuning failed: ERROR: dipolar P3M requires a cubic box'): - self.system.actors.add(solver) + with self.assertRaisesRegex(RuntimeError, 'DipolarP3M: requires a cubic box'): + self.system.actors.add(espressomd.magnetostatics.DipolarP3M( + **self.get_valid_params('DP3M'), tune=False)) ########################################## # block of tests with invalid parameters # ########################################## - def check_invalid_params(self, solver_class, **custom_params): + def check_invalid_params(self, class_solver, **custom_params): valid_params = { - 'prefactor': 2, 'accuracy': .01, 'tune': False, 'cao': 1, - 'r_cut': 0.373, 'alpha': 3.81, 'mesh': (8, 8, 8), - 'mesh_off': [-1, -1, -1]} + 'prefactor': 2.0, 'accuracy': .01, 'tune': False, 'cao': 3, + 'r_cut': 0.373, 'alpha': 3.81, 'mesh': (8, 8, 8), 'epsilon': 0., + 'mesh_off': [0.5, 0.5, 0.5]} valid_params.update(custom_params) invalid_params = [ - ('cao', 0, 'P3M: invalid cao'), - ('cao', 8, 'P3M: invalid cao'), - ('r_cut', -2.0, 'P3M: invalid r_cut'), - ('alpha', -2.0, 'P3M: invalid alpha'), - ('accuracy', -2.0, 'P3M: invalid accuracy'), - ('mesh', (-1, -1, -1), 'P3M: invalid mesh size'), - ('mesh', (0, 0, 0), 'P3M: cao larger than mesh size'), - ('mesh_off', (-2, 1, 1), 'P3M: invalid mesh offset'), + ('prefactor', -2.0, "Parameter 'prefactor' must be > 0"), + ('epsilon', -1.0, "Parameter 'epsilon' must be >= 0"), + ('cao', 0, "Parameter 'cao' must be >= 1 and <= 7"), + ('cao', 8, "Parameter 'cao' must be >= 1 and <= 7"), + ('r_cut', -2.0, "Parameter 'r_cut' must be > 0"), + ('alpha', -2.0, "Parameter 'alpha' must be > 0"), + ('accuracy', -2.0, "Parameter 'accuracy' must be > 0"), + ('mesh', (-1, -1, -1), "Parameter 'mesh' must be > 0"), + ('mesh', (2, 2, 2), "Parameter 'cao' cannot be larger than 'mesh'"), + ('mesh_off', (-2, 1, 1), "Parameter 'mesh_off' must be >= 0 and <= 1"), ] + if class_solver is espressomd.magnetostatics.DipolarP3M: + invalid_params.append( + ('mesh', (4, 2, 2), "DipolarP3M requires a cubic mesh") + ) for key, invalid_value, err_msg in invalid_params: params = valid_params.copy() params[key] = invalid_value - solver = solver_class(**params) - with self.assertRaisesRegex(RuntimeError, err_msg): - self.system.actors.add(solver) - self.system.actors.clear() - - @utx.skipIfMissingFeatures("P3M") - def test_04_invalid_params_p3m_cpu(self): - import espressomd.electrostatics - - self.system.time_step = 0.01 - self.add_charged_particles() - - self.check_invalid_params(espressomd.electrostatics.P3M) + with self.assertRaisesRegex(ValueError, err_msg): + class_solver(**params) - # set up a valid actor - solver = espressomd.electrostatics.P3M( - prefactor=2, accuracy=0.1, cao=2, r_cut=3.18, mesh=8) - self.system.actors.add(solver) + # cannot add an actor if cell system isn't compatible + with self.assertRaisesRegex(RuntimeError, "P3M: requires periodicity"): + self.system.periodicity = (True, True, False) + solver = class_solver(**valid_params) + self.system.actors.add(solver) + self.assertEqual(len(self.system.actors), 0) + self.system.periodicity = (True, True, True) + def check_invalid_cell_systems(self): # check periodicity exceptions for periodicity in itertools.product(range(2), range(2), range(2)): if periodicity == (1, 1, 1): continue - with self.assertRaisesRegex(Exception, r"P3M requires periodicity \(1, 1, 1\)"): + with self.assertRaisesRegex(Exception, r"P3M: requires periodicity \(1 1 1\)"): self.system.periodicity = periodicity self.system.periodicity = (1, 1, 1) # check cell system exceptions - with self.assertRaisesRegex(Exception, "P3M requires the regular decomposition cell system"): + with self.assertRaisesRegex(Exception, "P3M: requires the regular decomposition cell system"): self.system.cell_system.set_n_square() self.system.analysis.energy() self.system.cell_system.set_regular_decomposition() - self.system.actors.clear() + + @utx.skipIfMissingFeatures("P3M") + def test_04_invalid_params_p3m_cpu(self): + self.system.time_step = 0.01 + self.add_charged_particles() + + self.check_invalid_params(espressomd.electrostatics.P3M) + + # set up a valid actor + solver = espressomd.electrostatics.P3M( + prefactor=2, accuracy=0.1, cao=2, r_cut=3.18, mesh=8) + self.system.actors.add(solver) + self.check_invalid_cell_systems() @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures("P3M") def test_04_invalid_params_p3m_gpu(self): - import espressomd.electrostatics - self.system.time_step = 0.01 self.add_charged_particles() @@ -229,123 +260,104 @@ def test_04_invalid_params_p3m_gpu(self): @utx.skipIfMissingFeatures("DP3M") def test_04_invalid_params_dp3m_cpu(self): - import espressomd.magnetostatics - self.system.time_step = 0.01 self.add_magnetic_particles() self.check_invalid_params(espressomd.magnetostatics.DipolarP3M) # check bisection exception - self.system.periodicity = (0, 0, 0) - with self.assertRaisesRegex(Exception, r"Root must be bracketed for bisection in dp3m_rtbisection"): + with self.assertRaisesRegex(RuntimeError, r"Root must be bracketed for bisection in dp3m_rtbisection"): solver = espressomd.magnetostatics.DipolarP3M( - prefactor=2, accuracy=0.1) + prefactor=2, accuracy=0.01, cao=1, + r_cut=0.373, alpha=3.81, mesh=(8, 8, 8)) self.system.actors.add(solver) - self.system.periodicity = (1, 1, 1) self.system.actors.clear() # set up a valid actor solver = espressomd.magnetostatics.DipolarP3M( - prefactor=2, accuracy=0.1, cao=1, r_cut=3.28125, mesh=5) + **self.get_valid_params('DP3M'), tune=False) self.system.actors.add(solver) + self.check_invalid_cell_systems() + + def check_invalid_params_layer_corrections(self, solver_p3m, class_lc): + with self.assertRaisesRegex(ValueError, "Parameter 'gap_size' must be > 0"): + class_lc(actor=solver_p3m, gap_size=-1., maxPWerror=0.01) + with self.assertRaisesRegex(ValueError, "Parameter 'maxPWerror' must be > 0"): + class_lc(actor=solver_p3m, gap_size=1., maxPWerror=0.) + with self.assertRaisesRegex(ValueError, "Parameter 'far_cut' must be > 0"): + class_lc(actor=solver_p3m, gap_size=1., maxPWerror=1., far_cut=0.) + with self.assertRaisesRegex(ValueError, "Parameter 'far_cut' must be > 0"): + class_lc(actor=solver_p3m, gap_size=1., maxPWerror=1., far_cut=-2.) + with self.assertRaisesRegex(RuntimeError, "LC gap size .+ larger than box length in z-direction"): + lc = class_lc(actor=solver_p3m, gap_size=100., maxPWerror=0.01) + self.system.actors.add(lc) + self.assertEqual(len(self.system.actors), 0) + + def check_invalid_params_elc_p3m(self, solver_p3m): + ELC = espressomd.electrostatics.ELC + self.check_invalid_params_layer_corrections(solver_p3m, ELC) + + self.system.part.by_id(0).q = -1.00001 - # check periodicity and cell system exceptions - for periodicity in itertools.product(range(2), range(2), range(2)): - if periodicity == (1, 1, 1): - continue - with self.assertRaisesRegex(Exception, r"dipolar P3M requires periodicity \(1, 1, 1\)"): - self.system.periodicity = periodicity - self.system.periodicity = (1, 1, 1) - - # check cell system exceptions - with self.assertRaisesRegex(Exception, "dipolar P3M requires the regular decomposition cell system"): - self.system.cell_system.set_n_square() - self.system.analysis.energy() - self.system.cell_system.set_regular_decomposition() - self.system.actors.clear() - - @utx.skipIfMissingFeatures("P3M") - def test_04_invalid_params_p3m_elc_cpu(self): - import espressomd.electrostatics - - self.system.time_step = 0.01 - self.add_charged_particles() - - solver_p3m = espressomd.electrostatics.P3M( - prefactor=2, accuracy=0.01, tune=False, cao=1, - r_cut=0.373, alpha=3.81, mesh=(8, 8, 8), check_neutrality=False) - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_p3m, gap_size=100, maxPWerror=0.01) - with self.assertRaisesRegex(ValueError, "gap size too large"): - self.system.actors.add(solver_elc) - - self.system.actors.clear() - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_p3m, gap_size=-1, maxPWerror=0.01) - with self.assertRaisesRegex(ValueError, "gap_size must be > 0"): - self.system.actors.add(solver_elc) - - self.system.actors.clear() - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_p3m, gap_size=1, maxPWerror=0) - with self.assertRaisesRegex(ValueError, "maxPWerror must be > 0"): - self.system.actors.add(solver_elc) - - self.system.part.by_id(0).q = -2 - - self.system.actors.clear() - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_p3m, gap_size=1, - maxPWerror=0.01, const_pot=True, pot_diff=3, check_neutrality=False) with self.assertRaisesRegex(RuntimeError, "ELC does not currently support non-neutral systems with a dielectric contrast"): - self.system.actors.add(solver_elc) - + actor = ELC(actor=solver_p3m, gap_size=1., maxPWerror=1., + pot_diff=3., delta_mid_top=0.5, delta_mid_bot=0.5, + const_pot=True, check_neutrality=False) + self.system.actors.add(actor) self.system.actors.clear() - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_p3m, gap_size=1, maxPWerror=0.01, const_pot=False, - check_neutrality=False, delta_mid_top=0.5, delta_mid_bot=0.5) + with self.assertRaisesRegex(RuntimeError, "ELC does not work for non-neutral systems and non-metallic dielectric contrast"): - self.system.actors.add(solver_elc) + actor = ELC(actor=solver_p3m, gap_size=1., maxPWerror=1., + pot_diff=0., delta_mid_top=0.5, delta_mid_bot=0.5, + const_pot=False, check_neutrality=False) + self.system.actors.add(actor) + self.system.actors.clear() self.system.part.by_id(0).q = -1 - self.system.actors.clear() - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_p3m, gap_size=1, maxPWerror=0.01, const_pot=False, - check_neutrality=False, delta_mid_top=-1, delta_mid_bot=-1) - with self.assertRaisesRegex(RuntimeError, "ELC with two parallel metallic boundaries requires the const_pot option"): - self.system.actors.add(solver_elc) + with self.assertRaisesRegex(RuntimeError, "ELC tuning failed: maxPWerror too small"): + # reduce box size to make tuning converge in at most 50 steps + self.system.box_l = [1., 1., 1.] + elc = ELC(actor=solver_p3m, gap_size=0.5, maxPWerror=1e-90) + self.system.actors.add(elc) + self.assertEqual(len(self.system.actors), 0) + self.system.box_l = [10., 10., 10.] - self.system.actors.clear() - solver_dh = espressomd.electrostatics.DH( - prefactor=1.2, kappa=0.8, r_cut=2.0) - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_dh, gap_size=1, maxPWerror=0.01) - with self.assertRaisesRegex(ValueError, "p3m_actor has to be a P3M solver"): - self.system.actors.add(solver_elc) + # r_cut > gap isn't allowed with dielectric contrasts + p3m = espressomd.electrostatics.P3M( + prefactor=1.5, r_cut=3., accuracy=0.01, mesh=[8, 8, 8]) + with self.assertRaisesRegex(RuntimeError, "failed to reach requested accuracy"): + elc = ELC(actor=p3m, gap_size=p3m.r_cut / 2., maxPWerror=0.01, + delta_mid_top=0.5, delta_mid_bot=0.5, pot_diff=-3., + const_pot=True) + self.system.actors.add(elc) + self.assertEqual(len(self.system.actors), 0) + + # r_cut > gap is allowed without dielectric contrasts + elc = ELC(actor=p3m, gap_size=p3m.r_cut / 2., maxPWerror=0.01) + self.system.actors.add(elc) + self.assertEqual(len(self.system.actors), 1) + self.assertAlmostEqual(elc.prefactor, 1.5, delta=1e-12) - @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures("P3M") - def test_04_invalid_params_p3m_elc_gpu(self): - import espressomd.electrostatics - + def test_04_invalid_params_elc_p3m_cpu(self): self.system.time_step = 0.01 self.add_charged_particles() + params = self.get_valid_params('P3M', tune=False) + solver = espressomd.electrostatics.P3M(**params) + self.check_invalid_params_elc_p3m(solver) - solver_p3m = espressomd.electrostatics.P3MGPU( - prefactor=2, accuracy=0.01, tune=False, cao=1, - r_cut=4.4434, alpha=0.3548, mesh=(28, 28, 28)) - solver_elc = espressomd.electrostatics.ELC( - p3m_actor=solver_p3m, gap_size=1, maxPWerror=0.01) - with self.assertRaisesRegex(ValueError, "ELC is not set up to work with the GPU P3M"): - self.system.actors.add(solver_elc) + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures("P3M") + def test_04_invalid_params_elc_p3m_gpu(self): + self.system.time_step = 0.01 + self.add_charged_particles() + params = self.get_valid_params('P3MGPU', tune=False) + solver = espressomd.electrostatics.P3MGPU(**params) + self.check_invalid_params_elc_p3m(solver) @utx.skipIfMissingFeatures("DP3M") - def test_04_invalid_params_dp3m_dlc_cpu(self): - import espressomd.magnetostatics - import espressomd.magnetostatic_extensions - + def test_04_invalid_params_dlc_dp3m_cpu(self): self.system.time_step = 0.01 self.add_magnetic_particles() @@ -354,97 +366,73 @@ def test_04_invalid_params_dp3m_dlc_cpu(self): solver_dp3m = espressomd.magnetostatics.DipolarP3M( epsilon='metallic', tune=False, **dp3m_params) - self.system.actors.add(solver_dp3m) - - solver_mdlc = espressomd.magnetostatic_extensions.DLC( - gap_size=100, maxPWerror=1e-5) - with self.assertRaisesRegex(ValueError, "gap size too large"): - self.system.actors.add(solver_mdlc) - self.system.actors.remove(solver_mdlc) + self.check_invalid_params_layer_corrections( + solver_dp3m, espressomd.magnetostatics.DLC) - solver_mdlc = espressomd.magnetostatic_extensions.DLC( - gap_size=-1, maxPWerror=1e-5) - with self.assertRaisesRegex(ValueError, "gap_size must be > 0"): + solver_mdlc = espressomd.magnetostatics.DLC( + gap_size=1, maxPWerror=1e-30, actor=solver_dp3m) + with self.assertRaisesRegex(RuntimeError, "DLC tuning failed: maxPWerror too small"): self.system.actors.add(solver_mdlc) - self.system.actors.remove(solver_mdlc) - solver_mdlc = espressomd.magnetostatic_extensions.DLC( - gap_size=1, maxPWerror=0) - with self.assertRaisesRegex(ValueError, "maxPWerror must be > 0"): - self.system.actors.add(solver_mdlc) - self.system.actors.remove(solver_mdlc) + dp3m_params = {'accuracy': 1e-30, 'mesh': [6, 6, 6], + 'prefactor': 1.1, 'r_cut': 4.50} - solver_mdlc = espressomd.magnetostatic_extensions.DLC( - gap_size=1, maxPWerror=1e-30) - with self.assertRaisesRegex(RuntimeError, "MDLC tuning failed: unable to find a proper cut-off for the given accuracy"): + solver_dp3m = espressomd.magnetostatics.DipolarP3M( + prefactor=1., accuracy=1e-30, mesh=[6, 6, 6], r_cut=4.50) + solver_mdlc = espressomd.magnetostatics.DLC( + gap_size=1., maxPWerror=1e-2, actor=solver_dp3m) + with self.assertRaisesRegex(RuntimeError, "P3M: failed to reach requested accuracy"): self.system.actors.add(solver_mdlc) - self.system.actors.remove(solver_mdlc) ########################################################### # block of tests where tuning should not throw exceptions # ########################################################### - def add_actor_assert_failure(self, actor): - try: - self.system.actors.add(actor) - except Exception as err: - self.fail(f'tuning raised Exception("{err}")') - @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures("P3M") def test_09_no_errors_p3m_gpu(self): - import espressomd.electrostatics - self.system.time_step = 0.01 self.add_charged_particles() - solver = espressomd.electrostatics.P3MGPU(prefactor=2, accuracy=1e-2, - epsilon='metallic') - self.add_actor_assert_failure(solver) + # mesh is fixed to significantly speed up tuning + solver = espressomd.electrostatics.P3MGPU( + prefactor=2, accuracy=1e-2, epsilon='metallic', mesh=[20, 20, 20]) + self.system.actors.add(solver) @utx.skipIfMissingFeatures("P3M") def test_09_no_errors_p3m_cpu(self): - import espressomd.electrostatics - self.system.time_step = 0.01 self.add_charged_particles() - solver = espressomd.electrostatics.P3M(prefactor=2, accuracy=0.1) valid_params = { - 'mesh_off': solver.default_params()['mesh_off'], # sentinel + 'mesh_off': [-1., -1., -1.], # sentinel 'cao': 2, 'r_cut': 3.18, 'mesh': 8} # tuning with cao or r_cut or mesh constrained, or without constraints for key, value in valid_params.items(): solver = espressomd.electrostatics.P3M( prefactor=2, accuracy=1e-2, epsilon=0.0, **{key: value}) - self.add_actor_assert_failure(solver) + self.system.actors.add(solver) self.system.actors.clear() @utx.skipIfMissingFeatures("DP3M") def test_09_no_errors_dp3m_cpu(self): - import espressomd.magnetostatics - self.system.time_step = 0.01 self.add_magnetic_particles() - solver = espressomd.magnetostatics.DipolarP3M( - prefactor=2, accuracy=0.1) valid_params = { - 'mesh_off': solver.default_params()['mesh_off'], # sentinel + 'mesh_off': [-1., -1., -1.], # sentinel 'cao': 1, 'r_cut': 3.28125, 'mesh': 5} # tuning with cao or r_cut or mesh constrained, or without constraints for key, value in valid_params.items(): solver = espressomd.magnetostatics.DipolarP3M( prefactor=2, accuracy=1e-2, **{key: value}) - self.add_actor_assert_failure(solver) + self.system.actors.add(solver) self.system.actors.clear() @utx.skipIfMissingFeatures("P3M") def test_09_no_errors_p3m_cpu_rescale_mesh(self): - import espressomd.electrostatics - self.system.box_l = [10., 15., 20.] self.system.time_step = 0.01 self.add_charged_particles() @@ -452,55 +440,108 @@ def test_09_no_errors_p3m_cpu_rescale_mesh(self): solver = espressomd.electrostatics.P3M(prefactor=2, accuracy=1e-2, epsilon='metallic', mesh=[8, -1, -1]) - self.add_actor_assert_failure(solver) - tuned_mesh = solver.get_params()['mesh'] - self.assertEqual(tuned_mesh[0], 8) - self.assertEqual(tuned_mesh[1], 12) - self.assertEqual(tuned_mesh[2], 16) + self.system.actors.add(solver) + self.assertEqual(solver.mesh, [8, 12, 16]) # check MD cell reset event self.system.box_l = self.system.box_l self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures("P3M") def test_09_no_errors_p3m_gpu_rescale_mesh(self): - import espressomd.electrostatics - - self.system.box_l = [10., 15., 20.] + self.system.box_l = [10., 10., 20.] self.system.time_step = 0.01 self.add_charged_particles() solver = espressomd.electrostatics.P3MGPU(prefactor=2, accuracy=1e-1, epsilon='metallic', mesh=[20, -1, -1]) - self.add_actor_assert_failure(solver) - tuned_mesh = solver.get_params()['mesh'] - self.assertEqual(tuned_mesh[0], 20) - self.assertEqual(tuned_mesh[1], 30) - self.assertEqual(tuned_mesh[2], 40) + self.system.actors.add(solver) + self.assertEqual(solver.mesh, [20, 20, 40]) # check MD cell reset event self.system.box_l = self.system.box_l self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid @utx.skipIfMissingFeatures("DP3M") def test_09_no_errors_dp3m_cpu_rescale_mesh(self): - import espressomd.magnetostatics - self.system.time_step = 0.01 self.add_magnetic_particles() dp3m_params = {'accuracy': 1e-6, 'mesh': [25, 25, 25], 'cao': 7, 'prefactor': 1.1, 'r_cut': 4.50, 'alpha': 0.8216263} - solver_dp3m = espressomd.magnetostatics.DipolarP3M( + solver = espressomd.magnetostatics.DipolarP3M( epsilon='metallic', tune=False, **dp3m_params) - self.add_actor_assert_failure(solver_dp3m) + self.system.actors.add(solver) # check MD cell reset event self.system.box_l = self.system.box_l self.system.periodicity = self.system.periodicity + self.system.cell_system.node_grid = self.system.cell_system.node_grid + + def check_tuning_layer_corrections(self, class_p3m, class_lc, params): + if class_p3m is espressomd.magnetostatics.DipolarP3M: + mesh_a = np.array([2., 2., 2.]) + else: + mesh_a = np.array([2., 4., 8.]) + self.system.box_l = mesh_a * params["mesh"] + self.system.time_step = 0.01 + non_metallic_epsilon = 20. + p3m = class_p3m(epsilon=non_metallic_epsilon, **params) + self.assertEqual(p3m.epsilon, non_metallic_epsilon) + lc = class_lc(actor=p3m, gap_size=1., maxPWerror=1.) + # Non-metallic epsilon values are not allowed for non-cubic boxes and + # will cause tuning to fail. Since ELC doesn't support non-metallic + # epsilon values either, it sets metallic epsilon before tuning. + if class_lc is espressomd.electrostatics.ELC: + self.assertEqual(p3m.epsilon, 0.) + self.system.actors.add(lc) + + # check parameter rescaling + alpha = p3m.alpha + r_cut = params["r_cut"] + r_cut_iL = r_cut / self.system.box_l[0] + alpha_L = alpha * self.system.box_l[0] + np.testing.assert_allclose(np.copy(p3m.a), mesh_a, atol=1e-12) + np.testing.assert_allclose(p3m.r_cut, r_cut, atol=1e-12) + np.testing.assert_allclose(p3m.r_cut_iL, r_cut_iL, atol=1e-12) + np.testing.assert_allclose(p3m.alpha_L, alpha_L, atol=1e-12) + mesh_a = np.array([4., 4., 4.]) + self.system.box_l = mesh_a * params["mesh"] + np.testing.assert_allclose(np.copy(p3m.a), mesh_a, atol=1e-12) + np.testing.assert_allclose(p3m.r_cut, r_cut * 2., atol=1e-12) + np.testing.assert_allclose(p3m.r_cut_iL, r_cut_iL, atol=1e-12) + np.testing.assert_allclose(p3m.alpha, alpha / 2., atol=1e-12) + np.testing.assert_allclose(p3m.alpha_L, alpha_L, atol=1e-12) + + @utx.skipIfMissingFeatures("P3M") + def test_09_no_errors_elc_p3m_cpu_rescale_mesh(self): + self.add_charged_particles() + self.check_tuning_layer_corrections( + espressomd.electrostatics.P3M, + espressomd.electrostatics.ELC, + self.get_valid_params("P3M", accuracy=0.1)) + + @utx.skipIfMissingGPU() + @utx.skipIfMissingFeatures("P3M") + def test_09_no_errors_elc_p3m_gpu_rescale_mesh(self): + self.add_charged_particles() + self.check_tuning_layer_corrections( + espressomd.electrostatics.P3MGPU, + espressomd.electrostatics.ELC, + self.get_valid_params("P3MGPU", accuracy=0.1)) + + @utx.skipIfMissingFeatures("DP3M") + def test_09_no_errors_dlc_dp3m_cpu_rescale_mesh(self): + self.add_magnetic_particles() + self.check_tuning_layer_corrections( + espressomd.magnetostatics.DipolarP3M, + espressomd.magnetostatics.DLC, + self.get_valid_params("DP3M", accuracy=0.1)) if __name__ == "__main__": - ut.main() + ut.main(failfast=True) diff --git a/testsuite/python/save_checkpoint.py b/testsuite/python/save_checkpoint.py index 9bc321bea22..aa5a2e46a1b 100644 --- a/testsuite/python/save_checkpoint.py +++ b/testsuite/python/save_checkpoint.py @@ -21,6 +21,7 @@ import espressomd import espressomd.checkpointing +import espressomd.code_info import espressomd.electrostatics import espressomd.magnetostatics import espressomd.interactions @@ -114,14 +115,16 @@ tune=False) if 'ELC' in modes: elc = espressomd.electrostatics.ELC( - p3m_actor=p3m, + actor=p3m, gap_size=6.0, maxPWerror=0.1, delta_mid_top=0.9, delta_mid_bot=0.1) system.actors.add(elc) + elc.charge_neutrality_tolerance = 7e-12 else: system.actors.add(p3m) + p3m.charge_neutrality_tolerance = 5e-12 # accumulators obs = espressomd.observables.ParticlePositions(ids=[0, 1]) @@ -282,7 +285,6 @@ dp3m = espressomd.magnetostatics.DipolarP3M( prefactor=1., epsilon=2., - mesh_off=[0.5, 0.5, 0.5], r_cut=2.4, cao=1, mesh=[8, 8, 8], @@ -293,7 +295,7 @@ system.actors.add(dp3m) if espressomd.has_features('SCAFACOS') and 'SCAFACOS' in modes \ - and 'p3m' in espressomd.scafacos.available_methods(): + and 'p3m' in espressomd.code_info.scafacos_methods(): system.actors.add(espressomd.electrostatics.Scafacos( prefactor=0.5, method_name="p3m", @@ -304,7 +306,7 @@ "p3m_alpha": 2.084652})) if espressomd.has_features('SCAFACOS_DIPOLES') and 'SCAFACOS' in modes \ - and 'p2nfft' in espressomd.scafacos.available_methods(): + and 'p2nfft' in espressomd.code_info.scafacos_methods(): system.actors.add(espressomd.magnetostatics.Scafacos( prefactor=1.2, method_name='p2nfft', diff --git a/testsuite/python/scafacos_dipoles_1d_2d.py b/testsuite/python/scafacos_dipoles_1d_2d.py index b32b6a30aa7..014debef40b 100644 --- a/testsuite/python/scafacos_dipoles_1d_2d.py +++ b/testsuite/python/scafacos_dipoles_1d_2d.py @@ -41,11 +41,8 @@ def tearDown(self): self.system.actors.clear() self.system.periodicity = [1, 1, 1] - def vector_error(self, a, b): - return np.sum(np.linalg.norm(a - b, axis=1)) / np.sqrt(a.shape[0]) - def test_scafacos(self): - s = self.system + system = self.system rho = 0.3 # This is only for box size calculation. The actual particle number is @@ -55,90 +52,88 @@ def test_scafacos(self): particle_radius = 0.5 box_l = np.cbrt(4 * n_particle * np.pi / (3 * rho)) * particle_radius - s.box_l = 3 * [box_l] + system.box_l = 3 * [box_l] for dim in (2, 1): - print("Dimension", dim) - - # Read reference data - if dim == 2: - file_prefix = "mdlc" - s.periodicity = [1, 1, 0] - else: - s.periodicity = [1, 0, 0] - file_prefix = "scafacos_dipoles_1d" - - ref_E_path = tests_common.data_path( - f"{file_prefix}_reference_data_energy.dat") - ref_E = float(np.genfromtxt(ref_E_path)) - - # Particles - data = np.genfromtxt(tests_common.data_path( - f"{file_prefix}_reference_data_forces_torques.dat")) - s.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) - s.part.all().rotation = 3 * [True] - - if dim == 2: - scafacos = espressomd.magnetostatics.Scafacos( - prefactor=1, - method_name="p2nfft", - method_params={ - "p2nfft_verbose_tuning": 0, - "pnfft_N": "80,80,160", - "pnfft_window_name": "bspline", - "pnfft_m": "4", - "p2nfft_ignore_tolerance": "1", - "pnfft_diff_ik": "0", - "p2nfft_r_cut": "6", - "p2nfft_alpha": "0.8", - "p2nfft_epsB": "0.05"}) - s.actors.add(scafacos) - # change box geometry in x,y direction to ensure that - # scafacos survives it - s.box_l = np.array((1, 1, 1.3)) * box_l - - else: - # 1d periodic in x - scafacos = espressomd.magnetostatics.Scafacos( - prefactor=1, - method_name="p2nfft", - method_params={ - "p2nfft_verbose_tuning": 1, - "pnfft_N": "32,128,128", - "pnfft_direct": 0, - "p2nfft_r_cut": 2.855, - "p2nfft_alpha": "1.5", - "p2nfft_intpol_order": "-1", - "p2nfft_reg_kernel_name": "ewald", - "p2nfft_p": "16", - "p2nfft_ignore_tolerance": "1", - "pnfft_window_name": "bspline", - "pnfft_m": "8", - "pnfft_diff_ik": "1", - "p2nfft_epsB": "0.125"}) - s.box_l = np.array((1, 1, 1)) * box_l - s.actors.add(scafacos) - s.integrator.run(0) - - # Calculate errors - - err_f = self.vector_error(s.part.all().f, data[:, 7:10]) - err_t = self.vector_error(s.part.all().torque_lab, data[:, 10:13]) - err_e = s.analysis.energy()["dipolar"] - ref_E - - tol_f = 2E-3 - tol_t = 2E-3 - tol_e = 1E-3 - - self.assertLessEqual( - abs(err_e), tol_e, "Energy difference too large") - self.assertLessEqual( - abs(err_t), tol_t, "Torque difference too large") - self.assertLessEqual( - abs(err_f), tol_f, "Force difference too large") - - s.part.clear() - s.actors.clear() + with self.subTest(f"{dim} dimensions"): + # Read reference data + if dim == 2: + file_prefix = "mdlc" + system.periodicity = [1, 1, 0] + else: + system.periodicity = [1, 0, 0] + file_prefix = "scafacos_dipoles_1d" + + ref_e_path = tests_common.data_path( + f"{file_prefix}_reference_data_energy.dat") + ref_e = float(np.genfromtxt(ref_e_path)) + + # Particles + data = np.genfromtxt(tests_common.data_path( + f"{file_prefix}_reference_data_forces_torques.dat")) + system.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) + system.part.all().rotation = 3 * [True] + + if dim == 2: + scafacos = espressomd.magnetostatics.Scafacos( + prefactor=1., + method_name="p2nfft", + method_params={ + "p2nfft_verbose_tuning": 0, + "pnfft_N": "80,80,160", + "pnfft_window_name": "bspline", + "pnfft_m": "4", + "p2nfft_ignore_tolerance": "1", + "pnfft_diff_ik": "0", + "p2nfft_r_cut": "6", + "p2nfft_alpha": "0.8", + "p2nfft_epsB": "0.05"}) + # change box geometry in x,y direction to ensure that + # scafacos survives it + system.box_l = np.array([1., 1., 1.3]) * box_l + else: + # 1d periodic in x + scafacos = espressomd.magnetostatics.Scafacos( + prefactor=1., + method_name="p2nfft", + method_params={ + "p2nfft_verbose_tuning": 1, + "pnfft_N": "32,128,128", + "pnfft_direct": 0, + "p2nfft_r_cut": 2.855, + "p2nfft_alpha": "1.5", + "p2nfft_intpol_order": "-1", + "p2nfft_reg_kernel_name": "ewald", + "p2nfft_p": "16", + "p2nfft_ignore_tolerance": "1", + "pnfft_window_name": "bspline", + "pnfft_m": "8", + "pnfft_diff_ik": "1", + "p2nfft_epsB": "0.125"}) + system.box_l = np.array([1., 1., 1.]) * box_l + + system.actors.add(scafacos) + system.integrator.run(0) + + fcs_f = np.copy(system.part.all().f) + fcs_t = np.copy(system.part.all().torque_lab) + fcs_e = system.analysis.energy()["dipolar"] + ref_f = data[:, 7:10] + ref_t = data[:, 10:13] + + tol_f = 1E-4 + tol_t = 1E-3 + tol_e = 1E-3 + + np.testing.assert_allclose(fcs_e, ref_e, atol=tol_e, + err_msg="Energy doesn't match") + np.testing.assert_allclose(fcs_t, ref_t, atol=tol_t, + err_msg="Torques don't match") + np.testing.assert_allclose(fcs_f, ref_f, atol=tol_f, + err_msg="Forces don't match") + + system.part.clear() + system.actors.clear() if __name__ == "__main__": diff --git a/testsuite/python/scafacos_interface.py b/testsuite/python/scafacos_interface.py index 59069e141e5..720a4363fad 100644 --- a/testsuite/python/scafacos_interface.py +++ b/testsuite/python/scafacos_interface.py @@ -23,7 +23,7 @@ import espressomd import espressomd.electrostatics import espressomd.magnetostatics -import espressomd.scafacos +import espressomd.code_info @utx.skipIfMissingFeatures(["SCAFACOS"]) @@ -37,85 +37,217 @@ class ScafacosInterface(ut.TestCase): def tearDown(self): self.system.part.clear() self.system.actors.clear() + self.system.integrator.set_vv() - def test_available_methods(self): + def test_decorator(self): + decorator = utx.skipIfMissingScafacosMethod("unknown") + with self.assertRaisesRegex(ut.SkipTest, "ScaFaCoS method 'unknown' not available"): + decorator(lambda: None)() + + def check_available_methods(self, methods): # all methods that are accessible from the ScaFaCoS bridge in ESPResSo scafacos_methods = { "direct", "ewald", "fmm", "memd", "mmm1d", "mmm2d", "p2nfft", "p3m", "pepc", "pp3mg", "vmg", "wolf"} - # all methods that were compiled in when building ScaFaCoS - available_methods = espressomd.scafacos.available_methods() - self.assertGreaterEqual(len(available_methods), 1) - for method in available_methods: + self.assertGreaterEqual(len(methods), 1) + for method in methods: self.assertIn(method, scafacos_methods) - @ut.skipIf(not espressomd.has_features('SCAFACOS') or - 'p3m' not in espressomd.scafacos.available_methods(), - 'Skipping test: missing ScaFaCoS p3m method') - def test_actor_exceptions(self): + @utx.skipIfMissingFeatures(["SCAFACOS_DIPOLES"]) + @utx.skipIfMissingScafacosMethod("p3m") + @utx.skipIfMissingScafacosMethod("p2nfft") + def test_magnetostatics_actor_exceptions(self): system = self.system - - if espressomd.has_features('SCAFACOS_DIPOLES'): - with self.assertRaisesRegex(ValueError, "Dipolar prefactor has to be >= 0"): - system.actors.add(espressomd.magnetostatics.Scafacos( - prefactor=-1, method_name="p3m", method_params={"p3m_cao": 7})) - system.actors.clear() - with self.assertRaisesRegex(ValueError, "Coulomb prefactor has to be >= 0"): - system.actors.add(espressomd.electrostatics.Scafacos( - prefactor=-1, method_name="p3m", method_params={"p3m_cao": 7})) - system.actors.clear() - with self.assertRaisesRegex(ValueError, "method 'impossible' is unknown or not compiled in ScaFaCoS"): - system.actors.add(espressomd.electrostatics.Scafacos( - prefactor=1, method_name="impossible", method_params={"p3m_cao": 7})) - system.actors.clear() + with self.assertRaisesRegex(ValueError, "Parameter 'prefactor' must be > 0"): + espressomd.magnetostatics.Scafacos( + prefactor=-0.8, method_name="p2nfft", + method_params={"p2nfft_alpha": "0.37"}) + with self.assertRaisesRegex(RuntimeError, "Dipole particles not implemented for solver method 'p3m'"): + actor = espressomd.magnetostatics.Scafacos( + prefactor=1., method_name="p3m", method_params={"p3m_cao": 7}) + system.actors.add(actor) + self.assertEqual(len(system.actors), 0) + + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("p3m") + @utx.skipIfMissingScafacosMethod("ewald") + def test_electrostatics_actor_exceptions(self): + system = self.system + with self.assertRaisesRegex(ValueError, "Parameter 'prefactor' must be > 0"): + espressomd.electrostatics.Scafacos( + prefactor=-0.8, method_name="p3m", method_params={"p3m_cao": 7}) + with self.assertRaisesRegex(ValueError, "Method 'impossible' is unknown or not compiled in ScaFaCoS"): + espressomd.electrostatics.Scafacos( + prefactor=1., method_name="impossible", method_params={"p3m_cao": 7}) with self.assertRaisesRegex(ValueError, "ScaFaCoS methods require at least 1 parameter"): - system.actors.add(espressomd.electrostatics.Scafacos( - prefactor=1, method_name="p3m", method_params={})) + espressomd.electrostatics.Scafacos( + prefactor=1., method_name="p3m", method_params={}) + + # choose a method that doesn't support near-field delegation + scafacos = espressomd.electrostatics.Scafacos( + prefactor=1., + method_name="ewald", + method_params={"tolerance_field": 0.1}) + self.system.actors.add(scafacos) + self.assertFalse(scafacos.call_method("get_near_field_delegation")) + scafacos.call_method("set_near_field_delegation", delegate=False) + with self.assertRaisesRegex(RuntimeError, "Method 'ewald' cannot delegate short-range calculation"): + scafacos.call_method("set_near_field_delegation", delegate=True) system.actors.clear() - @ut.skipIf(not espressomd.has_features('SCAFACOS') or - 'p3m' not in espressomd.scafacos.available_methods(), - 'Skipping test: missing ScaFaCoS p3m method') + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("p3m") def test_actor_coulomb(self): system = self.system - system.actors.add(espressomd.electrostatics.Scafacos( + actor = espressomd.electrostatics.Scafacos( prefactor=0.5, method_name="p3m", method_params={ "p3m_r_cut": 1.0, "p3m_alpha": 2.799269, "p3m_grid": 32, - "p3m_cao": 7})) - actor = system.actors[0] + "p3m_cao": 7}) + system.actors.add(actor) params = actor.get_params() self.assertEqual(params["prefactor"], 0.5) self.assertEqual(params["method_name"], "p3m") self.assertEqual(params["method_params"], - {'p3m_cao': '7', 'p3m_r_cut': '1.0', - 'p3m_grid': '32', 'p3m_alpha': '2.799269'}) + {'p3m_cao': 7, 'p3m_r_cut': 1.0, + 'p3m_grid': 32, 'p3m_alpha': 2.799269}) # check MD cell reset event system.box_l = system.box_l system.periodicity = system.periodicity - @ut.skipIf(not espressomd.has_features('SCAFACOS_DIPOLES') or - 'p2nfft' not in espressomd.scafacos.available_methods(), - 'Skipping test: missing ScaFaCoS p2nfft method') + # force data array update, no-op since there are no particles + system.integrator.run(0) + + # check available methods + available_methods = espressomd.code_info.scafacos_methods() + methods = actor.get_available_methods() + self.assertEqual(set(methods), set(available_methods)) + self.check_available_methods(methods) + + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("p3m") + def test_tuning_r_cut_near_field(self): + system = self.system + + actor = espressomd.electrostatics.Scafacos( + prefactor=0.5, + method_name="p3m", + method_params={ + "p3m_r_cut": -1.0, + "p3m_alpha": 2.8, + "p3m_grid": 32, + "p3m_cao": 7}) + system.actors.add(actor) + tuned_r_cut = actor.method_params['p3m_r_cut'] + self.assertGreaterEqual(tuned_r_cut, 0.1) + self.assertLessEqual(tuned_r_cut, min(self.system.box_l) / 2.) + + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("ewald") + def test_tuning_exceptions(self): + system = self.system + # add particles + N = 100 + np.random.seed(42) + system.part.add(pos=np.random.uniform(0., system.box_l[0], (N, 3)), + q=np.sign((np.arange(N) % 2) * 2. - 1.)) + + # minimize system + system.non_bonded_inter[0, 0].lennard_jones.set_params( + epsilon=1.0, sigma=1.0, cutoff=2.**(1.0 / 6.0), shift="auto") + system.integrator.set_steepest_descent( + f_max=10., + gamma=0.001, + max_displacement=0.01) + system.integrator.run(100) + + actor = espressomd.electrostatics.Scafacos( + prefactor=0.5, + method_name="ewald", + method_params={"ewald_r_cut": -1.0, "ewald_alpha": 2.8}) + + # Ewald cannot delegate near-field calculation + with self.assertRaisesRegex(RuntimeError, "Method 'ewald' cannot delegate short-range calculation"): + actor.call_method('set_near_field_delegation', delegate=True) + actor.call_method('set_near_field_delegation', delegate=False) + + # attempt to tune r_cut with ScaFaCoS + with self.assertRaisesRegex(RuntimeError, "r_cut is negative"): + system.actors.add(actor) + system.actors.clear() + + tuned_r_cut = actor.method_params['ewald_r_cut'] + self.assertAlmostEqual(tuned_r_cut, -1., delta=1e-12) + + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("ewald") + def test_tuning_r_cut_ewald(self): + system = self.system + + actor = espressomd.electrostatics.Scafacos( + prefactor=0.5, + method_name="ewald", + method_params={ + "ewald_alpha": 2., + "ewald_maxkmax": 200}) + + # let ScaFaCoS tune r_cut + system.actors.add(actor) + + # cutoff is hidden since we don't delegate near-field + self.assertNotIn("ewald_r_cut", actor.method_params) + + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("ewald") + def test_tuning_alpha_ewald(self): + system = self.system + + actor = espressomd.electrostatics.Scafacos( + prefactor=0.5, + method_name="ewald", + method_params={ + "ewald_r_cut": 0.2, + "ewald_maxkmax": 200}) + + # let ScaFaCoS tune alpha + system.actors.add(actor) + + tuned_r_cut = actor.method_params['ewald_r_cut'] + self.assertAlmostEqual(tuned_r_cut, 0.2, delta=1e-12) + + @utx.skipIfMissingFeatures(["SCAFACOS_DIPOLES"]) + @utx.skipIfMissingScafacosMethod("p2nfft") def test_actor_dipoles(self): system = self.system + method_params_ref = { + "p2nfft_verbose_tuning": 0, + "pnfft_N": [32, 32, 32], + "pnfft_n": [32, 32, 32], + "pnfft_window_name": "bspline", + "pnfft_m": 4, + "p2nfft_ignore_tolerance": 1, + "pnfft_diff_ik": 0, + "p2nfft_r_cut": 11, + "p2nfft_alpha": 0.37, + } method_params = { - "p2nfft_verbose_tuning": "0", - "pnfft_N": "32,32,32", - "pnfft_n": "32,32,32", + "p2nfft_verbose_tuning": "0", # should come back as an integer + "pnfft_N": "32,32,32", # should come back as a list + "pnfft_n": [32, 32, 32], "pnfft_window_name": "bspline", - "pnfft_m": "4", - "p2nfft_ignore_tolerance": "1", - "pnfft_diff_ik": "0", - "p2nfft_r_cut": "11", - "p2nfft_alpha": "0.37"} + "pnfft_m": 4, + "p2nfft_ignore_tolerance": [1], # shouldn't come back as a list + "pnfft_diff_ik": 0, + "p2nfft_r_cut": 11, # should come back as an integer + "p2nfft_alpha": "0.37" + 30 * "0" + "1", # discard extra digits + } system.actors.add(espressomd.magnetostatics.Scafacos( prefactor=1.2, method_name="p2nfft", @@ -124,12 +256,23 @@ def test_actor_dipoles(self): params = actor.get_params() self.assertEqual(params["prefactor"], 1.2) self.assertEqual(params["method_name"], "p2nfft") - self.assertEqual(params["method_params"], method_params) + self.assertEqual(params["method_params"], method_params_ref) + self.assertEqual({k: type(v) for k, v in actor.method_params.items()}, + {k: type(v) for k, v in method_params_ref.items()}) # check MD cell reset event system.box_l = system.box_l system.periodicity = system.periodicity + # force data array update, no-op since there are no particles + system.integrator.run(0) + + # check available methods + available_methods = espressomd.code_info.scafacos_methods() + methods = actor.get_available_methods() + self.assertEqual(set(methods), set(available_methods)) + self.check_available_methods(methods) + def p3m_data(self): system = self.system @@ -200,14 +343,26 @@ def fcs_data(self): ref_forces = np.copy(system.part.all().f) ref_torques = np.copy(system.part.all().torque_lab) + # check MD cell reset has no impact + system.box_l = system.box_l + system.periodicity = system.periodicity + system.cell_system.node_grid = system.cell_system.node_grid + system.integrator.run(0, recalc_forces=True) + new_E_coulomb = system.analysis.energy()["coulomb"] + new_E_dipoles = system.analysis.energy()["dipolar"] + new_forces = np.copy(system.part.all().f) + new_torques = np.copy(system.part.all().torque_lab) + self.assertAlmostEqual(new_E_coulomb, ref_E_coulomb, delta=0) + self.assertAlmostEqual(new_E_dipoles, ref_E_dipoles, delta=0) + np.testing.assert_allclose(new_forces, ref_forces, atol=0, rtol=0.) + np.testing.assert_allclose(new_torques, ref_torques, atol=0, rtol=0.) + system.actors.clear() return (ref_E_coulomb, ref_E_dipoles, ref_forces, ref_torques) - @utx.skipIfMissingFeatures(["LENNARD_JONES", "P3M"]) - @ut.skipIf(not espressomd.has_features('SCAFACOS_DIPOLES') or - 'p2nfft' not in espressomd.scafacos.available_methods(), - 'Skipping test: missing SCAFACOS_DIPOLES or p2nfft method') + @utx.skipIfMissingFeatures(["LENNARD_JONES", "P3M", "SCAFACOS_DIPOLES"]) + @utx.skipIfMissingScafacosMethod("p2nfft") def test_electrostatics_plus_magnetostatics(self): # check that two instances of ScaFaCoS can be used system = self.system @@ -215,16 +370,16 @@ def test_electrostatics_plus_magnetostatics(self): # add particles N = 100 np.random.seed(42) - system.part.add(pos=np.random.uniform(0, system.box_l[0], (N, 3)), - dip=np.random.uniform(0, 1, (N, 3)), - q=np.sign((np.arange(N) % 2) * 2 - 1), - rotation=N * [(1, 1, 1)]) + system.part.add(pos=np.random.uniform(0., system.box_l[0], (N, 3)), + dip=np.random.uniform(0., 1., (N, 3)), + q=np.sign((np.arange(N) % 2) * 2. - 1.), + rotation=N * [(True, True, True)]) # minimize system system.non_bonded_inter[0, 0].lennard_jones.set_params( - epsilon=1.0, sigma=1.0, cutoff=2**(1.0 / 6.0), shift="auto") + epsilon=1.0, sigma=1.0, cutoff=2.**(1.0 / 6.0), shift="auto") system.integrator.set_steepest_descent( - f_max=10, + f_max=10., gamma=0.001, max_displacement=0.01) system.integrator.run(100) diff --git a/testsuite/python/test_checkpoint.py b/testsuite/python/test_checkpoint.py index 979b8e487a3..10e546c5bf7 100644 --- a/testsuite/python/test_checkpoint.py +++ b/testsuite/python/test_checkpoint.py @@ -27,7 +27,6 @@ import espressomd.checkpointing import espressomd.electrostatics import espressomd.magnetostatics -import espressomd.scafacos import espressomd.io.writer # pylint: disable=unused-import import espressomd.virtual_sites import espressomd.integrate @@ -481,11 +480,12 @@ def test_dp3m(self): @ut.skipIf(not has_p3m_mode, "Skipping test due to missing combination.") def test_p3m(self): actor = self.get_active_actor_of_type( - espressomd.electrostatics.ElectrostaticInteraction) + espressomd.electrostatics._P3MBase) state = actor.get_params() reference = {'prefactor': 1.0, 'accuracy': 0.1, 'mesh': 3 * [10], 'cao': 1, 'alpha': 1.0, 'r_cut': 1.0, 'tune': False, - 'timings': 15} + 'timings': 15, 'check_neutrality': True, + 'charge_neutrality_tolerance': 1e-12} for key in reference: self.assertIn(key, state) np.testing.assert_almost_equal(state[key], reference[key], @@ -496,12 +496,15 @@ def test_p3m(self): def test_elc(self): actor = self.get_active_actor_of_type(espressomd.electrostatics.ELC) elc_state = actor.get_params() - p3m_state = elc_state['p3m_actor'].get_params() + p3m_state = elc_state['actor'].get_params() p3m_reference = {'prefactor': 1.0, 'accuracy': 0.1, 'mesh': 3 * [10], 'cao': 1, 'alpha': 1.0, 'r_cut': 1.0, 'tune': False, - 'timings': 15} + 'timings': 15, 'check_neutrality': True, + 'charge_neutrality_tolerance': 7e-12} elc_reference = {'gap_size': 6.0, 'maxPWerror': 0.1, - 'delta_mid_top': 0.9, 'delta_mid_bot': 0.1} + 'delta_mid_top': 0.9, 'delta_mid_bot': 0.1, + 'check_neutrality': True, + 'charge_neutrality_tolerance': 5e-12} for key in elc_reference: self.assertIn(key, elc_state) np.testing.assert_almost_equal(elc_state[key], elc_reference[key], @@ -511,42 +514,40 @@ def test_elc(self): np.testing.assert_almost_equal(p3m_state[key], p3m_reference[key], err_msg=f'for parameter {key}') - @ut.skipIf(not espressomd.has_features('SCAFACOS') or - 'SCAFACOS' not in modes or - 'p3m' not in espressomd.scafacos.available_methods(), - "Skipping test due to missing combination or p3m method.") - def test_scafacos(self): + @utx.skipIfMissingFeatures(["SCAFACOS"]) + @utx.skipIfMissingScafacosMethod("p3m") + @ut.skipIf('SCAFACOS' not in modes, "Missing combination.") + def test_scafacos_coulomb(self): actor = self.get_active_actor_of_type( espressomd.electrostatics.Scafacos) state = actor.get_params() reference = {'prefactor': 0.5, 'method_name': 'p3m', 'method_params': { - 'p3m_cao': '7', - 'p3m_r_cut': '1.0', - 'p3m_grid': '64', - 'p3m_alpha': '2.084652'}} + 'p3m_cao': 7, + 'p3m_r_cut': 1.0, + 'p3m_grid': 64, + 'p3m_alpha': 2.084652}} for key in reference: self.assertEqual(state[key], reference[key], msg=f'for {key}') - @ut.skipIf(not espressomd.has_features('SCAFACOS_DIPOLES') or - 'SCAFACOS' not in modes or - 'p2nfft' not in espressomd.scafacos.available_methods(), - "Skipping test due to missing combination or p2nfft method.") + @utx.skipIfMissingFeatures(["SCAFACOS_DIPOLES"]) + @utx.skipIfMissingScafacosMethod("p2nfft") + @ut.skipIf('SCAFACOS' not in modes, "Missing combination.") def test_scafacos_dipoles(self): actor = self.get_active_actor_of_type( espressomd.magnetostatics.Scafacos) state = actor.get_params() reference = {'prefactor': 1.2, 'method_name': 'p2nfft', 'method_params': { - "p2nfft_verbose_tuning": "0", - "pnfft_N": "32,32,32", - "pnfft_n": "32,32,32", + "p2nfft_verbose_tuning": 0, + "pnfft_N": [32, 32, 32], + "pnfft_n": [32, 32, 32], "pnfft_window_name": "bspline", - "pnfft_m": "4", - "p2nfft_ignore_tolerance": "1", - "pnfft_diff_ik": "0", - "p2nfft_r_cut": "11", - "p2nfft_alpha": "0.37"}} + "pnfft_m": 4, + "p2nfft_ignore_tolerance": 1, + "pnfft_diff_ik": 0, + "p2nfft_r_cut": 11, + "p2nfft_alpha": 0.37}} for key in reference: self.assertIn(key, state) self.assertEqual(state[key], reference[key], msg=f'for {key}') diff --git a/testsuite/python/tests_common.py b/testsuite/python/tests_common.py index 85cc76d6ca8..c880f4bfc23 100644 --- a/testsuite/python/tests_common.py +++ b/testsuite/python/tests_common.py @@ -18,35 +18,50 @@ import numpy as np -def assert_params_match(ut_obj, inParams, outParams, msg_long=None): +def is_float(value): + float_types = (float, np.float16, np.float32, np.float64, np.longdouble) + if hasattr(np, 'float128'): + float_types = float_types + (np.float128,) + return isinstance(value, float_types) + + +def is_array(value): + return isinstance(value, (list, tuple, np.ndarray)) + + +def assert_params_match(ut_obj, inParams, outParams, msg_long=""): """Check if the parameters set and gotten back match. Only check keys present in ``inParams``. """ if msg_long: msg_long = "\n" + msg_long - else: - msg_long = "" - for k in inParams.keys(): + for k, value_in in inParams.items(): ut_obj.assertIn(k, outParams) - if isinstance(inParams[k], float): - ut_obj.assertAlmostEqual( - outParams[k], inParams[k], delta=1E-14, - msg=f"Mismatching parameter {k!r}{msg_long}") + value_out = outParams[k] + msg = f"Mismatching parameter {k!r}{msg_long}" + if is_array(value_out) or is_array(value_in): + value_out = np.copy(value_out) + value_in = np.copy(value_in) + if is_float(value_in[0]) or is_float(value_out[0]): + np.testing.assert_allclose( + value_out, value_in, atol=1e-14, err_msg=msg) + else: + np.testing.assert_array_equal(value_out, value_in, err_msg=msg) + elif is_float(value_in) or is_float(value_out): + ut_obj.assertAlmostEqual(value_out, value_in, delta=1e-14, msg=msg) else: - ut_obj.assertEqual( - outParams[k], inParams[k], - msg=f"Mismatching parameter {k!r}{msg_long}") + ut_obj.assertEqual(value_out, value_in, msg=msg) -def generate_test_for_class(_system, _interClass, _params): - """Generates test cases for checking interaction parameters set and gotten back - from Es actually match. Only keys which are present in _params are checked - 1st: Interaction parameters as dictionary, i.e., ``{"k": 1., "r_0": 0}`` - 2nd: Name of the interaction property to set (i.e. ``"P3M"``) +def generate_test_for_actor_class(_system, _class_actor, _params): + """ + Generate a test case for an actor to verify parameters in the interface + and the core match. + Only keys which are present in ``_params`` are checked. """ - params = _params - interClass = _interClass + params_in = _params + class_actor = _class_actor system = _system def func(self): @@ -54,16 +69,15 @@ def func(self): # It will use the state of the variables in the outer function, # which was there, when the outer function was called - # set Parameter - Inter = interClass(**params) - Inter.validate_params() - system.actors.add(Inter) - # Read them out again - outParams = Inter.get_params() - del system.actors[0] + # set parameters + actor = class_actor(**params_in) + system.actors.add(actor) + # read them out again + params_out = {key: getattr(actor, key) for key in params_in} + system.actors.remove(actor) - assert_params_match(self, params, outParams, - f"Parameters set {params} vs. {outParams}") + assert_params_match(self, params_in, params_out, + msg_long=f"Parameters set {params_in} vs. {params_out}") return func diff --git a/testsuite/python/tune_skin.py b/testsuite/python/tune_skin.py index eddd7d6cfa7..65820a4610d 100644 --- a/testsuite/python/tune_skin.py +++ b/testsuite/python/tune_skin.py @@ -33,7 +33,7 @@ def setUp(self): shift="auto") def test_fails_without_adjustment(self): - with self.assertRaisesRegex(Exception, r"calling method tune_skin\(\): .+ \(interaction range too large or min_num_cells too large\)"): + with self.assertRaisesRegex(Exception, "number of cells 2 is smaller than minimum 8: either interaction range is too large for the current skin .+ or min_num_cells too large"): self.system.cell_system.tune_skin( min_skin=0.1, max_skin=0.6, diff --git a/testsuite/python/unittest_decorators.py b/testsuite/python/unittest_decorators.py index 0275a1085e9..a4f438392cc 100644 --- a/testsuite/python/unittest_decorators.py +++ b/testsuite/python/unittest_decorators.py @@ -35,6 +35,13 @@ def skipIfMissingFeatures(*args): return no_skip +def skipIfMissingScafacosMethod(method_name): + """Unittest skipIf decorator for missing ScaFaCoS features.""" + if method_name not in espressomd.code_info.scafacos_methods(): + return unittest.skip(f"ScaFaCoS method '{method_name}' not available") + return no_skip + + def skipIfMissingModules(*args): """Unittest skipIf decorator for missing Python modules.""" if len(args) == 1 and not isinstance(