From a22eae465d505b1721cad2ed58c00b0ed71ae373 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 09:47:47 -0600 Subject: [PATCH 01/41] refactor(cell): add geometry and atom data helpers for PCC Build: cmake --build build_pcc0d --target cell MODULE_CELL_cell_geometry MODULE_CELL_cell_tools --parallel 4 passed. CTest: both focused targets passed (10 tests), OMP_NUM_THREADS=1 outside sandbox. C++11 syntax checks passed for both changed production sources. Governance: no blockers; geometry header needs Vector3 and array for value members, string for validation diagnostics, and vector for input containers. No INPUT or user-visible behavior changes in this foundation step. --- source/Makefile.Objects | 1 + source/source_cell/CMakeLists.txt | 1 + source/source_cell/cell_geometry.cpp | 135 ++++++++++++++ source/source_cell/cell_geometry.h | 53 ++++++ source/source_cell/cell_tools.cpp | 23 +++ source/source_cell/cell_tools.h | 12 ++ source/source_cell/test/CMakeLists.txt | 12 ++ .../source_cell/test/test_cell_geometry.cpp | 165 ++++++++++++++++++ source/source_cell/test/test_cell_tools.cpp | 42 +++++ 9 files changed, 444 insertions(+) create mode 100644 source/source_cell/cell_geometry.cpp create mode 100644 source/source_cell/cell_geometry.h create mode 100644 source/source_cell/test/test_cell_geometry.cpp create mode 100644 source/source_cell/test/test_cell_tools.cpp diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 98d85e38d9c..91b9b98474a 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -220,6 +220,7 @@ OBJS_CELL=atom_pseudo.o\ reciprocal_grid.o\ cell_index.o\ cell_tools.o\ + cell_geometry.o\ record_adj.o\ check_atomic_stru.o\ update_cell.o\ diff --git a/source/source_cell/CMakeLists.txt b/source/source_cell/CMakeLists.txt index b3e574e7144..17a6d6481a5 100644 --- a/source/source_cell/CMakeLists.txt +++ b/source/source_cell/CMakeLists.txt @@ -33,6 +33,7 @@ add_library( parallel_kpoints.cpp cell_index.cpp cell_tools.cpp + cell_geometry.cpp record_adj.cpp check_atomic_stru.cpp update_cell.cpp diff --git a/source/source_cell/cell_geometry.cpp b/source/source_cell/cell_geometry.cpp new file mode 100644 index 00000000000..01745e38551 --- /dev/null +++ b/source/source_cell/cell_geometry.cpp @@ -0,0 +1,135 @@ +#include "cell_geometry.h" + +#include "source_base/matrix3.h" + +#include + +namespace +{ +bool finite_vector(const ModuleBase::Vector3& value) +{ + return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); +} +} // namespace + +namespace unitcell +{ + +bool make_orthogonal_cell(const ModuleBase::Matrix3& lattice, + const double lattice_scale, + const double relative_tolerance, + OrthogonalCell& cell, + std::string& error) +{ + error.clear(); + if (!std::isfinite(lattice_scale) || lattice_scale <= 0.0 + || !std::isfinite(relative_tolerance) || relative_tolerance <= 0.0) + { + error = "cell scale and relative tolerance must be positive and finite"; + return false; + } + + const ModuleBase::Vector3 a(lattice.e11, lattice.e12, lattice.e13); + const ModuleBase::Vector3 b(lattice.e21, lattice.e22, lattice.e23); + const ModuleBase::Vector3 c(lattice.e31, lattice.e32, lattice.e33); + const std::array, 3> vectors = {{a, b, c}}; + OrthogonalCell candidate; + for (int axis = 0; axis < 3; ++axis) + { + const ModuleBase::Vector3 physical_vector = vectors[axis] * lattice_scale; + const double length = physical_vector.norm(); + if (!finite_vector(physical_vector) || !std::isfinite(length) || length <= 0.0) + { + error = "cell lattice vectors must have positive finite lengths"; + return false; + } + candidate.lengths[axis] = length; + candidate.axes[axis] = physical_vector / length; + } + for (int left = 0; left < 3; ++left) + { + for (int right = left + 1; right < 3; ++right) + { + const double projection = candidate.axes[left] * candidate.axes[right]; + if (std::abs(projection) > relative_tolerance) + { + error = "cell lattice vectors must be mutually perpendicular"; + return false; + } + } + } + const ModuleBase::Vector3 vector_sum = a + b + c; + const double half_scale = 0.5 * lattice_scale; + candidate.origin = vector_sum * half_scale; + if (!finite_vector(candidate.origin)) + { + error = "cell center must be finite"; + return false; + } + cell = candidate; + return true; +} + +ModuleBase::Vector3 relative_position(const ModuleBase::Vector3& position, + const OrthogonalCell& cell) +{ + const ModuleBase::Vector3 displacement = position - cell.origin; + ModuleBase::Vector3 relative; + for (int axis = 0; axis < 3; ++axis) + { + const double projection = displacement * cell.axes[axis]; + const double length = cell.lengths[axis]; + const double fractional_image = projection / length + 0.5; + const double image = std::floor(fractional_image); + const double wrapped = projection - length * image; + relative += cell.axes[axis] * wrapped; + } + return relative; +} + +bool weighted_center(const std::vector>& positions, + const std::vector& weights, + const OrthogonalCell& cell, + ModuleBase::Vector3& center, + std::string& error) +{ + error.clear(); + if (positions.empty() || positions.size() != weights.size()) + { + error = "weighted center requires matching non-empty positions and weights"; + return false; + } + OrthogonalCell reference = cell; + reference.origin = positions.front(); + double total_weight = 0.0; + ModuleBase::Vector3 weighted_displacement; + for (std::size_t index = 0; index < positions.size(); ++index) + { + if (!finite_vector(positions[index]) || !std::isfinite(weights[index]) || weights[index] <= 0.0) + { + error = "weighted center requires finite positions and positive finite weights"; + return false; + } + const ModuleBase::Vector3 displacement = relative_position(positions[index], reference); + weighted_displacement += displacement * weights[index]; + total_weight += weights[index]; + } + if (!std::isfinite(total_weight) || !finite_vector(weighted_displacement)) + { + error = "weighted center accumulation must be finite"; + return false; + } + const ModuleBase::Vector3 mean_displacement = weighted_displacement / total_weight; + const ModuleBase::Vector3 unwrapped_center = positions.front() + mean_displacement; + const ModuleBase::Vector3 wrapped_center = relative_position(unwrapped_center, cell); + const ModuleBase::Vector3 candidate = cell.origin + wrapped_center; + if (!finite_vector(candidate)) + { + error = "weighted center must be finite"; + return false; + } + center = candidate; + return true; +} + +} // namespace unitcell diff --git a/source/source_cell/cell_geometry.h b/source/source_cell/cell_geometry.h new file mode 100644 index 00000000000..6f35714056d --- /dev/null +++ b/source/source_cell/cell_geometry.h @@ -0,0 +1,53 @@ +#ifndef CELL_GEOMETRY_H +#define CELL_GEOMETRY_H + +#include "source_base/vector3.h" + +#include +#include +#include + +namespace ModuleBase +{ +class Matrix3; +} + +namespace unitcell +{ + +/// Orthogonal cell in Cartesian coordinates; lengths and origin are in Bohr. +/// Construct with make_orthogonal_cell before using relative_position. +struct OrthogonalCell +{ + std::array, 3> axes; + std::array lengths; + ModuleBase::Vector3 origin; +}; + +/// Accept rectangular cells, including rotated cells. On failure, leave cell +/// unchanged and explain the invalid input in error. No model parameters are read. +bool make_orthogonal_cell(const ModuleBase::Matrix3& lattice, + double lattice_scale, + double relative_tolerance, + OrthogonalCell& cell, + std::string& error); + +/// Minimum-image displacement from origin, with projections in [-L/2, L/2). +/// Requires a cell constructed above and a finite Cartesian position. +ModuleBase::Vector3 relative_position(const ModuleBase::Vector3& position, + const OrthogonalCell& cell); + +/// Unwrap positions about the first position, take their positive-weight center, +/// then wrap the center about cell.origin. Intended for localized systems whose +/// extent about the first position is less than half a cell in each direction. +/// Requires a cell constructed by make_orthogonal_cell. +/// On failure, leave center unchanged and set error. +bool weighted_center(const std::vector>& positions, + const std::vector& weights, + const OrthogonalCell& cell, + ModuleBase::Vector3& center, + std::string& error); + +} // namespace unitcell + +#endif diff --git a/source/source_cell/cell_tools.cpp b/source/source_cell/cell_tools.cpp index 6c380927cb4..624f04145fd 100644 --- a/source/source_cell/cell_tools.cpp +++ b/source/source_cell/cell_tools.cpp @@ -6,6 +6,29 @@ namespace unitcell { + std::vector get_atom_data(const Atom* atoms, const int ntype, const double lattice_scale) + { + int nat = 0; + for (int it = 0; it < ntype; ++it) + { + nat += atoms[it].na; + } + std::vector data; + data.reserve(nat); + for (int it = 0; it < ntype; ++it) + { + for (int ia = 0; ia < atoms[it].na; ++ia) + { + AtomData atom; + atom.position = atoms[it].tau[ia] * lattice_scale; + atom.mass = atoms[it].mass; + atom.valence_charge = atoms[it].ncpp.zv; + data.push_back(atom); + } + } + return data; + } + std::vector get_atomLabels(const Atom* atoms, const int ntype) { std::vector atomLabels(ntype); diff --git a/source/source_cell/cell_tools.h b/source/source_cell/cell_tools.h index 226416166af..037d152a282 100644 --- a/source/source_cell/cell_tools.h +++ b/source/source_cell/cell_tools.h @@ -16,6 +16,18 @@ */ namespace unitcell { + /// Cartesian atom information in type-major order, matching ABACUS atom indices. + struct AtomData + { + ModuleBase::Vector3 position; ///< Bohr + double mass = 0.0; + double valence_charge = 0.0; ///< Positive ionic charge in units of e + }; + + /// Extract positions, masses and pseudopotential valence charges. + /// Requires initialized Atom storage and a positive lattice scale in Bohr. + std::vector get_atom_data(const Atom* atoms, int ntype, double lattice_scale); + /// @brief Get atom labels for each atom type. /// @param atoms atom pointer [in] /// @param ntype number of atom types [in] diff --git a/source/source_cell/test/CMakeLists.txt b/source/source_cell/test/CMakeLists.txt index e0db610c5bf..b58299a9636 100644 --- a/source/source_cell/test/CMakeLists.txt +++ b/source/source_cell/test/CMakeLists.txt @@ -51,6 +51,18 @@ list(APPEND cell_simple_srcs add_library(cell_info OBJECT ${cell_simple_srcs}) +AddTest( + TARGET MODULE_CELL_cell_geometry + LIBS base device + SOURCES test_cell_geometry.cpp ../cell_geometry.cpp +) + +AddTest( + TARGET MODULE_CELL_cell_tools + LIBS base device + SOURCES test_cell_tools.cpp ../cell_tools.cpp ../atom_spec.cpp ../atom_pseudo.cpp ../pseudo.cpp +) + if(NOT ENABLE_MPI) return() endif() diff --git a/source/source_cell/test/test_cell_geometry.cpp b/source/source_cell/test/test_cell_geometry.cpp new file mode 100644 index 00000000000..7dec40d4437 --- /dev/null +++ b/source/source_cell/test/test_cell_geometry.cpp @@ -0,0 +1,165 @@ +#include "source_cell/cell_geometry.h" +#include "source_base/matrix3.h" + +#include + +#include +#include + +namespace +{ +class CellGeometryTest : public testing::Test +{ + protected: + void SetUp() override + { + const ModuleBase::Matrix3 lattice(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell, error)); + } + + unitcell::OrthogonalCell cell; + std::string error; +}; + +void expect_vector(const ModuleBase::Vector3& actual, + const ModuleBase::Vector3& expected) +{ + EXPECT_NEAR(actual.x, expected.x, 1.0e-13); + EXPECT_NEAR(actual.y, expected.y, 1.0e-13); + EXPECT_NEAR(actual.z, expected.z, 1.0e-13); +} +} // namespace + +TEST_F(CellGeometryTest, BuildsRotatedRectangularCell) +{ + const ModuleBase::Matrix3 lattice(0.0, 1.0, 0.0, + -2.0, 0.0, 0.0, + 0.0, 0.0, 3.0); + ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell, error)); + EXPECT_DOUBLE_EQ(cell.lengths[0], 10.0); + EXPECT_DOUBLE_EQ(cell.lengths[1], 20.0); + EXPECT_DOUBLE_EQ(cell.lengths[2], 30.0); + const ModuleBase::Vector3 expected_origin(-10.0, 5.0, 15.0); + expect_vector(cell.origin, expected_origin); + + const ModuleBase::Vector3 displacement(11.0, 6.0, 16.0); + const ModuleBase::Vector3 position = cell.origin + displacement; + const ModuleBase::Vector3 actual = unitcell::relative_position(position, cell); + const ModuleBase::Vector3 expected(-9.0, -4.0, -14.0); + expect_vector(actual, expected); +} + +TEST_F(CellGeometryTest, WrapsMultipleImagesAndUsesHalfOpenInterval) +{ + cell.origin = ModuleBase::Vector3(0.0, 0.0, 0.0); + const ModuleBase::Vector3 position(25.0, -25.0, 31.0); + const ModuleBase::Vector3 actual = unitcell::relative_position(position, cell); + const ModuleBase::Vector3 expected(-5.0, -5.0, 1.0); + expect_vector(actual, expected); +} + +TEST_F(CellGeometryTest, UnwrapsWeightedCenterAcrossCellBoundary) +{ + const std::vector> positions = { + ModuleBase::Vector3(9.8, 9.7, 9.6), + ModuleBase::Vector3(0.2, 0.3, 0.4)}; + const std::vector weights = {1.0, 3.0}; + ModuleBase::Vector3 center; + ASSERT_TRUE(unitcell::weighted_center(positions, weights, cell, center, error)); + const ModuleBase::Vector3 expected(0.1, 0.15, 0.2); + expect_vector(center, expected); +} + +TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) +{ + const std::vector> original = { + ModuleBase::Vector3(9.8, 9.6, 0.1), + ModuleBase::Vector3(0.3, 0.4, 9.7)}; + const std::vector weights = {2.0, 1.0}; + ModuleBase::Vector3 center; + ASSERT_TRUE(unitcell::weighted_center(original, weights, cell, center, error)); + unitcell::OrthogonalCell first = cell; + first.origin = center; + + std::vector> translated = original; + for (auto& position : translated) + { + position.x = std::fmod(position.x + 1.4, 10.0); + position.y = std::fmod(position.y + 1.4, 10.0); + position.z = std::fmod(position.z + 1.4, 10.0); + } + ASSERT_TRUE(unitcell::weighted_center(translated, weights, cell, center, error)); + unitcell::OrthogonalCell second = cell; + second.origin = center; + for (std::size_t index = 0; index < original.size(); ++index) + { + const ModuleBase::Vector3 expected = unitcell::relative_position(original[index], first); + const ModuleBase::Vector3 actual = unitcell::relative_position(translated[index], second); + expect_vector(actual, expected); + } +} + +TEST_F(CellGeometryTest, RejectsSkewAndDegenerateCellsWithoutChangingResult) +{ + const ModuleBase::Vector3 original_origin = cell.origin; + const ModuleBase::Matrix3 skew(1.0, 0.0, 0.0, + 0.1, 1.0, 0.0, + 0.0, 0.0, 1.0); + EXPECT_FALSE(unitcell::make_orthogonal_cell(skew, 10.0, 1.0e-10, cell, error)); + EXPECT_FALSE(error.empty()); + expect_vector(cell.origin, original_origin); + const ModuleBase::Matrix3 degenerate(1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0); + EXPECT_FALSE(unitcell::make_orthogonal_cell(degenerate, 10.0, 1.0e-10, cell, error)); + expect_vector(cell.origin, original_origin); +} + +TEST_F(CellGeometryTest, RejectsInvalidScaleToleranceAndNonfiniteLattice) +{ + const ModuleBase::Matrix3 identity(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + const double infinity = std::numeric_limits::infinity(); + EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, 0.0, 1.0e-10, cell, error)); + EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, infinity, 1.0e-10, cell, error)); + EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, 10.0, 0.0, cell, error)); + EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, 10.0, infinity, cell, error)); + const ModuleBase::Matrix3 nonfinite(infinity, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + EXPECT_FALSE(unitcell::make_orthogonal_cell(nonfinite, 10.0, 1.0e-10, cell, error)); + ASSERT_TRUE(unitcell::make_orthogonal_cell(identity, 10.0, 1.0e-10, cell, error)); + EXPECT_TRUE(error.empty()); +} + +TEST_F(CellGeometryTest, RejectsInvalidCenterInputsWithoutChangingResult) +{ + std::vector> positions = { + ModuleBase::Vector3(1.0, 2.0, 3.0)}; + const std::vector empty_weights; + const std::vector zero_weights = {0.0}; + const std::vector weights = {1.0}; + const ModuleBase::Vector3 original(4.0, 5.0, 6.0); + ModuleBase::Vector3 center = original; + EXPECT_FALSE(unitcell::weighted_center(positions, empty_weights, cell, center, error)); + EXPECT_FALSE(unitcell::weighted_center(positions, zero_weights, cell, center, error)); + positions[0].x = std::numeric_limits::quiet_NaN(); + EXPECT_FALSE(unitcell::weighted_center(positions, weights, cell, center, error)); + expect_vector(center, original); + EXPECT_FALSE(error.empty()); +} + +TEST_F(CellGeometryTest, RejectsOverflowingCenterAccumulation) +{ + const std::vector> positions = { + ModuleBase::Vector3(1.0, 1.0, 1.0), + ModuleBase::Vector3(2.0, 2.0, 2.0)}; + const double maximum = std::numeric_limits::max(); + const std::vector weights = {maximum, maximum}; + ModuleBase::Vector3 center; + EXPECT_FALSE(unitcell::weighted_center(positions, weights, cell, center, error)); + EXPECT_FALSE(error.empty()); +} diff --git a/source/source_cell/test/test_cell_tools.cpp b/source/source_cell/test/test_cell_tools.cpp new file mode 100644 index 00000000000..d3b9af4b966 --- /dev/null +++ b/source/source_cell/test/test_cell_tools.cpp @@ -0,0 +1,42 @@ +#include "source_cell/cell_tools.h" + +#include + +TEST(CellTools, ExtractsPositionsMassesAndValenceChargesInAtomOrder) +{ + Atom atoms[2]; + atoms[0].na = 2; + atoms[0].mass = 12.0; + atoms[0].ncpp.zv = 4.0; + atoms[0].tau = {ModuleBase::Vector3(0.1, 0.2, 0.3), + ModuleBase::Vector3(0.4, 0.5, 0.6)}; + atoms[1].na = 1; + atoms[1].mass = 1.0; + atoms[1].ncpp.zv = 1.0; + atoms[1].tau = {ModuleBase::Vector3(0.7, 0.8, 0.9)}; + + const std::vector data = unitcell::get_atom_data(atoms, 2, 10.0); + ASSERT_EQ(data.size(), 3u); + EXPECT_DOUBLE_EQ(data[0].position.x, 1.0); + EXPECT_DOUBLE_EQ(data[0].position.y, 2.0); + EXPECT_DOUBLE_EQ(data[0].position.z, 3.0); + EXPECT_DOUBLE_EQ(data[1].position.x, 4.0); + EXPECT_DOUBLE_EQ(data[1].position.y, 5.0); + EXPECT_DOUBLE_EQ(data[1].position.z, 6.0); + EXPECT_DOUBLE_EQ(data[2].position.x, 7.0); + EXPECT_DOUBLE_EQ(data[2].position.y, 8.0); + EXPECT_DOUBLE_EQ(data[2].position.z, 9.0); + EXPECT_DOUBLE_EQ(data[0].mass, 12.0); + EXPECT_DOUBLE_EQ(data[1].mass, 12.0); + EXPECT_DOUBLE_EQ(data[2].mass, 1.0); + EXPECT_DOUBLE_EQ(data[0].valence_charge, 4.0); + EXPECT_DOUBLE_EQ(data[1].valence_charge, 4.0); + EXPECT_DOUBLE_EQ(data[2].valence_charge, 1.0); + EXPECT_DOUBLE_EQ(atoms[0].tau[0].x, 0.1); +} + +TEST(CellTools, AcceptsNoAtomTypes) +{ + const std::vector data = unitcell::get_atom_data(nullptr, 0, 10.0); + EXPECT_TRUE(data.empty()); +} From 146b18c8333df2bbf8db9a5433fb033c118d9518 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 09:53:20 -0600 Subject: [PATCH 02/41] feat(estate): extract standalone PCC 0D numerical kernels Place charge moments and PCC formulas beside makov_payne; geometry and atom extraction remain in source_cell. No INPUT behavior changes yet. Built both focused targets and passed 11 tests outside sandbox with OMP_NUM_THREADS=1, including energy/potential and ionic-force finite differences. C++11 syntax checks passed. Governance has no blockers; includes define the value moment type, diagnostic string and kernel interface. --- source/Makefile.Objects | 2 + source/source_estate/CMakeLists.txt | 2 + source/source_estate/charge_moments.cpp | 57 +++++++ source/source_estate/charge_moments.h | 33 ++++ source/source_estate/pcc_0d.cpp | 91 ++++++++++ source/source_estate/pcc_0d.h | 52 ++++++ source/source_estate/test/CMakeLists.txt | 12 ++ .../test/test_charge_moments.cpp | 74 ++++++++ source/source_estate/test/test_pcc_0d.cpp | 158 ++++++++++++++++++ 9 files changed, 481 insertions(+) create mode 100644 source/source_estate/charge_moments.cpp create mode 100644 source/source_estate/charge_moments.h create mode 100644 source/source_estate/pcc_0d.cpp create mode 100644 source/source_estate/pcc_0d.h create mode 100644 source/source_estate/test/test_charge_moments.cpp create mode 100644 source/source_estate/test/test_pcc_0d.cpp diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 91b9b98474a..c18ed24b74c 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -271,6 +271,8 @@ OBJS_ELECSTAT=elecstate.o\ elecstate_pw_sdft.o\ elecstate_pw_cal_tau.o\ makov_payne.o\ + charge_moments.o\ + pcc_0d.o\ elecstate_op.o\ efield.o\ gatefield.o\ diff --git a/source/source_estate/CMakeLists.txt b/source/source_estate/CMakeLists.txt index 2aed5421060..8898bde9456 100644 --- a/source/source_estate/CMakeLists.txt +++ b/source/source_estate/CMakeLists.txt @@ -11,6 +11,8 @@ list(APPEND objects elecstate_pw_sdft.cpp elecstate_pw_cal_tau.cpp makov_payne.cpp + charge_moments.cpp + pcc_0d.cpp module_pot/gatefield.cpp module_pot/efield.cpp module_pot/h_hartree_pw.cpp diff --git a/source/source_estate/charge_moments.cpp b/source/source_estate/charge_moments.cpp new file mode 100644 index 00000000000..054f742fda2 --- /dev/null +++ b/source/source_estate/charge_moments.cpp @@ -0,0 +1,57 @@ +#include "charge_moments.h" + +#include + +namespace elecstate +{ + +bool charge_moments(const double* charges, + const ModuleBase::Vector3* relative_positions, + const int count, + const double weight, + ChargeMoments& moments, + std::string& error) +{ + error.clear(); + if (count < 0 || (count > 0 && (charges == nullptr || relative_positions == nullptr)) + || !std::isfinite(weight) || weight <= 0.0) + { + error = "charge moments require valid sample storage and a positive finite weight"; + return false; + } + ChargeMoments candidate; + for (int index = 0; index < count; ++index) + { + const double charge = charges[index] * weight; + const ModuleBase::Vector3& relative = relative_positions[index]; + if (!std::isfinite(charge) || !std::isfinite(relative.x) + || !std::isfinite(relative.y) || !std::isfinite(relative.z)) + { + error = "charge moments require finite charges and relative positions"; + return false; + } + candidate.charge += charge; + candidate.dipole += relative * charge; + candidate.second_moment += charge * relative.norm2(); + } + if (!std::isfinite(candidate.charge) || !std::isfinite(candidate.dipole.x) + || !std::isfinite(candidate.dipole.y) || !std::isfinite(candidate.dipole.z) + || !std::isfinite(candidate.second_moment)) + { + error = "charge moment accumulation must be finite"; + return false; + } + moments = candidate; + return true; +} + +ChargeMoments add_charge_moments(const ChargeMoments& left, const ChargeMoments& right) +{ + ChargeMoments result; + result.charge = left.charge + right.charge; + result.dipole = left.dipole + right.dipole; + result.second_moment = left.second_moment + right.second_moment; + return result; +} + +} // namespace elecstate diff --git a/source/source_estate/charge_moments.h b/source/source_estate/charge_moments.h new file mode 100644 index 00000000000..f3fac2e9251 --- /dev/null +++ b/source/source_estate/charge_moments.h @@ -0,0 +1,33 @@ +#ifndef CHARGE_MOMENTS_H +#define CHARGE_MOMENTS_H + +#include "source_base/vector3.h" + +#include + +namespace elecstate +{ + +/// Moments about one common origin. Charges carry their physical sign. +struct ChargeMoments +{ + double charge = 0.0; + ModuleBase::Vector3 dipole; + double second_moment = 0.0; ///< Trace of the second moment, in e Bohr^2 +}; + +/// Local moments only; no MPI or cell/model dependencies. Use weight=1 for +/// point charges and the volume element for a signed grid density. +/// Zero samples are permitted. On failure, leave moments unchanged and set error. +bool charge_moments(const double* charges, + const ModuleBase::Vector3* relative_positions, + int count, + double weight, + ChargeMoments& moments, + std::string& error); + +ChargeMoments add_charge_moments(const ChargeMoments& left, const ChargeMoments& right); + +} // namespace elecstate + +#endif diff --git a/source/source_estate/pcc_0d.cpp b/source/source_estate/pcc_0d.cpp new file mode 100644 index 00000000000..84a4c72039b --- /dev/null +++ b/source/source_estate/pcc_0d.cpp @@ -0,0 +1,91 @@ +#include "pcc_0d.h" + +#include "source_base/constants.h" +#include "source_cell/cell_geometry.h" + +#include + +namespace elecstate +{ + +bool make_pcc_0d_parameters(const unitcell::OrthogonalCell& cell, + const double relative_tolerance, + Pcc0dParameters& parameters, + std::string& error) +{ + error.clear(); + if (!std::isfinite(relative_tolerance) || relative_tolerance <= 0.0) + { + error = "PCC 0D requires a positive finite geometry tolerance"; + return false; + } + const double length = cell.lengths[0]; + for (int axis = 0; axis < 3; ++axis) + { + const double edge = cell.lengths[axis]; + const double difference = edge - length; + if (!std::isfinite(edge) || edge <= 0.0 || std::abs(difference) > relative_tolerance * length) + { + error = "PCC 0D requires an equal-edge cubic cell"; + return false; + } + } + Pcc0dParameters candidate; + candidate.length = length; + parameters = candidate; + return true; +} + +double pcc_0d_potential(const ChargeMoments& moments, + const ModuleBase::Vector3& position, + const Pcc0dParameters& parameters) +{ + const double length = parameters.length; + const double volume = length * length * length; + const double dipole_projection = moments.dipole * position; + const double parabolic = moments.charge * position.norm2() + - 2.0 * dipole_projection + moments.second_moment; + return parameters.madelung * moments.charge / length + - 2.0 * ModuleBase::PI * parabolic / (3.0 * volume); +} + +ModuleBase::Vector3 pcc_0d_gradient(const ChargeMoments& moments, + const ModuleBase::Vector3& position, + const Pcc0dParameters& parameters) +{ + const double length = parameters.length; + const double volume = length * length * length; + const double factor = -4.0 * ModuleBase::PI / (3.0 * volume); + const ModuleBase::Vector3 field = position * moments.charge - moments.dipole; + return field * factor; +} + +double pcc_0d_bilinear_energy(const ChargeMoments& left, + const ChargeMoments& right, + const Pcc0dParameters& parameters) +{ + const double length = parameters.length; + const double volume = length * length * length; + const double monopole = parameters.madelung * left.charge * right.charge / length; + const double dipole_product = left.dipole * right.dipole; + const double multipole = left.second_moment * right.charge + + left.charge * right.second_moment - 2.0 * dipole_product; + return monopole - 2.0 * ModuleBase::PI * multipole / (3.0 * volume); +} + +double pcc_0d_energy(const ChargeMoments& moments, const Pcc0dParameters& parameters) +{ + const double bilinear = pcc_0d_bilinear_energy(moments, moments, parameters); + return 0.5 * bilinear; +} + +ModuleBase::Vector3 pcc_0d_force(const ChargeMoments& moments, + const double ionic_charge, + const ModuleBase::Vector3& position, + const Pcc0dParameters& parameters) +{ + const ModuleBase::Vector3 gradient = pcc_0d_gradient(moments, position, parameters); + return gradient * (-ionic_charge); +} + +} // namespace elecstate diff --git a/source/source_estate/pcc_0d.h b/source/source_estate/pcc_0d.h new file mode 100644 index 00000000000..8d0bbd994d2 --- /dev/null +++ b/source/source_estate/pcc_0d.h @@ -0,0 +1,52 @@ +#ifndef PCC_0D_H +#define PCC_0D_H + +#include "charge_moments.h" + +namespace unitcell +{ +struct OrthogonalCell; +} + +namespace elecstate +{ + +struct Pcc0dParameters +{ + double length = 0.0; ///< Cubic cell edge in Bohr + double madelung = 2.837297479480619; +}; + +/// Check the equal-edge constraint after constructing an OrthogonalCell. +/// Leave parameters unchanged on failure. No cell geometry is stored here. +bool make_pcc_0d_parameters(const unitcell::OrthogonalCell& cell, + double relative_tolerance, + Pcc0dParameters& parameters, + std::string& error); + +/// Numerical kernels require validated parameters and moments about a common +/// origin. Coordinates are relative to that origin. Energies/potentials are in +/// Hartree; gradients/forces are in Hartree/Bohr. The potential is for unit +/// positive charge; electron potential conversion belongs to the host adapter. +double pcc_0d_potential(const ChargeMoments& moments, + const ModuleBase::Vector3& position, + const Pcc0dParameters& parameters); + +ModuleBase::Vector3 pcc_0d_gradient(const ChargeMoments& moments, + const ModuleBase::Vector3& position, + const Pcc0dParameters& parameters); + +double pcc_0d_bilinear_energy(const ChargeMoments& left, + const ChargeMoments& right, + const Pcc0dParameters& parameters); + +double pcc_0d_energy(const ChargeMoments& moments, const Pcc0dParameters& parameters); + +ModuleBase::Vector3 pcc_0d_force(const ChargeMoments& moments, + double ionic_charge, + const ModuleBase::Vector3& position, + const Pcc0dParameters& parameters); + +} // namespace elecstate + +#endif diff --git a/source/source_estate/test/CMakeLists.txt b/source/source_estate/test/CMakeLists.txt index d3aecdbef57..5360087e94f 100644 --- a/source/source_estate/test/CMakeLists.txt +++ b/source/source_estate/test/CMakeLists.txt @@ -7,6 +7,18 @@ abacus_disable_feature_definitions(__ROCM) abacus_disable_feature_definitions(__MLALGO) abacus_disable_feature_definitions(_OPENMP) +AddTest( + TARGET MODULE_ESTATE_charge_moments + LIBS base device + SOURCES test_charge_moments.cpp ../charge_moments.cpp +) + +AddTest( + TARGET MODULE_ESTATE_pcc_0d + LIBS base device + SOURCES test_pcc_0d.cpp ../pcc_0d.cpp ../charge_moments.cpp ../../source_cell/cell_geometry.cpp +) + if (ENABLE_MPI) # Copy at configure time so a plain `make` + `ctest` run finds the data. diff --git a/source/source_estate/test/test_charge_moments.cpp b/source/source_estate/test/test_charge_moments.cpp new file mode 100644 index 00000000000..766e411045a --- /dev/null +++ b/source/source_estate/test/test_charge_moments.cpp @@ -0,0 +1,74 @@ +#include "source_estate/charge_moments.h" + +#include + +#include + +TEST(ChargeMoments, IntegratesSignedDensityWithVolumeElement) +{ + const double charges[2] = {2.0, -1.0}; + const ModuleBase::Vector3 positions[2] = { + ModuleBase::Vector3(1.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 2.0, 0.0)}; + elecstate::ChargeMoments moments; + std::string error; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 0.5, moments, error)); + EXPECT_DOUBLE_EQ(moments.charge, 0.5); + EXPECT_DOUBLE_EQ(moments.dipole.x, 1.0); + EXPECT_DOUBLE_EQ(moments.dipole.y, -1.0); + EXPECT_DOUBLE_EQ(moments.dipole.z, 0.0); + EXPECT_DOUBLE_EQ(moments.second_moment, -1.0); +} + +TEST(ChargeMoments, AddsIndependentContributionsAndAcceptsEmptyLocalGrid) +{ + elecstate::ChargeMoments ions; + ions.charge = 2.0; + ions.dipole.x = 3.0; + ions.second_moment = 4.0; + elecstate::ChargeMoments electrons; + electrons.charge = -1.0; + electrons.dipole.y = -2.0; + electrons.second_moment = -3.0; + const elecstate::ChargeMoments total = elecstate::add_charge_moments(ions, electrons); + EXPECT_DOUBLE_EQ(total.charge, 1.0); + EXPECT_DOUBLE_EQ(total.dipole.x, 3.0); + EXPECT_DOUBLE_EQ(total.dipole.y, -2.0); + EXPECT_DOUBLE_EQ(total.second_moment, 1.0); + + std::string error; + ASSERT_TRUE(elecstate::charge_moments(nullptr, nullptr, 0, 1.0, electrons, error)); + EXPECT_DOUBLE_EQ(electrons.charge, 0.0); + EXPECT_DOUBLE_EQ(electrons.second_moment, 0.0); +} + +TEST(ChargeMoments, RejectsInvalidStorageAndNonfiniteValuesWithoutChangingResult) +{ + double charge = 1.0; + ModuleBase::Vector3 position(1.0, 2.0, 3.0); + elecstate::ChargeMoments moments; + moments.charge = 7.0; + std::string error; + EXPECT_FALSE(elecstate::charge_moments(nullptr, &position, 1, 1.0, moments, error)); + EXPECT_FALSE(elecstate::charge_moments(&charge, nullptr, 1, 1.0, moments, error)); + EXPECT_FALSE(elecstate::charge_moments(&charge, &position, -1, 1.0, moments, error)); + EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 0.0, moments, error)); + charge = std::numeric_limits::infinity(); + EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 1.0, moments, error)); + charge = 1.0; + position.x = std::numeric_limits::quiet_NaN(); + EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 1.0, moments, error)); + EXPECT_DOUBLE_EQ(moments.charge, 7.0); + EXPECT_FALSE(error.empty()); +} + +TEST(ChargeMoments, RejectsOverflowingMoments) +{ + const double charge = 1.0; + const double maximum = std::numeric_limits::max(); + const ModuleBase::Vector3 position(maximum, 0.0, 0.0); + elecstate::ChargeMoments moments; + std::string error; + EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 1.0, moments, error)); + EXPECT_DOUBLE_EQ(moments.charge, 0.0); +} diff --git a/source/source_estate/test/test_pcc_0d.cpp b/source/source_estate/test/test_pcc_0d.cpp new file mode 100644 index 00000000000..a9b4f7c5268 --- /dev/null +++ b/source/source_estate/test/test_pcc_0d.cpp @@ -0,0 +1,158 @@ +#include "source_estate/pcc_0d.h" +#include "source_base/constants.h" +#include "source_base/matrix3.h" +#include "source_cell/cell_geometry.h" + +#include + +namespace +{ +class Pcc0dTest : public testing::Test +{ + protected: + void SetUp() override + { + const ModuleBase::Matrix3 lattice(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell, error)); + ASSERT_TRUE(elecstate::make_pcc_0d_parameters(cell, 1.0e-10, parameters, error)); + moments.charge = 1.2; + moments.dipole = ModuleBase::Vector3(0.1, -0.2, 0.3); + moments.second_moment = 0.8; + } + + unitcell::OrthogonalCell cell; + elecstate::Pcc0dParameters parameters; + elecstate::ChargeMoments moments; + std::string error; +}; +} // namespace + +TEST_F(Pcc0dTest, SeparatesCubicRestrictionFromOrthogonalGeometry) +{ + EXPECT_DOUBLE_EQ(parameters.length, 10.0); + const ModuleBase::Matrix3 rectangular(1.0, 0.0, 0.0, + 0.0, 2.0, 0.0, + 0.0, 0.0, 1.0); + ASSERT_TRUE(unitcell::make_orthogonal_cell(rectangular, 10.0, 1.0e-10, cell, error)); + EXPECT_FALSE(elecstate::make_pcc_0d_parameters(cell, 1.0e-10, parameters, error)); + EXPECT_DOUBLE_EQ(parameters.length, 10.0); + EXPECT_FALSE(error.empty()); + EXPECT_FALSE(elecstate::make_pcc_0d_parameters(cell, 0.0, parameters, error)); +} + +TEST_F(Pcc0dTest, MatchesAnalyticalMonopoleAndNeutralDipoleEnergy) +{ + elecstate::ChargeMoments monopole; + monopole.charge = 2.0; + const double actual_monopole = elecstate::pcc_0d_energy(monopole, parameters); + const double expected_monopole = 0.5 * parameters.madelung * 4.0 / parameters.length; + EXPECT_NEAR(actual_monopole, expected_monopole, 1.0e-14); + + elecstate::ChargeMoments neutral; + neutral.dipole = ModuleBase::Vector3(1.0, 2.0, 3.0); + const double actual_neutral = elecstate::pcc_0d_energy(neutral, parameters); + const double expected_neutral = 2.0 * ModuleBase::PI * 14.0 / 3000.0; + EXPECT_NEAR(actual_neutral, expected_neutral, 1.0e-14); +} + +TEST_F(Pcc0dTest, BilinearKernelIsSymmetricAndHasConsistentSelfEnergy) +{ + elecstate::ChargeMoments other; + other.charge = -0.4; + other.dipole = ModuleBase::Vector3(0.7, 0.5, -0.1); + other.second_moment = 1.3; + const double forward = elecstate::pcc_0d_bilinear_energy(moments, other, parameters); + const double reverse = elecstate::pcc_0d_bilinear_energy(other, moments, parameters); + EXPECT_NEAR(forward, reverse, 1.0e-14); + const elecstate::ChargeMoments sum = elecstate::add_charge_moments(moments, other); + const double sum_energy = elecstate::pcc_0d_energy(sum, parameters); + const double left_energy = elecstate::pcc_0d_energy(moments, parameters); + const double right_energy = elecstate::pcc_0d_energy(other, parameters); + const double expected = left_energy + right_energy + forward; + EXPECT_NEAR(sum_energy, expected, 1.0e-14); +} + +TEST_F(Pcc0dTest, GradientMatchesPotentialFiniteDifferences) +{ + const ModuleBase::Vector3 position(0.4, -0.5, 0.6); + const ModuleBase::Vector3 gradient = elecstate::pcc_0d_gradient(moments, position, parameters); + const double expected[3] = {gradient.x, gradient.y, gradient.z}; + const double step = 1.0e-5; + for (int axis = 0; axis < 3; ++axis) + { + ModuleBase::Vector3 plus = position; + ModuleBase::Vector3 minus = position; + double* plus_component[3] = {&plus.x, &plus.y, &plus.z}; + double* minus_component[3] = {&minus.x, &minus.y, &minus.z}; + *plus_component[axis] += step; + *minus_component[axis] -= step; + const double plus_potential = elecstate::pcc_0d_potential(moments, plus, parameters); + const double minus_potential = elecstate::pcc_0d_potential(moments, minus, parameters); + const double derivative = (plus_potential - minus_potential) / (2.0 * step); + EXPECT_NEAR(derivative, expected[axis], 1.0e-10); + } +} + +TEST_F(Pcc0dTest, IonicForceMatchesTotalEnergyFiniteDifferences) +{ + const double charges[2] = {1.3, -0.4}; + ModuleBase::Vector3 positions[2] = { + ModuleBase::Vector3(0.2, -0.3, 0.4), + ModuleBase::Vector3(-0.5, 0.6, -0.7)}; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const ModuleBase::Vector3 force = elecstate::pcc_0d_force(moments, charges[0], positions[0], parameters); + const double expected[3] = {force.x, force.y, force.z}; + const double step = 1.0e-5; + for (int axis = 0; axis < 3; ++axis) + { + double* component[3] = {&positions[0].x, &positions[0].y, &positions[0].z}; + const double original = *component[axis]; + *component[axis] = original + step; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const double plus_energy = elecstate::pcc_0d_energy(moments, parameters); + *component[axis] = original - step; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const double minus_energy = elecstate::pcc_0d_energy(moments, parameters); + *component[axis] = original; + const double numerical_force = -(plus_energy - minus_energy) / (2.0 * step); + EXPECT_NEAR(numerical_force, expected[axis], 1.0e-10); + } +} + +TEST_F(Pcc0dTest, ElectronPotentialMatchesEnergyDerivative) +{ + const ModuleBase::Vector3 position(0.2, -0.3, 0.4); + const double positive_potential = elecstate::pcc_0d_potential(moments, position, parameters); + const double electron_potential_ry = -2.0 * positive_potential; + const double step = 1.0e-5; + elecstate::ChargeMoments plus = moments; + plus.charge -= step; + plus.dipole -= position * step; + plus.second_moment -= step * position.norm2(); + elecstate::ChargeMoments minus = moments; + minus.charge += step; + minus.dipole += position * step; + minus.second_moment += step * position.norm2(); + const double plus_energy = 2.0 * elecstate::pcc_0d_energy(plus, parameters); + const double minus_energy = 2.0 * elecstate::pcc_0d_energy(minus, parameters); + const double derivative = (plus_energy - minus_energy) / (2.0 * step); + EXPECT_NEAR(derivative, electron_potential_ry, 1.0e-10); +} + +TEST_F(Pcc0dTest, EnergyIsIndependentOfMultipoleOrigin) +{ + const double charges[2] = {1.2, -0.3}; + ModuleBase::Vector3 positions[2] = { + ModuleBase::Vector3(0.3, -0.2, 0.1), + ModuleBase::Vector3(-0.5, 0.6, -0.7)}; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const double original_energy = elecstate::pcc_0d_energy(moments, parameters); + const ModuleBase::Vector3 shift(0.4, -0.3, 0.2); + positions[0] -= shift; + positions[1] -= shift; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const double shifted_energy = elecstate::pcc_0d_energy(moments, parameters); + EXPECT_NEAR(original_energy, shifted_energy, 1.0e-14); +} From 6a1069cffd9c96cf63c702cd77aea471a4531242 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 10:15:23 -0600 Subject: [PATCH 03/41] feat(estate): integrate vacuum PCC 0D as a potential component Register standalone PCC for CPU KS-DFT PW/LCAO, add its energy once, and expose force and electrostatic output from the same component result. Generated INPUT YAML/Markdown from the new CLI. Full abacus_basic_para build and focused potential, energy, INPUT and grid tests passed. Neutral water, charged H3O+, LCAO and two-rank MPI SCF runs converged; old H3O+ energy/forces match at printed precision. C++11 syntax and staged governance checks passed without blockers. Remaining PARAM read is the existing PW registration dependency (net zero); header includes provide value member types, base class and standard containers/diagnostics. --- docs/advanced/input_files/input-main.md | 3 +- docs/parameters.yaml | 3 +- source/Makefile.Objects | 2 + source/source_basis/module_pw/CMakeLists.txt | 1 + .../module_pw/pw_grid_geometry.cpp | 48 ++++ .../source_basis/module_pw/pw_grid_geometry.h | 27 ++ .../module_pw/test_serial/CMakeLists.txt | 7 + .../test_serial/test_pw_grid_geometry.cpp | 50 ++++ source/source_estate/CMakeLists.txt | 1 + source/source_estate/elecstate_energy.cpp | 4 + source/source_estate/elecstate_print.cpp | 6 + source/source_estate/module_pot/pot_pcc.cpp | 232 ++++++++++++++++++ source/source_estate/module_pot/pot_pcc.h | 35 +++ .../module_pot/potential_new.cpp | 43 ++++ .../source_estate/module_pot/potential_new.h | 7 + .../module_pot/potential_types.cpp | 5 + source/source_estate/test/CMakeLists.txt | 13 + .../test/elecstate_energy_test.cpp | 17 ++ source/source_estate/test/test_pot_pcc.cpp | 152 ++++++++++++ source/source_estate/write_elecstat_pot.cpp | 9 + source/source_estate/write_elecstat_pot.h | 1 + .../source_io/module_ctrl/ctrl_output_fp.cpp | 2 + .../module_parameter/read_inp_sys.cpp | 16 +- .../test_serial/read_input_item_test.cpp | 39 +++ source/source_lcao/force_stress_assemble.cpp | 4 + source/source_lcao/force_stress_lcao.cpp | 5 + source/source_lcao/force_stress_lcao.h | 1 + source/source_lcao/hamilt_lcao.cpp | 1 + source/source_pw/module_pwdft/force_pw.cpp | 4 +- source/source_pw/module_pwdft/hamilt_pw.cpp | 7 +- 30 files changed, 739 insertions(+), 6 deletions(-) create mode 100644 source/source_basis/module_pw/pw_grid_geometry.cpp create mode 100644 source/source_basis/module_pw/pw_grid_geometry.h create mode 100644 source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp create mode 100644 source/source_estate/module_pot/pot_pcc.cpp create mode 100644 source/source_estate/module_pot/pot_pcc.h create mode 100644 source/source_estate/test/test_pot_pcc.cpp diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index e06d4e873bd..5478038a0e7 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -768,8 +768,9 @@ - none: regular periodic calculation without isolated-system correction. - makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). + - pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. - Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995). + Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014). - **Default**: none ### init_wfc diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 9a27e790dbc..2e329c503be 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -179,8 +179,9 @@ parameters: Available options are: * none: regular periodic calculation without isolated-system correction. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). + * pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. - Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995). + Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014). default_value: none unit: "" availability: "" diff --git a/source/Makefile.Objects b/source/Makefile.Objects index c18ed24b74c..d3313138c34 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -273,6 +273,7 @@ OBJS_ELECSTAT=elecstate.o\ makov_payne.o\ charge_moments.o\ pcc_0d.o\ + pot_pcc.o\ elecstate_op.o\ efield.o\ gatefield.o\ @@ -559,6 +560,7 @@ OBJS_PSI_INITIALIZER=psi_base.o\ OBJS_PW=fft_bundle.o\ fft_cpu.o\ pw_basis.o\ + pw_grid_geometry.o\ pw_basis_k.o\ pw_basis_sup.o\ pw_distributeg.o\ diff --git a/source/source_basis/module_pw/CMakeLists.txt b/source/source_basis/module_pw/CMakeLists.txt index d2abf09b418..12d037dab73 100644 --- a/source/source_basis/module_pw/CMakeLists.txt +++ b/source/source_basis/module_pw/CMakeLists.txt @@ -22,6 +22,7 @@ endif() list(APPEND objects pw_basis.cpp + pw_grid_geometry.cpp pw_basis_k.cpp pw_basis_sup.cpp pw_distributeg.cpp diff --git a/source/source_basis/module_pw/pw_grid_geometry.cpp b/source/source_basis/module_pw/pw_grid_geometry.cpp new file mode 100644 index 00000000000..ccac0c0b3f7 --- /dev/null +++ b/source/source_basis/module_pw/pw_grid_geometry.cpp @@ -0,0 +1,48 @@ +#include "pw_grid_geometry.h" + +#include "pw_basis.h" +#include "source_base/matrix3.h" + +#include + +namespace ModulePW +{ +bool grid_positions(const PW_Basis& basis, + const ModuleBase::Matrix3& lattice, + const double lattice_scale, + std::vector>& positions, + std::string& error) +{ + error.clear(); + if (basis.nx <= 0 || basis.ny <= 0 || basis.nz <= 0 || basis.nplane < 0 + || basis.nrxx != basis.nx * basis.ny * basis.nplane + || basis.startz_current < 0 || basis.startz_current + basis.nplane > basis.nz + || !std::isfinite(lattice_scale) || lattice_scale <= 0.0) + { + error = "PW grid coordinates require valid dimensions, slab offset and lattice scale"; + return false; + } + const ModuleBase::Vector3 a1(lattice.e11, lattice.e12, lattice.e13); + const ModuleBase::Vector3 a2(lattice.e21, lattice.e22, lattice.e23); + const ModuleBase::Vector3 a3(lattice.e31, lattice.e32, lattice.e33); + std::vector> candidate(basis.nrxx); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + const int ix = ir / (basis.ny * basis.nplane); + const int iy = ir / basis.nplane - ix * basis.ny; + const int iz = ir % basis.nplane + basis.startz_current; + const double fx = static_cast(ix) / basis.nx; + const double fy = static_cast(iy) / basis.ny; + const double fz = static_cast(iz) / basis.nz; + const ModuleBase::Vector3 position = (a1 * fx + a2 * fy + a3 * fz) * lattice_scale; + if (!std::isfinite(position.x) || !std::isfinite(position.y) || !std::isfinite(position.z)) + { + error = "PW grid coordinates must be finite"; + return false; + } + candidate[ir] = position; + } + positions.swap(candidate); + return true; +} +} // namespace ModulePW diff --git a/source/source_basis/module_pw/pw_grid_geometry.h b/source/source_basis/module_pw/pw_grid_geometry.h new file mode 100644 index 00000000000..52390ad19ce --- /dev/null +++ b/source/source_basis/module_pw/pw_grid_geometry.h @@ -0,0 +1,27 @@ +#ifndef PW_GRID_GEOMETRY_H +#define PW_GRID_GEOMETRY_H + +#include "source_base/vector3.h" + +#include +#include + +namespace ModuleBase +{ +class Matrix3; +} + +namespace ModulePW +{ +class PW_Basis; + +/// Cartesian coordinates in Bohr of this rank's integer FFT grid nodes. +/// Supports an empty local slab; on failure, leave positions unchanged. +bool grid_positions(const PW_Basis& basis, + const ModuleBase::Matrix3& lattice, + double lattice_scale, + std::vector>& positions, + std::string& error); +} // namespace ModulePW + +#endif diff --git a/source/source_basis/module_pw/test_serial/CMakeLists.txt b/source/source_basis/module_pw/test_serial/CMakeLists.txt index 3ef55984a4d..0ab7d3216a9 100644 --- a/source/source_basis/module_pw/test_serial/CMakeLists.txt +++ b/source/source_basis/module_pw/test_serial/CMakeLists.txt @@ -12,6 +12,7 @@ add_library( ../../../source_base/module_fft/fft_bundle.cpp ../../../source_base/module_fft/fft_cpu.cpp ../pw_basis.cpp + ../pw_grid_geometry.cpp ../pw_basis_k.cpp ../pw_basis_sup.cpp ../pw_distributeg.cpp @@ -23,6 +24,12 @@ add_library( ../pw_transform_k.cpp ) +AddTest( + TARGET MODULE_PW_grid_geometry + LIBS parameter planewave_serial device base + SOURCES test_pw_grid_geometry.cpp +) + AddTest( TARGET MODULE_PW_basis_pw_serial LIBS parameter planewave_serial device base diff --git a/source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp b/source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp new file mode 100644 index 00000000000..89077bcc1ee --- /dev/null +++ b/source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp @@ -0,0 +1,50 @@ +#include "source_basis/module_pw/pw_grid_geometry.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_base/matrix3.h" + +#include + +TEST(PwGridGeometry, UsesGlobalSlabOffsetAndRotatedLattice) +{ + ModulePW::PW_Basis basis("cpu", "double"); + basis.nx = 2; + basis.ny = 2; + basis.nz = 4; + basis.nplane = 2; + basis.startz_current = 2; + basis.nrxx = 8; + const ModuleBase::Matrix3 lattice(0.0, 1.0, 0.0, + -2.0, 0.0, 0.0, + 0.0, 0.0, 3.0); + std::vector> positions; + std::string error; + ASSERT_TRUE(ModulePW::grid_positions(basis, lattice, 10.0, positions, error)); + ASSERT_EQ(positions.size(), 8u); + EXPECT_DOUBLE_EQ(positions[0].x, 0.0); + EXPECT_DOUBLE_EQ(positions[0].y, 0.0); + EXPECT_DOUBLE_EQ(positions[0].z, 15.0); + EXPECT_DOUBLE_EQ(positions[7].x, -10.0); + EXPECT_DOUBLE_EQ(positions[7].y, 5.0); + EXPECT_DOUBLE_EQ(positions[7].z, 22.5); +} + +TEST(PwGridGeometry, PermitsEmptySlabsAndRejectsInvalidOffsets) +{ + ModulePW::PW_Basis basis("cpu", "double"); + basis.nx = 2; + basis.ny = 2; + basis.nz = 4; + basis.nplane = 0; + basis.startz_current = 4; + basis.nrxx = 0; + const ModuleBase::Matrix3 lattice; + std::vector> positions; + std::string error; + ASSERT_TRUE(ModulePW::grid_positions(basis, lattice, 10.0, positions, error)); + EXPECT_TRUE(positions.empty()); + basis.nplane = 1; + basis.nrxx = 4; + EXPECT_FALSE(ModulePW::grid_positions(basis, lattice, 10.0, positions, error)); + EXPECT_TRUE(positions.empty()); + EXPECT_FALSE(error.empty()); +} diff --git a/source/source_estate/CMakeLists.txt b/source/source_estate/CMakeLists.txt index 8898bde9456..cefee5e400b 100644 --- a/source/source_estate/CMakeLists.txt +++ b/source/source_estate/CMakeLists.txt @@ -19,6 +19,7 @@ list(APPEND objects module_pot/h_tddft_pw.cpp module_pot/pot_xc.cpp module_pot/pot_local.cpp + module_pot/pot_pcc.cpp module_pot/potential_new.cpp module_pot/potential_types.cpp module_pot/pot_ml_exx.cpp diff --git a/source/source_estate/elecstate_energy.cpp b/source/source_estate/elecstate_energy.cpp index b417b910091..0c8e9687761 100644 --- a/source/source_estate/elecstate_energy.cpp +++ b/source/source_estate/elecstate_energy.cpp @@ -374,6 +374,10 @@ void ElecState::cal_energies(const int type, } this->f_en.correction_el = makov_payne_correction(*ucell, *this->charge, v_elecstat_ptr).total; } + else if (assume_isolated == "pcc_0d") + { + this->f_en.correction_el = this->pot->pcc_energy_rydberg(); + } else { this->f_en.correction_el = 0.0; diff --git a/source/source_estate/elecstate_print.cpp b/source/source_estate/elecstate_print.cpp index 05566d819ab..f87263e296a 100644 --- a/source/source_estate/elecstate_print.cpp +++ b/source/source_estate/elecstate_print.cpp @@ -287,6 +287,12 @@ void print_etot(const Magnetism& magnet, energies_Ry.push_back(elec.f_en.esol_cav); } + if (inp.assume_isolated == "pcc_0d") + { + titles.push_back("E_pcc"); + energies_Ry.push_back(elec.f_en.correction_el); + } + //! electric field energy if (inp.efield_flag) { diff --git a/source/source_estate/module_pot/pot_pcc.cpp b/source/source_estate/module_pot/pot_pcc.cpp new file mode 100644 index 00000000000..a4f844e4dd9 --- /dev/null +++ b/source/source_estate/module_pot/pot_pcc.cpp @@ -0,0 +1,232 @@ +#include "pot_pcc.h" + +#include "source_base/parallel_reduce.h" +#include "source_base/timer.h" +#include "source_base/tool_quit.h" +#include "source_basis/module_pw/pw_grid_geometry.h" +#include "source_cell/cell_tools.h" + +#include + +namespace +{ +void require_valid_on_pool(const bool valid, const std::string& error) +{ + double invalid = valid ? 0.0 : 1.0; +#ifdef __MPI + Parallel_Reduce::reduce_pool(invalid); +#endif + if (invalid != 0) + { + std::string message = error; + if (message.empty()) + { + message = "invalid PCC input or result on another process in this pool"; + } + ModuleBase::WARNING_QUIT("PotPcc::cal_v_eff", message); + } +} + +bool fractional_translation(const ModuleBase::Matrix3& rotation, + const ModuleBase::Vector3& translation) +{ + const double tolerance = 1.0e-6; + const double identity_error = std::abs(rotation.e11 - 1.0) + std::abs(rotation.e22 - 1.0) + + std::abs(rotation.e33 - 1.0) + std::abs(rotation.e12) + + std::abs(rotation.e13) + std::abs(rotation.e21) + + std::abs(rotation.e23) + std::abs(rotation.e31) + + std::abs(rotation.e32); + if (identity_error > tolerance) + { + return false; + } + const double dx = translation.x - std::round(translation.x); + const double dy = translation.y - std::round(translation.y); + const double dz = translation.z - std::round(translation.z); + return std::abs(dx) > tolerance || std::abs(dy) > tolerance || std::abs(dz) > tolerance; +} + +bool primitive_symmetry(const ModuleSymmetry::Symmetry& symmetry) +{ + for (int operation = 0; operation < symmetry.nrotk; ++operation) + { + const bool forbidden = fractional_translation(symmetry.gmatrix[operation], symmetry.gtrans[operation]); + if (forbidden) + { + return false; + } + } + for (int operation = 0; operation < symmetry.nrotk_anti; ++operation) + { + const bool forbidden = fractional_translation(symmetry.gmatrix_anti[operation], symmetry.gtrans_anti[operation]); + if (forbidden) + { + return false; + } + } + const ModuleBase::Matrix3 identity; + for (const auto& translation : symmetry.ptrans) + { + const bool forbidden = fractional_translation(identity, translation); + if (forbidden) + { + return false; + } + } + return true; +} +} // namespace + +namespace elecstate +{ + +PotPcc::PotPcc(const ModulePW::PW_Basis* basis) +{ + this->rho_basis_ = basis; + this->dynamic_mode = true; +} + +void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) +{ + ModuleBase::timer::start("PotPcc", "cal_v_eff"); + result_valid_ = false; + const bool storage_valid = charge != nullptr && cell != nullptr && this->rho_basis_ != nullptr; + require_valid_on_pool(storage_valid, "PCC requires charge, cell and PW basis storage"); + const ModulePW::PW_Basis& basis = *this->rho_basis_; + const bool grid_valid = basis.nx > 0 && basis.ny > 0 && basis.nz > 0 && basis.nxyz > 0 + && basis.nplane >= 0 && basis.nrxx == basis.nx * basis.ny * basis.nplane + && (charge->nspin == 1 || charge->nspin == 2) + && potential.nr == charge->nspin && potential.nc == basis.nrxx + && std::isfinite(cell->omega) && cell->omega > 0.0; + require_valid_on_pool(grid_valid, "PCC requires an initialized grid and nspin=1/2 potential"); + std::string error; + const bool geometry_valid = unitcell::make_orthogonal_cell(cell->latvec, cell->lat0, 1.0e-10, geometry_, error); + require_valid_on_pool(geometry_valid, error); + const bool parameters_valid = make_pcc_0d_parameters(geometry_, 1.0e-10, parameters_, error); + require_valid_on_pool(parameters_valid, error); + const bool symmetry_valid = primitive_symmetry(cell->symm); + require_valid_on_pool(symmetry_valid, "PCC 0D requires a primitive cell without fractional-translation symmetry"); + + const std::vector atoms = unitcell::get_atom_data(cell->atoms, cell->ntype, cell->lat0); + const int atom_count = static_cast(atoms.size()); + std::vector masses(atom_count); + ionic_positions_.resize(atom_count); + ionic_charges_.resize(atom_count); + for (int atom = 0; atom < atom_count; ++atom) + { + ionic_positions_[atom] = atoms[atom].position; + ionic_charges_[atom] = atoms[atom].valence_charge; + masses[atom] = atoms[atom].mass; + } + ModuleBase::Vector3 center; + const bool center_valid = unitcell::weighted_center(ionic_positions_, masses, geometry_, center, error); + require_valid_on_pool(center_valid, error); + geometry_.origin = center; + std::vector> relative_ions(atom_count); + for (int atom = 0; atom < atom_count; ++atom) + { + relative_ions[atom] = unitcell::relative_position(ionic_positions_[atom], geometry_); + } + ChargeMoments ionic_moments; + const double* ionic_charge_data = ionic_charges_.data(); + const ModuleBase::Vector3* ionic_position_data = relative_ions.data(); + const bool ions_valid = charge_moments(ionic_charge_data, ionic_position_data, atom_count, 1.0, ionic_moments, error); + require_valid_on_pool(ions_valid, error); + + std::vector electronic_charge(basis.nrxx, 0.0); + std::vector> positions; + const bool positions_valid = ModulePW::grid_positions(basis, cell->latvec, cell->lat0, positions, error); + require_valid_on_pool(positions_valid, error); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + for (int spin = 0; spin < charge->nspin; ++spin) + { + electronic_charge[ir] -= charge->rho[spin][ir]; + } + positions[ir] = unitcell::relative_position(positions[ir], geometry_); + } + ChargeMoments electronic_moments; + const double volume_element = cell->omega / basis.nxyz; + const double* electronic_data = electronic_charge.data(); + const ModuleBase::Vector3* position_data = positions.data(); + const bool density_valid = charge_moments(electronic_data, position_data, basis.nrxx, volume_element, electronic_moments, error); + require_valid_on_pool(density_valid, error); + double reduced[5] = {electronic_moments.charge, electronic_moments.dipole.x, + electronic_moments.dipole.y, electronic_moments.dipole.z, + electronic_moments.second_moment}; +#ifdef __MPI + Parallel_Reduce::reduce_pool(reduced, 5); +#endif + electronic_moments.charge = reduced[0]; + electronic_moments.dipole = ModuleBase::Vector3(reduced[1], reduced[2], reduced[3]); + electronic_moments.second_moment = reduced[4]; + // Ions are replicated on every rank and must be added after the reduction. + moments_ = add_charge_moments(ionic_moments, electronic_moments); + energy_rydberg_ = 2.0 * pcc_0d_energy(moments_, parameters_); + electron_potential_.resize(basis.nrxx); + bool result_finite = std::isfinite(energy_rydberg_); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + const double positive_potential = pcc_0d_potential(moments_, positions[ir], parameters_); + electron_potential_[ir] = -2.0 * positive_potential; + result_finite = result_finite && std::isfinite(electron_potential_[ir]); + } + require_valid_on_pool(result_finite, "PCC energy or potential is nonfinite"); + for (int spin = 0; spin < charge->nspin; ++spin) + { + for (int ir = 0; ir < basis.nrxx; ++ir) + { + potential(spin, ir) += electron_potential_[ir]; + } + } + result_valid_ = true; + ModuleBase::timer::end("PotPcc", "cal_v_eff"); +} + +double PotPcc::get_energy() const +{ + if (!result_valid_) + { + ModuleBase::WARNING_QUIT("PotPcc::get_energy", "PCC energy requires an updated potential"); + } + return energy_rydberg_; +} + +const std::vector& PotPcc::electron_potential() const +{ + if (!result_valid_) + { + ModuleBase::WARNING_QUIT("PotPcc::electron_potential", "PCC output requires an updated potential"); + } + return electron_potential_; +} + +void PotPcc::add_force(const UnitCell& cell, ModuleBase::matrix& force) const +{ + ModuleBase::timer::start("PotPcc", "add_force"); + if (!result_valid_ || force.nr != cell.nat || force.nc != 3) + { + ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC force requires a current potential and nat-by-3 output"); + } + const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); + if (atoms.size() != ionic_positions_.size()) + { + ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC atom count changed since the potential update"); + } + for (int atom = 0; atom < cell.nat; ++atom) + { + const ModuleBase::Vector3 difference = atoms[atom].position - ionic_positions_[atom]; + if (difference.norm2() > 1.0e-24 || atoms[atom].valence_charge != ionic_charges_[atom]) + { + ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC atoms changed since the potential update"); + } + const ModuleBase::Vector3 relative = unitcell::relative_position(ionic_positions_[atom], geometry_); + const ModuleBase::Vector3 correction = pcc_0d_force(moments_, ionic_charges_[atom], relative, parameters_); + force(atom, 0) += 2.0 * correction.x; + force(atom, 1) += 2.0 * correction.y; + force(atom, 2) += 2.0 * correction.z; + } + ModuleBase::timer::end("PotPcc", "add_force"); +} + +} // namespace elecstate diff --git a/source/source_estate/module_pot/pot_pcc.h b/source/source_estate/module_pot/pot_pcc.h new file mode 100644 index 00000000000..fb9fdc8ab28 --- /dev/null +++ b/source/source_estate/module_pot/pot_pcc.h @@ -0,0 +1,35 @@ +#ifndef POT_PCC_H +#define POT_PCC_H + +#include "pot_base.h" +#include "source_cell/cell_geometry.h" +#include "source_estate/pcc_0d.h" + +namespace elecstate +{ + +/// Self-consistent vacuum PCC. Owns the result of its last density update. +class PotPcc : public PotBase +{ + public: + explicit PotPcc(const ModulePW::PW_Basis* basis); + + void cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) override; + double get_energy() const override; + const std::vector& electron_potential() const; + void add_force(const UnitCell& cell, ModuleBase::matrix& force) const; + + private: + unitcell::OrthogonalCell geometry_; + Pcc0dParameters parameters_; + ChargeMoments moments_; + std::vector> ionic_positions_; + std::vector ionic_charges_; + std::vector electron_potential_; + double energy_rydberg_ = 0.0; + bool result_valid_ = false; +}; + +} // namespace elecstate + +#endif diff --git a/source/source_estate/module_pot/potential_new.cpp b/source/source_estate/module_pot/potential_new.cpp index 150dfe893a1..201af74e730 100644 --- a/source/source_estate/module_pot/potential_new.cpp +++ b/source/source_estate/module_pot/potential_new.cpp @@ -1,4 +1,5 @@ #include "potential_new.h" +#include "pot_pcc.h" #include "pot_ml_exx.h" #include "source_base/global_function.h" @@ -14,6 +15,48 @@ namespace elecstate { +const PotPcc* Potential::pcc_component() const +{ + for (const PotBase* component : this->components) + { + const PotPcc* pcc = dynamic_cast(component); + if (pcc != nullptr) + { + return pcc; + } + } + return nullptr; +} + +double Potential::pcc_energy_rydberg() const +{ + const PotPcc* pcc = this->pcc_component(); + if (pcc == nullptr) + { + return 0.0; + } + return pcc->get_energy(); +} + +const std::vector* Potential::pcc_electron_potential() const +{ + const PotPcc* pcc = this->pcc_component(); + if (pcc == nullptr) + { + return nullptr; + } + return &pcc->electron_potential(); +} + +void Potential::add_pcc_force(const UnitCell& cell, ModuleBase::matrix& force) const +{ + const PotPcc* pcc = this->pcc_component(); + if (pcc != nullptr) + { + pcc->add_force(cell, force); + } +} + Potential::Potential(const ModulePW::PW_Basis* rho_basis_in, const ModulePW::PW_Basis* rho_basis_smooth_in, diff --git a/source/source_estate/module_pot/potential_new.h b/source/source_estate/module_pot/potential_new.h index 77ab063c99f..77cc8e210b5 100644 --- a/source/source_estate/module_pot/potential_new.h +++ b/source/source_estate/module_pot/potential_new.h @@ -12,6 +12,7 @@ namespace elecstate { +class PotPcc; class TDFieldManager; /** @@ -79,6 +80,11 @@ class Potential : public PotBase PotBase* get_pot_type(const std::string& pot_type); + /// PCC results belong to this electronic state's registered potential. + double pcc_energy_rydberg() const; + const std::vector* pcc_electron_potential() const; + void add_pcc_force(const UnitCell& cell, ModuleBase::matrix& force) const; + /** * @brief Inject the shared RT-TDDFT field state before potential setup. * @@ -243,6 +249,7 @@ class Potential : public PotBase double vl_of_0 = 0.0; std::vector components; + const PotPcc* pcc_component() const; const UnitCell* ucell_ = nullptr; const ModuleBase::matrix* vloc_ = nullptr; diff --git a/source/source_estate/module_pot/potential_types.cpp b/source/source_estate/module_pot/potential_types.cpp index 4aa14ba6283..ecbcc7243f1 100644 --- a/source/source_estate/module_pot/potential_types.cpp +++ b/source/source_estate/module_pot/potential_types.cpp @@ -2,6 +2,7 @@ #include "efield.h" #include "gatefield.h" #include "pot_local.h" +#include "pot_pcc.h" #include "pot_sep.h" #include "pot_surchem.hpp" #include "pot_xc.h" @@ -35,6 +36,10 @@ PotBase* Potential::get_pot_type(const std::string& pot_type) { return new PotXC(PARAM.globalv.domag, PARAM.globalv.domag_z, PARAM.inp.gga_grad, this->rho_basis_, this->etxc_, this->vtxc_, &(this->vofk_eff)); } + else if (pot_type == "pcc") + { + return new PotPcc(this->rho_basis_); + } else if (pot_type == "surchem") { return new PotSurChem(this->rho_basis_, this->structure_factors_, this->v_eff_fixed.data(), this->solvent_); diff --git a/source/source_estate/test/CMakeLists.txt b/source/source_estate/test/CMakeLists.txt index 5360087e94f..e9fbf59952b 100644 --- a/source/source_estate/test/CMakeLists.txt +++ b/source/source_estate/test/CMakeLists.txt @@ -21,6 +21,18 @@ AddTest( if (ENABLE_MPI) +set(pcc_test_sources + ../module_pot/pot_pcc.cpp ../pcc_0d.cpp ../charge_moments.cpp + ../../source_cell/cell_geometry.cpp ../../source_cell/cell_tools.cpp + ../../source_cell/atom_spec.cpp ../../source_cell/atom_pseudo.cpp ../../source_cell/pseudo.cpp +) + +AddTest( + TARGET MODULE_ESTATE_pot_pcc + LIBS parameter base device planewave_serial + SOURCES test_pot_pcc.cpp ${pcc_test_sources} +) + # Copy at configure time so a plain `make` + `ctest` run finds the data. # install() only runs during `cmake --install`, which local test runs skip. file(COPY support DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) @@ -116,6 +128,7 @@ AddTest( LIBS parameter base device planewave_serial SOURCES potential_new_test.cpp td_field_test.cpp ../module_pot/potential_new.cpp ../module_pot/td_field.cpp ../module_pot/td_field_profiles.cpp ../module_pot/td_field_manager.cpp + ${pcc_test_sources} ) endif() diff --git a/source/source_estate/test/elecstate_energy_test.cpp b/source/source_estate/test/elecstate_energy_test.cpp index 136b1d08f78..2c112c8a862 100644 --- a/source/source_estate/test/elecstate_energy_test.cpp +++ b/source/source_estate/test/elecstate_energy_test.cpp @@ -12,6 +12,10 @@ int XC_Functional::func_type = 1; bool XC_Functional::ked_flag = false; namespace elecstate { +double Potential::pcc_energy_rydberg() const +{ + return 0.25; +} void Potential::get_vnew(Charge const*, ModuleBase::matrix&) { return; @@ -146,6 +150,19 @@ TEST_F(ElecStateEnergyTest, CalEnergiesEtotDFTU) EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 1.3); } +TEST_F(ElecStateEnergyTest, AddsPccEnergyOnceAndKeepsItSeparateFromSolvation) +{ + elecstate->f_en.deband = 0.1; + assume_isolated = "pcc_0d"; + elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); + EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.25); + EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.95); + assume_isolated = "none"; + elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); + EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.0); + EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.7); +} + TEST_F(ElecStateEnergyTest, CalConverged) { elecstate->cal_converged(); diff --git a/source/source_estate/test/test_pot_pcc.cpp b/source/source_estate/test/test_pot_pcc.cpp new file mode 100644 index 00000000000..1c44ca8ab89 --- /dev/null +++ b/source/source_estate/test/test_pot_pcc.cpp @@ -0,0 +1,152 @@ +#include "source_estate/module_pot/pot_pcc.h" +#include "source_base/constants.h" + +#include + +// These fixtures use public cell/charge storage without running INPUT setup. +UnitCell::UnitCell() {} +UnitCell::~UnitCell() {} +Magnetism::Magnetism() {} +Magnetism::~Magnetism() {} +SepPot::SepPot() {} +SepPot::~SepPot() {} +Sep_Cell::Sep_Cell() noexcept {} +Sep_Cell::~Sep_Cell() noexcept {} +Charge::Charge() {} +Charge::~Charge() {} + +namespace +{ +class PotPccTest : public testing::Test +{ + protected: + void SetUp() override + { + basis.nx = 2; + basis.ny = 2; + basis.nz = 2; + basis.nplane = 2; + basis.startz_current = 0; + basis.nrxx = 8; + basis.nxyz = 8; + cell.lat0 = 10.0; + cell.omega = 1000.0; + cell.latvec = ModuleBase::Matrix3(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + cell.ntype = 1; + cell.nat = 1; + atom.na = 1; + atom.mass = 1.0; + atom.ncpp.zv = 1.0; + atom.tau = {ModuleBase::Vector3(0.45, 0.45, 0.45)}; + cell.atoms = &atom; + density.assign(8, 0.0); + density[7] = 1.0 / 125.0; + channels[0] = density.data(); + charge.rho = channels; + charge.nspin = 1; + } + + ModulePW::PW_Basis basis{"cpu", "double"}; + UnitCell cell; + Atom atom; + Charge charge; + std::vector density; + double* channels[2] = {nullptr, nullptr}; +}; +} // namespace + +TEST_F(PotPccTest, NeutralDipoleProducesConsistentEnergyPotentialAndForce) +{ + elecstate::PotPcc correction(&basis); + ModuleBase::matrix potential(1, 8); + correction.cal_v_eff(&charge, &cell, potential); + const double expected_energy = 4.0 * ModuleBase::PI * 0.75 / 3000.0; + EXPECT_NEAR(correction.get_energy(), expected_energy, 1.0e-14); + EXPECT_NEAR(potential(0, 7), expected_energy, 1.0e-14); + const std::vector& stored = correction.electron_potential(); + ASSERT_EQ(stored.size(), 8u); + EXPECT_DOUBLE_EQ(stored[7], potential(0, 7)); + ModuleBase::matrix force(1, 3); + correction.add_force(cell, force); + const double expected_force = 8.0 * ModuleBase::PI * 0.5 / 3000.0; + EXPECT_NEAR(force(0, 0), expected_force, 1.0e-14); + EXPECT_NEAR(force(0, 1), expected_force, 1.0e-14); + EXPECT_NEAR(force(0, 2), expected_force, 1.0e-14); +} + +TEST_F(PotPccTest, TwoSpinChannelsShareCorrectionAndAddToExistingPotential) +{ + std::vector down(8, 0.0); + density[7] = 0.3 / 125.0; + down[7] = 0.7 / 125.0; + channels[1] = down.data(); + charge.nspin = 2; + elecstate::PotPcc correction(&basis); + ModuleBase::matrix potential(2, 8); + potential(0, 7) = 2.0; + potential(1, 7) = 3.0; + correction.cal_v_eff(&charge, &cell, potential); + const double energy = correction.get_energy(); + const double first_expected = 2.0 + energy; + const double second_expected = 3.0 + energy; + EXPECT_NEAR(potential(0, 7), first_expected, 1.0e-14); + EXPECT_NEAR(potential(1, 7), second_expected, 1.0e-14); +} + +TEST_F(PotPccTest, RecomputesDensityAndKeepsInstancesIndependent) +{ + elecstate::PotPcc first(&basis); + elecstate::PotPcc second(&basis); + ModuleBase::matrix potential(1, 8); + first.cal_v_eff(&charge, &cell, potential); + const double neutral_energy = first.get_energy(); + density[7] = 2.0 / 125.0; + potential.zero_out(); + second.cal_v_eff(&charge, &cell, potential); + const double charged_energy = second.get_energy(); + const double expected_charged = 2.837297479480619 / 10.0 + 4.0 * ModuleBase::PI * 1.5 / 3000.0; + EXPECT_NEAR(charged_energy, expected_charged, 1.0e-14); + EXPECT_DOUBLE_EQ(first.get_energy(), neutral_energy); + potential.zero_out(); + first.cal_v_eff(&charge, &cell, potential); + EXPECT_NEAR(first.get_energy(), charged_energy, 1.0e-14); +} + +TEST_F(PotPccTest, ForceMatchesFixedDensityEnergyDerivativeAfterMovingIons) +{ + elecstate::PotPcc correction(&basis); + ModuleBase::matrix potential(1, 8); + correction.cal_v_eff(&charge, &cell, potential); + ModuleBase::matrix force(1, 3); + correction.add_force(cell, force); + const double step_bohr = 1.0e-4; + const double step_lat0 = step_bohr / cell.lat0; + atom.tau[0].x += step_lat0; + potential.zero_out(); + correction.cal_v_eff(&charge, &cell, potential); + const double plus_energy = correction.get_energy(); + atom.tau[0].x -= 2.0 * step_lat0; + potential.zero_out(); + correction.cal_v_eff(&charge, &cell, potential); + const double minus_energy = correction.get_energy(); + const double numerical_force = -(plus_energy - minus_energy) / (2.0 * step_bohr); + EXPECT_NEAR(force(0, 0), numerical_force, 1.0e-10); +} + +TEST_F(PotPccTest, RejectsEnergyBeforeDensityUpdate) +{ + elecstate::PotPcc correction(&basis); + EXPECT_EXIT(correction.get_energy(), testing::ExitedWithCode(1), ""); +} + +TEST_F(PotPccTest, RejectsForceFromAnOlderGeometry) +{ + elecstate::PotPcc correction(&basis); + ModuleBase::matrix potential(1, 8); + correction.cal_v_eff(&charge, &cell, potential); + atom.tau[0].x += 0.01; + ModuleBase::matrix force(1, 3); + EXPECT_EXIT(correction.add_force(cell, force), testing::ExitedWithCode(1), ""); +} diff --git a/source/source_estate/write_elecstat_pot.cpp b/source/source_estate/write_elecstat_pot.cpp index 15a5363ed62..f3076de2dbf 100644 --- a/source/source_estate/write_elecstat_pot.cpp +++ b/source/source_estate/write_elecstat_pot.cpp @@ -22,12 +22,17 @@ void write_elecstat_pot( const UnitCell* ucell, const double* v_eff, const surchem& solvent, + const std::vector* pcc_potential, const int precision) { ModuleBase::TITLE("ModuleIO", "write_elecstat_pot"); ModuleBase::timer::start("ModuleIO", "write_elecstat_pot"); std::vector v_elecstat(rho_basis->nrxx, 0.0); + if (pcc_potential != nullptr && pcc_potential->size() != static_cast(rho_basis->nrxx)) + { + ModuleBase::WARNING_QUIT("write_elecstat_pot", "PCC potential does not match the output grid"); + } const int nspin = PARAM.inp.nspin; const int efield = PARAM.inp.efield_flag; @@ -66,6 +71,10 @@ void write_elecstat_pot( { v_elecstat[ir] += v_efield(0, ir); } + if (pcc_potential != nullptr) + { + v_elecstat[ir] += (*pcc_potential)[ir]; + } if(imp_sol == true) { v_elecstat[ir] += solvent.delta_phi[ir]; diff --git a/source/source_estate/write_elecstat_pot.h b/source/source_estate/write_elecstat_pot.h index bee575b95c3..2ad8452ed43 100644 --- a/source/source_estate/write_elecstat_pot.h +++ b/source/source_estate/write_elecstat_pot.h @@ -33,6 +33,7 @@ void write_elecstat_pot( const UnitCell* ucell_, const double* v_eff_fixed, const surchem& solvent, + const std::vector* pcc_potential, const int precision); } // namespace ModuleIO diff --git a/source/source_io/module_ctrl/ctrl_output_fp.cpp b/source/source_io/module_ctrl/ctrl_output_fp.cpp index 249f471694c..09a26c15a74 100644 --- a/source/source_io/module_ctrl/ctrl_output_fp.cpp +++ b/source/source_io/module_ctrl/ctrl_output_fp.cpp @@ -149,6 +149,7 @@ void ctrl_output_fp(UnitCell& ucell, std::string fn = PARAM.globalv.global_out_dir + "potes"; fn += geom_block + ".cube"; + const std::vector* pcc_potential = pelec->pot->pcc_electron_potential(); ModuleIO::write_elecstat_pot( #ifdef __MPI pw_big->bz, @@ -161,6 +162,7 @@ void ctrl_output_fp(UnitCell& ucell, &(ucell), pelec->pot->get_fixed_v(), solvent, + pcc_potential, inp.out_pot[1]); } diff --git a/source/source_io/module_parameter/read_inp_sys.cpp b/source/source_io/module_parameter/read_inp_sys.cpp index 97a40123eb8..c37383a4177 100644 --- a/source/source_io/module_parameter/read_inp_sys.cpp +++ b/source/source_io/module_parameter/read_inp_sys.cpp @@ -507,8 +507,9 @@ Available options are: Available options are: * none: regular periodic calculation without isolated-system correction. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). +* pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. -Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995).)"; +Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014).)"; item.default_value = "none"; read_sync_string(input.assume_isolated); item.reset_value = [](const Input_Item& item, Parameter& para) { @@ -518,7 +519,7 @@ Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995).)"; } }; item.check_value = [](const Input_Item& item, const Parameter& para) { - const std::vector allowed = {"none", "makov-payne", "m-p", "mp"}; + const std::vector allowed = {"none", "makov-payne", "m-p", "mp", "pcc_0d"}; if (std::find(allowed.begin(), allowed.end(), para.input.assume_isolated) == allowed.end()) { ModuleBase::WARNING_QUIT("ReadInput", nofound_str(allowed, "assume_isolated")); @@ -530,6 +531,17 @@ Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995).)"; ModuleBase::WARNING_QUIT("ReadInput", "Makov-Payne correction is available only for cubic lattices: latname = sc, fcc, or bcc."); } + const Input_para& inp = para.input; + if (inp.assume_isolated == "pcc_0d" + && (inp.imp_sol || inp.nspin == 4 || inp.device == "gpu" || inp.esolver_type != "ksdft" + || (inp.basis_type != "pw" && inp.basis_type != "lcao") + || (inp.calculation != "scf" && inp.calculation != "relax") + || inp.efield_flag || inp.gate_flag || inp.cal_stress || inp.dfthalf_type != 0 + || inp.deepks_scf || inp.deepks_out_labels || inp.deepks_bandgap || inp.deepks_v_delta + || inp.dm_to_rho)) + { + ModuleBase::WARNING_QUIT("ReadInput", "PCC 0D requires CPU KS-DFT PW/LCAO scf or fixed-cell relax, nspin=1/2, without solvent, stress or other fields"); + } }; this->add_item(item); } diff --git a/source/source_io/test_serial/read_input_item_test.cpp b/source/source_io/test_serial/read_input_item_test.cpp index ffe37e6ba80..49b7ca4bbba 100644 --- a/source/source_io/test_serial/read_input_item_test.cpp +++ b/source/source_io/test_serial/read_input_item_test.cpp @@ -98,6 +98,45 @@ TEST_F(InputTest, RelaxMethod) EXPECT_EQ(find_label("relax_new", readinput.input_lists), readinput.input_lists.end()); } +TEST_F(InputTest, Pcc0dSelectionAndUnsupportedCombinations) +{ + ModuleIO::ReadInput readinput(0); + Parameter param; + Input_para& input = TestParameters::input(param); + input.device = "cpu"; + input.esolver_type = "ksdft"; + input.basis_type = "pw"; + input.calculation = "scf"; + auto it = find_label("assume_isolated", readinput.input_lists); + ASSERT_NE(it, readinput.input_lists.end()); + it->second.str_values = {"pcc_0d"}; + it->second.read_value(it->second, param); + EXPECT_EQ(input.assume_isolated, "pcc_0d"); + it->second.check_value(it->second, param); + input.basis_type = "lcao"; + input.calculation = "relax"; + it->second.check_value(it->second, param); + const Input_para valid = input; + + input.imp_sol = true; + EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); + input = valid; + input.nspin = 4; + EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); + input = valid; + input.device = "gpu"; + EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); + input = valid; + input.cal_stress = true; + EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); + input = valid; + input.calculation = "cell-relax"; + EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); + input = valid; + input.assume_isolated = "pcc_2d"; + EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); +} + TEST_F(InputTest, Item_test) { ModuleIO::ReadInput readinput(0); diff --git a/source/source_lcao/force_stress_assemble.cpp b/source/source_lcao/force_stress_assemble.cpp index 6590de0c2a2..331b75d39f3 100644 --- a/source/source_lcao/force_stress_assemble.cpp +++ b/source/source_lcao/force_stress_assemble.cpp @@ -38,6 +38,10 @@ void sum_force_terms(const int iat, + parts.fcc(iat, i) // nonlinear core correction force (pw) + parts.fscc(iat, i) // self consistent corretion force (pw) + parts.fpothybrid(iat, i); // pulay force for hybrid gauge rt-tddft + if (parts.fpcc.nr != 0) + { + fcs(iat, i) += parts.fpcc(iat, i); + } // Force contribution from DFT+U, Quxin add on 20201029 if (PARAM.inp.dft_plus_u) diff --git a/source/source_lcao/force_stress_lcao.cpp b/source/source_lcao/force_stress_lcao.cpp index aab0dc61166..a65ba5b204e 100644 --- a/source/source_lcao/force_stress_lcao.cpp +++ b/source/source_lcao/force_stress_lcao.cpp @@ -186,6 +186,11 @@ void Force_Stress_LCAO::getForceStress(UnitCell& ucell, // vdW force/stress and external-field forces LCAO_domain::cal_vdw_fields_fs(vdw_result, ucell, solvent, rhopw, locpp, isforce, isstress, parts, sparts); + if (isforce) + { + parts.fpcc.create(ucell.nat, 3); + pelec->pot->add_pcc_force(ucell, parts.fpcc); + } // DFT+U force/stress LCAO_domain::cal_dftu_fs(ucell, gd, pv, orb, kv, dmat, two_center_bundle, dftu, isforce, isstress, parts, sparts); diff --git a/source/source_lcao/force_stress_lcao.h b/source/source_lcao/force_stress_lcao.h index 3eeb5c1a605..d1a9b29d776 100644 --- a/source/source_lcao/force_stress_lcao.h +++ b/source/source_lcao/force_stress_lcao.h @@ -69,6 +69,7 @@ struct LCAOForceParts ModuleBase::matrix fefield_tddft; ModuleBase::matrix fgate; ModuleBase::matrix fsol; + ModuleBase::matrix fpcc; }; struct LCAOStressParts diff --git a/source/source_lcao/hamilt_lcao.cpp b/source/source_lcao/hamilt_lcao.cpp index 3ea53637d95..a12fa0ce528 100644 --- a/source/source_lcao/hamilt_lcao.cpp +++ b/source/source_lcao/hamilt_lcao.cpp @@ -87,6 +87,7 @@ HamiltLCAO::HamiltLCAO(const UnitCell& ucell, if (inp.vh_in_h) { pot_register_in.push_back("hartree"); } pot_register_in.push_back("xc"); if (inp.imp_sol) { pot_register_in.push_back("surchem"); } + if (inp.assume_isolated == "pcc_0d") { pot_register_in.push_back("pcc"); } if (inp.efield_flag) { pot_register_in.push_back("efield"); } if (inp.gate_flag) { pot_register_in.push_back("gatefield"); } if (inp.esolver_type == "tddft") { pot_register_in.push_back("tddft"); } diff --git a/source/source_pw/module_pwdft/force_pw.cpp b/source/source_pw/module_pwdft/force_pw.cpp index d37e2379954..9381316b46f 100644 --- a/source/source_pw/module_pwdft/force_pw.cpp +++ b/source/source_pw/module_pwdft/force_pw.cpp @@ -138,6 +138,8 @@ void Forces::cal_force(const int nspin, } ModuleBase::matrix forcesol; + ModuleBase::matrix forcepcc(this->nat, 3); + elec.pot->add_pcc_force(ucell, forcepcc); if (PARAM.inp.imp_sol) { forcesol.create(this->nat, 3); @@ -158,7 +160,7 @@ void Forces::cal_force(const int nspin, for (int ia = 0; ia < ucell.atoms[it].na; ia++) { force(iat, ipol) = forcelc(iat, ipol) + forceion(iat, ipol) + forcenl(iat, ipol) + forcecc(iat, ipol) - + forcescc(iat, ipol); + + forcescc(iat, ipol) + forcepcc(iat, ipol); if (vdw_result != nullptr) // linpz and jiyy added vdw force, modified by zhengdy { diff --git a/source/source_pw/module_pwdft/hamilt_pw.cpp b/source/source_pw/module_pwdft/hamilt_pw.cpp index 18beccada0c..7b72627768b 100644 --- a/source/source_pw/module_pwdft/hamilt_pw.cpp +++ b/source/source_pw/module_pwdft/hamilt_pw.cpp @@ -81,10 +81,15 @@ HamiltPW::HamiltPW(elecstate::Potential* pot_in, } // no variable can choose xc, maybe it is necessary pot_register_in.push_back("xc"); - if (PARAM.inp.imp_sol) + const Input_para& input = PARAM.inp; + if (input.imp_sol) { pot_register_in.push_back("surchem"); } + if (input.assume_isolated == "pcc_0d") + { + pot_register_in.push_back("pcc"); + } if (PARAM.inp.efield_flag) { pot_register_in.push_back("efield"); From 03a2967e4e2f6a2533262e2f01fb58ed43f8f4e3 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 10:18:36 -0600 Subject: [PATCH 04/41] refactor(estate): separate PCC ion preparation and electron moments Keep potential update orchestration distinct from geometry/atom preparation and distributed density integration. Rebuilt abacus_basic_para, MODULE_ESTATE_pot_pcc and MODULE_ESTATE_potentials_new; both CTest targets passed outside sandbox with OMP_NUM_THREADS=1. Quality score improves from 82 to 90. No INPUT behavior changes. --- source/source_estate/module_pot/pot_pcc.cpp | 84 +++++++++++++++------ source/source_estate/module_pot/pot_pcc.h | 5 ++ 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/source/source_estate/module_pot/pot_pcc.cpp b/source/source_estate/module_pot/pot_pcc.cpp index a4f844e4dd9..3bf45b9b2c5 100644 --- a/source/source_estate/module_pot/pot_pcc.cpp +++ b/source/source_estate/module_pot/pot_pcc.cpp @@ -58,7 +58,8 @@ bool primitive_symmetry(const ModuleSymmetry::Symmetry& symmetry) } for (int operation = 0; operation < symmetry.nrotk_anti; ++operation) { - const bool forbidden = fractional_translation(symmetry.gmatrix_anti[operation], symmetry.gtrans_anti[operation]); + const bool forbidden = fractional_translation(symmetry.gmatrix_anti[operation], + symmetry.gtrans_anti[operation]); if (forbidden) { return false; @@ -86,28 +87,18 @@ PotPcc::PotPcc(const ModulePW::PW_Basis* basis) this->dynamic_mode = true; } -void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) +void PotPcc::prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments) { - ModuleBase::timer::start("PotPcc", "cal_v_eff"); - result_valid_ = false; - const bool storage_valid = charge != nullptr && cell != nullptr && this->rho_basis_ != nullptr; - require_valid_on_pool(storage_valid, "PCC requires charge, cell and PW basis storage"); - const ModulePW::PW_Basis& basis = *this->rho_basis_; - const bool grid_valid = basis.nx > 0 && basis.ny > 0 && basis.nz > 0 && basis.nxyz > 0 - && basis.nplane >= 0 && basis.nrxx == basis.nx * basis.ny * basis.nplane - && (charge->nspin == 1 || charge->nspin == 2) - && potential.nr == charge->nspin && potential.nc == basis.nrxx - && std::isfinite(cell->omega) && cell->omega > 0.0; - require_valid_on_pool(grid_valid, "PCC requires an initialized grid and nspin=1/2 potential"); + ModuleBase::timer::start("PotPcc", "prepare_ions"); std::string error; - const bool geometry_valid = unitcell::make_orthogonal_cell(cell->latvec, cell->lat0, 1.0e-10, geometry_, error); + const bool geometry_valid = unitcell::make_orthogonal_cell(cell.latvec, cell.lat0, 1.0e-10, geometry_, error); require_valid_on_pool(geometry_valid, error); const bool parameters_valid = make_pcc_0d_parameters(geometry_, 1.0e-10, parameters_, error); require_valid_on_pool(parameters_valid, error); - const bool symmetry_valid = primitive_symmetry(cell->symm); + const bool symmetry_valid = primitive_symmetry(cell.symm); require_valid_on_pool(symmetry_valid, "PCC 0D requires a primitive cell without fractional-translation symmetry"); - const std::vector atoms = unitcell::get_atom_data(cell->atoms, cell->ntype, cell->lat0); + const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); const int atom_count = static_cast(atoms.size()); std::vector masses(atom_count); ionic_positions_.resize(atom_count); @@ -127,29 +118,48 @@ void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::m { relative_ions[atom] = unitcell::relative_position(ionic_positions_[atom], geometry_); } - ChargeMoments ionic_moments; const double* ionic_charge_data = ionic_charges_.data(); const ModuleBase::Vector3* ionic_position_data = relative_ions.data(); - const bool ions_valid = charge_moments(ionic_charge_data, ionic_position_data, atom_count, 1.0, ionic_moments, error); + const bool ions_valid = charge_moments(ionic_charge_data, + ionic_position_data, + atom_count, + 1.0, + ionic_moments, + error); require_valid_on_pool(ions_valid, error); + ModuleBase::timer::end("PotPcc", "prepare_ions"); +} + +ChargeMoments PotPcc::collect_electrons( + const Charge& charge, + const UnitCell& cell, + std::vector>& positions) const +{ + ModuleBase::timer::start("PotPcc", "collect_electrons"); + const ModulePW::PW_Basis& basis = *this->rho_basis_; + std::string error; std::vector electronic_charge(basis.nrxx, 0.0); - std::vector> positions; - const bool positions_valid = ModulePW::grid_positions(basis, cell->latvec, cell->lat0, positions, error); + const bool positions_valid = ModulePW::grid_positions(basis, cell.latvec, cell.lat0, positions, error); require_valid_on_pool(positions_valid, error); for (int ir = 0; ir < basis.nrxx; ++ir) { - for (int spin = 0; spin < charge->nspin; ++spin) + for (int spin = 0; spin < charge.nspin; ++spin) { - electronic_charge[ir] -= charge->rho[spin][ir]; + electronic_charge[ir] -= charge.rho[spin][ir]; } positions[ir] = unitcell::relative_position(positions[ir], geometry_); } ChargeMoments electronic_moments; - const double volume_element = cell->omega / basis.nxyz; + const double volume_element = cell.omega / basis.nxyz; const double* electronic_data = electronic_charge.data(); const ModuleBase::Vector3* position_data = positions.data(); - const bool density_valid = charge_moments(electronic_data, position_data, basis.nrxx, volume_element, electronic_moments, error); + const bool density_valid = charge_moments(electronic_data, + position_data, + basis.nrxx, + volume_element, + electronic_moments, + error); require_valid_on_pool(density_valid, error); double reduced[5] = {electronic_moments.charge, electronic_moments.dipole.x, electronic_moments.dipole.y, electronic_moments.dipole.z, @@ -160,6 +170,27 @@ void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::m electronic_moments.charge = reduced[0]; electronic_moments.dipole = ModuleBase::Vector3(reduced[1], reduced[2], reduced[3]); electronic_moments.second_moment = reduced[4]; + ModuleBase::timer::end("PotPcc", "collect_electrons"); + return electronic_moments; +} + +void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) +{ + ModuleBase::timer::start("PotPcc", "cal_v_eff"); + result_valid_ = false; + const bool storage_valid = charge != nullptr && cell != nullptr && this->rho_basis_ != nullptr; + require_valid_on_pool(storage_valid, "PCC requires charge, cell and PW basis storage"); + const ModulePW::PW_Basis& basis = *this->rho_basis_; + const bool grid_valid = basis.nx > 0 && basis.ny > 0 && basis.nz > 0 && basis.nxyz > 0 + && basis.nplane >= 0 && basis.nrxx == basis.nx * basis.ny * basis.nplane + && (charge->nspin == 1 || charge->nspin == 2) + && potential.nr == charge->nspin && potential.nc == basis.nrxx + && std::isfinite(cell->omega) && cell->omega > 0.0; + require_valid_on_pool(grid_valid, "PCC requires an initialized grid and nspin=1/2 potential"); + ChargeMoments ionic_moments; + this->prepare_ions(*cell, ionic_moments); + std::vector> positions; + const ChargeMoments electronic_moments = this->collect_electrons(*charge, *cell, positions); // Ions are replicated on every rank and must be added after the reduction. moments_ = add_charge_moments(ionic_moments, electronic_moments); energy_rydberg_ = 2.0 * pcc_0d_energy(moments_, parameters_); @@ -221,7 +252,10 @@ void PotPcc::add_force(const UnitCell& cell, ModuleBase::matrix& force) const ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC atoms changed since the potential update"); } const ModuleBase::Vector3 relative = unitcell::relative_position(ionic_positions_[atom], geometry_); - const ModuleBase::Vector3 correction = pcc_0d_force(moments_, ionic_charges_[atom], relative, parameters_); + const ModuleBase::Vector3 correction = pcc_0d_force(moments_, + ionic_charges_[atom], + relative, + parameters_); force(atom, 0) += 2.0 * correction.x; force(atom, 1) += 2.0 * correction.y; force(atom, 2) += 2.0 * correction.z; diff --git a/source/source_estate/module_pot/pot_pcc.h b/source/source_estate/module_pot/pot_pcc.h index fb9fdc8ab28..a4a9fe5dd73 100644 --- a/source/source_estate/module_pot/pot_pcc.h +++ b/source/source_estate/module_pot/pot_pcc.h @@ -20,6 +20,11 @@ class PotPcc : public PotBase void add_force(const UnitCell& cell, ModuleBase::matrix& force) const; private: + void prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments); + ChargeMoments collect_electrons(const Charge& charge, + const UnitCell& cell, + std::vector>& positions) const; + unitcell::OrthogonalCell geometry_; Pcc0dParameters parameters_; ChargeMoments moments_; From d8decb6459ab81a38abe7a9b98fca4481dcfeb8a Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 10:29:46 -0600 Subject: [PATCH 05/41] test(pcc): add standalone PW and LCAO integration regressions Register neutral water and charged H3O+ PW cases plus neutral water LCAO. Collect E_pcc in eV alongside existing total-energy and force properties. The new references use two MPI ranks; existing references are unchanged. Verification: - cmake --build build_pcc0d --target abacus_basic_para --parallel 2: passed. - OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 Autotest.sh, -n 2 -o 1: PW 220_PW_PCC0D_WATER / 221_PW_PCC0D_H3O and LCAO scf_pcc0d_water / existing scf_solvation passed default thresholds. - Nine targeted CTest executables passed, including INPUT and PotPcc. - Staged governance and git diff --cached --check: passed. --- tests/01_PW/220_PW_PCC0D_WATER/INPUT | 20 ++++++++++++++ tests/01_PW/220_PW_PCC0D_WATER/KPT | 4 +++ tests/01_PW/220_PW_PCC0D_WATER/README.md | 7 +++++ tests/01_PW/220_PW_PCC0D_WATER/STRU | 20 ++++++++++++++ tests/01_PW/220_PW_PCC0D_WATER/result.ref | 5 ++++ tests/01_PW/221_PW_PCC0D_H3O/INPUT | 21 +++++++++++++++ tests/01_PW/221_PW_PCC0D_H3O/KPT | 4 +++ tests/01_PW/221_PW_PCC0D_H3O/README.md | 7 +++++ tests/01_PW/221_PW_PCC0D_H3O/STRU | 26 +++++++++++++++++++ tests/01_PW/221_PW_PCC0D_H3O/result.ref | 5 ++++ tests/01_PW/CASES_CPU.txt | 2 ++ tests/02_NAO_Gamma/CASES_CPU.txt | 1 + tests/02_NAO_Gamma/scf_pcc0d_water/INPUT | 22 ++++++++++++++++ tests/02_NAO_Gamma/scf_pcc0d_water/KPT | 4 +++ tests/02_NAO_Gamma/scf_pcc0d_water/README.md | 7 +++++ tests/02_NAO_Gamma/scf_pcc0d_water/STRU | 24 +++++++++++++++++ tests/02_NAO_Gamma/scf_pcc0d_water/result.ref | 5 ++++ tests/integrate/tools/catch_properties.sh | 9 +++++++ 18 files changed, 193 insertions(+) create mode 100644 tests/01_PW/220_PW_PCC0D_WATER/INPUT create mode 100644 tests/01_PW/220_PW_PCC0D_WATER/KPT create mode 100644 tests/01_PW/220_PW_PCC0D_WATER/README.md create mode 100644 tests/01_PW/220_PW_PCC0D_WATER/STRU create mode 100644 tests/01_PW/220_PW_PCC0D_WATER/result.ref create mode 100644 tests/01_PW/221_PW_PCC0D_H3O/INPUT create mode 100644 tests/01_PW/221_PW_PCC0D_H3O/KPT create mode 100644 tests/01_PW/221_PW_PCC0D_H3O/README.md create mode 100644 tests/01_PW/221_PW_PCC0D_H3O/STRU create mode 100644 tests/01_PW/221_PW_PCC0D_H3O/result.ref create mode 100644 tests/02_NAO_Gamma/scf_pcc0d_water/INPUT create mode 100644 tests/02_NAO_Gamma/scf_pcc0d_water/KPT create mode 100644 tests/02_NAO_Gamma/scf_pcc0d_water/README.md create mode 100644 tests/02_NAO_Gamma/scf_pcc0d_water/STRU create mode 100644 tests/02_NAO_Gamma/scf_pcc0d_water/result.ref diff --git a/tests/01_PW/220_PW_PCC0D_WATER/INPUT b/tests/01_PW/220_PW_PCC0D_WATER/INPUT new file mode 100644 index 00000000000..189737894ae --- /dev/null +++ b/tests/01_PW/220_PW_PCC0D_WATER/INPUT @@ -0,0 +1,20 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type pw +ks_solver cg +pseudo_dir ../../PP_ORB +ntype 2 +nbands 8 +ecutwfc 40 +ecutrho 160 +scf_thr 1e-10 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 1 +init_wfc atomic +imp_sol 0 +assume_isolated pcc_0d diff --git a/tests/01_PW/220_PW_PCC0D_WATER/KPT b/tests/01_PW/220_PW_PCC0D_WATER/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/01_PW/220_PW_PCC0D_WATER/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/01_PW/220_PW_PCC0D_WATER/README.md b/tests/01_PW/220_PW_PCC0D_WATER/README.md new file mode 100644 index 00000000000..42f90a8fac1 --- /dev/null +++ b/tests/01_PW/220_PW_PCC0D_WATER/README.md @@ -0,0 +1,7 @@ +Vacuum PCC 0D regression with `assume_isolated pcc_0d` and no solvent. + +The integration harness compares total energy, ionic force magnitudes and `E_pcc` +(in eV). References were generated with two MPI ranks, one OpenMP thread and +the repository pseudopotentials/orbitals. Numerical kernels have independent +analytic and finite-difference unit tests; PW neutral/charged results were also +compared against the original full PCC implementation. diff --git a/tests/01_PW/220_PW_PCC0D_WATER/STRU b/tests/01_PW/220_PW_PCC0D_WATER/STRU new file mode 100644 index 00000000000..dbcbe1d3767 --- /dev/null +++ b/tests/01_PW/220_PW_PCC0D_WATER/STRU @@ -0,0 +1,20 @@ +ATOMIC_SPECIES +O 15.999 O_ONCV_PBE-1.0.upf +H 1.008 H_ONCV_PBE-1.0.upf +LATTICE_CONSTANT +1.8897261258369282 +LATTICE_VECTORS +12.0 0 0 +0 12.0 0 +0 0 12.0 +ATOMIC_POSITIONS +Cartesian +O +0 +1 +6.0 6.0 6.0 1 1 1 +H +0 +2 +6.75716000000000 6.0 6.58626 1 1 1 +5.24284 6.0 6.58626 1 1 1 diff --git a/tests/01_PW/220_PW_PCC0D_WATER/result.ref b/tests/01_PW/220_PW_PCC0D_WATER/result.ref new file mode 100644 index 00000000000..92753a90d6b --- /dev/null +++ b/tests/01_PW/220_PW_PCC0D_WATER/result.ref @@ -0,0 +1,5 @@ +etotref -463.2670165144035650 +etotperatomref -154.4223388381 +totalforceref 4.388509 +epccref 0.0023549763 +totaltimeref 45.53 diff --git a/tests/01_PW/221_PW_PCC0D_H3O/INPUT b/tests/01_PW/221_PW_PCC0D_H3O/INPUT new file mode 100644 index 00000000000..d84a6039c12 --- /dev/null +++ b/tests/01_PW/221_PW_PCC0D_H3O/INPUT @@ -0,0 +1,21 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type pw +ks_solver cg +pseudo_dir ../../PP_ORB +ntype 2 +nbands 8 +ecutwfc 40 +ecutrho 160 +scf_thr 1e-10 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 1 +init_wfc atomic +imp_sol 0 +assume_isolated pcc_0d +nelec_delta -1 diff --git a/tests/01_PW/221_PW_PCC0D_H3O/KPT b/tests/01_PW/221_PW_PCC0D_H3O/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/01_PW/221_PW_PCC0D_H3O/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/01_PW/221_PW_PCC0D_H3O/README.md b/tests/01_PW/221_PW_PCC0D_H3O/README.md new file mode 100644 index 00000000000..42f90a8fac1 --- /dev/null +++ b/tests/01_PW/221_PW_PCC0D_H3O/README.md @@ -0,0 +1,7 @@ +Vacuum PCC 0D regression with `assume_isolated pcc_0d` and no solvent. + +The integration harness compares total energy, ionic force magnitudes and `E_pcc` +(in eV). References were generated with two MPI ranks, one OpenMP thread and +the repository pseudopotentials/orbitals. Numerical kernels have independent +analytic and finite-difference unit tests; PW neutral/charged results were also +compared against the original full PCC implementation. diff --git a/tests/01_PW/221_PW_PCC0D_H3O/STRU b/tests/01_PW/221_PW_PCC0D_H3O/STRU new file mode 100644 index 00000000000..75b5d3b13e0 --- /dev/null +++ b/tests/01_PW/221_PW_PCC0D_H3O/STRU @@ -0,0 +1,26 @@ +ATOMIC_SPECIES +O 15.9994 O_ONCV_PBE-1.0.upf +H 1.008 H_ONCV_PBE-1.0.upf + +LATTICE_CONSTANT +1.0 + +LATTICE_VECTORS +20.0 0.0 0.0 +0.0 20.0 0.0 +0.0 0.0 20.0 + +ATOMIC_POSITIONS +Cartesian + +O +0.0 +1 +10.0000000000 10.0000000000 10.0000000000 0 0 0 + +H +0.0 +3 +11.8077500000 9.6969000000 10.2918300000 0 0 0 +8.9439100000 8.5571900000 10.4978700000 0 0 0 +9.6429200000 10.5858300000 8.2760700000 0 0 0 diff --git a/tests/01_PW/221_PW_PCC0D_H3O/result.ref b/tests/01_PW/221_PW_PCC0D_H3O/result.ref new file mode 100644 index 00000000000..2ad5a347f6d --- /dev/null +++ b/tests/01_PW/221_PW_PCC0D_H3O/result.ref @@ -0,0 +1,5 @@ +etotref -470.6438272229737549 +etotperatomref -117.6609568057 +totalforceref 3.084305 +epccref 1.9931032737 +totaltimeref 12.31 diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index a302df9cad5..0dc9b7b79aa 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -138,3 +138,5 @@ scf_out_chg_tau 816_PW_DFTU_S4_XY scf_deltaspin2 scf_deltaspin4 +220_PW_PCC0D_WATER +221_PW_PCC0D_H3O diff --git a/tests/02_NAO_Gamma/CASES_CPU.txt b/tests/02_NAO_Gamma/CASES_CPU.txt index de87db6ecd4..6ec0573256b 100644 --- a/tests/02_NAO_Gamma/CASES_CPU.txt +++ b/tests/02_NAO_Gamma/CASES_CPU.txt @@ -38,3 +38,4 @@ scf_u_spin2_old #scf_u_yukawa scf_upf100 test.sum +scf_pcc0d_water diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT b/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT new file mode 100644 index 00000000000..d86e4840392 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT @@ -0,0 +1,22 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type lcao +ks_solver genelpa +pseudo_dir ../../PP_ORB +ntype 2 +nbands 8 +ecutwfc 40 +ecutrho 160 +scf_thr 1e-10 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 1 +init_wfc atomic +imp_sol 0 +assume_isolated pcc_0d +orbital_dir ../../PP_ORB +gamma_only 1 diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/KPT b/tests/02_NAO_Gamma/scf_pcc0d_water/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/README.md b/tests/02_NAO_Gamma/scf_pcc0d_water/README.md new file mode 100644 index 00000000000..42f90a8fac1 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/README.md @@ -0,0 +1,7 @@ +Vacuum PCC 0D regression with `assume_isolated pcc_0d` and no solvent. + +The integration harness compares total energy, ionic force magnitudes and `E_pcc` +(in eV). References were generated with two MPI ranks, one OpenMP thread and +the repository pseudopotentials/orbitals. Numerical kernels have independent +analytic and finite-difference unit tests; PW neutral/charged results were also +compared against the original full PCC implementation. diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/STRU b/tests/02_NAO_Gamma/scf_pcc0d_water/STRU new file mode 100644 index 00000000000..331f96c9743 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/STRU @@ -0,0 +1,24 @@ +ATOMIC_SPECIES +O 15.999 O_ONCV_PBE-1.0.upf +H 1.008 H_ONCV_PBE-1.0.upf +NUMERICAL_ORBITAL +O_gga_7au_100Ry_2s2p1d.orb +H_gga_8au_100Ry_2s1p.orb + +LATTICE_CONSTANT +1.8897261258369282 +LATTICE_VECTORS +12.0 0 0 +0 12.0 0 +0 0 12.0 +ATOMIC_POSITIONS +Cartesian +O +0 +1 +6.0 6.0 6.0 1 1 1 +H +0 +2 +6.75716000000000 6.0 6.58626 1 1 1 +5.24284 6.0 6.58626 1 1 1 diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/result.ref b/tests/02_NAO_Gamma/scf_pcc0d_water/result.ref new file mode 100644 index 00000000000..f15d83c334e --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/result.ref @@ -0,0 +1,5 @@ +etotref -466.4814265138713 +etotperatomref -155.4938088380 +totalforceref 3.801512 +epccref 0.0032764842 +totaltimeref 10.76 diff --git a/tests/integrate/tools/catch_properties.sh b/tests/integrate/tools/catch_properties.sh index c34b286e8fd..4bf8a375c01 100755 --- a/tests/integrate/tools/catch_properties.sh +++ b/tests/integrate/tools/catch_properties.sh @@ -104,6 +104,7 @@ out_dm=$(get_input_key_value "out_dm" "INPUT") out_mul=$(get_input_key_value "out_mul" "INPUT") gamma_only=$(get_input_key_value "gamma_only" "INPUT") imp_sol=$(get_input_key_value "imp_sol" "INPUT") +assume_isolated=$(get_input_key_value "assume_isolated" "INPUT") run_rpa=$(get_input_key_value "rpa" "INPUT") out_pot=$(get_input_key_value "out_pot" "INPUT") out_elf=$(get_input_key_value "out_elf" "INPUT") @@ -803,6 +804,14 @@ if ! test -z "$imp_sol" && [ $imp_sol == 1 ]; then echo "esolcavref $esol_cav" >>$1 fi +#-------------------------------------------- +# vacuum point-countercharge correction +#-------------------------------------------- +if [[ "$assume_isolated" == "pcc_0d" ]]; then + epcc=$(grep E_pcc "$running_path" | tail -1 | awk '{print $3}') + echo "epccref $epcc" >> "$1" +fi + #-------------------------------------------- # random phase approximation #-------------------------------------------- From 9b17a32e20c6b540fe87e258a27b9440232033e2 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 10:50:33 -0600 Subject: [PATCH 06/41] test(pcc): remove redundant slow neutral PW regression Drop the newly added 220_PW_PCC0D_WATER case and CASES_CPU entry. This case took about 48 seconds and overlaps neutral LCAO plus analytic and host coverage. Retain charged H3O+ PW and neutral water LCAO so both solver paths remain covered. Existing tests and thresholds are unchanged. Verification (OMP_NUM_THREADS=1, OPENBLAS_NUM_THREADS=1, outside sandbox): - Autotest.sh -n 2 -o 1 -j 1 -r '^221_PW_PCC0D_H3O$': passed, 10.826 s; energy, force and E_pcc checked at default thresholds. - Autotest.sh -n 2 -o 1 -j 1 -r '^scf_pcc0d_water$': passed, 9.858 s; energy, force and E_pcc checked at default thresholds. - Nine targeted CTest executables: passed, 0.21 s total. - Staged governance and git diff --cached --check: passed. Only test files and registration changed; no executable rebuild required. --- tests/01_PW/220_PW_PCC0D_WATER/INPUT | 20 -------------------- tests/01_PW/220_PW_PCC0D_WATER/KPT | 4 ---- tests/01_PW/220_PW_PCC0D_WATER/README.md | 7 ------- tests/01_PW/220_PW_PCC0D_WATER/STRU | 20 -------------------- tests/01_PW/220_PW_PCC0D_WATER/result.ref | 5 ----- tests/01_PW/CASES_CPU.txt | 1 - 6 files changed, 57 deletions(-) delete mode 100644 tests/01_PW/220_PW_PCC0D_WATER/INPUT delete mode 100644 tests/01_PW/220_PW_PCC0D_WATER/KPT delete mode 100644 tests/01_PW/220_PW_PCC0D_WATER/README.md delete mode 100644 tests/01_PW/220_PW_PCC0D_WATER/STRU delete mode 100644 tests/01_PW/220_PW_PCC0D_WATER/result.ref diff --git a/tests/01_PW/220_PW_PCC0D_WATER/INPUT b/tests/01_PW/220_PW_PCC0D_WATER/INPUT deleted file mode 100644 index 189737894ae..00000000000 --- a/tests/01_PW/220_PW_PCC0D_WATER/INPUT +++ /dev/null @@ -1,20 +0,0 @@ -INPUT_PARAMETERS -suffix autotest -calculation scf -basis_type pw -ks_solver cg -pseudo_dir ../../PP_ORB -ntype 2 -nbands 8 -ecutwfc 40 -ecutrho 160 -scf_thr 1e-10 -scf_nmax 200 -mixing_type pulay -mixing_beta 0.2 -smearing_method fixed -symmetry 0 -cal_force 1 -init_wfc atomic -imp_sol 0 -assume_isolated pcc_0d diff --git a/tests/01_PW/220_PW_PCC0D_WATER/KPT b/tests/01_PW/220_PW_PCC0D_WATER/KPT deleted file mode 100644 index c289c0158aa..00000000000 --- a/tests/01_PW/220_PW_PCC0D_WATER/KPT +++ /dev/null @@ -1,4 +0,0 @@ -K_POINTS -0 -Gamma -1 1 1 0 0 0 diff --git a/tests/01_PW/220_PW_PCC0D_WATER/README.md b/tests/01_PW/220_PW_PCC0D_WATER/README.md deleted file mode 100644 index 42f90a8fac1..00000000000 --- a/tests/01_PW/220_PW_PCC0D_WATER/README.md +++ /dev/null @@ -1,7 +0,0 @@ -Vacuum PCC 0D regression with `assume_isolated pcc_0d` and no solvent. - -The integration harness compares total energy, ionic force magnitudes and `E_pcc` -(in eV). References were generated with two MPI ranks, one OpenMP thread and -the repository pseudopotentials/orbitals. Numerical kernels have independent -analytic and finite-difference unit tests; PW neutral/charged results were also -compared against the original full PCC implementation. diff --git a/tests/01_PW/220_PW_PCC0D_WATER/STRU b/tests/01_PW/220_PW_PCC0D_WATER/STRU deleted file mode 100644 index dbcbe1d3767..00000000000 --- a/tests/01_PW/220_PW_PCC0D_WATER/STRU +++ /dev/null @@ -1,20 +0,0 @@ -ATOMIC_SPECIES -O 15.999 O_ONCV_PBE-1.0.upf -H 1.008 H_ONCV_PBE-1.0.upf -LATTICE_CONSTANT -1.8897261258369282 -LATTICE_VECTORS -12.0 0 0 -0 12.0 0 -0 0 12.0 -ATOMIC_POSITIONS -Cartesian -O -0 -1 -6.0 6.0 6.0 1 1 1 -H -0 -2 -6.75716000000000 6.0 6.58626 1 1 1 -5.24284 6.0 6.58626 1 1 1 diff --git a/tests/01_PW/220_PW_PCC0D_WATER/result.ref b/tests/01_PW/220_PW_PCC0D_WATER/result.ref deleted file mode 100644 index 92753a90d6b..00000000000 --- a/tests/01_PW/220_PW_PCC0D_WATER/result.ref +++ /dev/null @@ -1,5 +0,0 @@ -etotref -463.2670165144035650 -etotperatomref -154.4223388381 -totalforceref 4.388509 -epccref 0.0023549763 -totaltimeref 45.53 diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index 0dc9b7b79aa..1df4d1fc743 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -138,5 +138,4 @@ scf_out_chg_tau 816_PW_DFTU_S4_XY scf_deltaspin2 scf_deltaspin4 -220_PW_PCC0D_WATER 221_PW_PCC0D_H3O From 7b2fef0cd2969dc2140f8e2a2cfbe846df86cce2 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 11:35:26 -0600 Subject: [PATCH 07/41] feat(pcc): extract slab geometry and standalone PCC 2D kernels Build: abacus_basic_para, MODULE_CELL_cell_geometry and MODULE_ESTATE_pcc_2d passed. Both focused CTest targets and explicit C++11 syntax checks passed. This stage adds no INPUT behavior; user documentation follows in the integration stage. pcc_2d.h includes the charge-moment API used by its public kernel declarations; cell geometry is forward declared. --- source/Makefile.Objects | 1 + source/source_cell/cell_geometry.cpp | 99 +++++++++++++++++++ source/source_cell/cell_geometry.h | 29 ++++++ .../source_cell/test/test_cell_geometry.cpp | 47 +++++++++ source/source_estate/CMakeLists.txt | 1 + source/source_estate/pcc_2d.cpp | 66 +++++++++++++ source/source_estate/pcc_2d.h | 46 +++++++++ source/source_estate/test/CMakeLists.txt | 6 ++ source/source_estate/test/test_pcc_2d.cpp | 84 ++++++++++++++++ 9 files changed, 379 insertions(+) create mode 100644 source/source_estate/pcc_2d.cpp create mode 100644 source/source_estate/pcc_2d.h create mode 100644 source/source_estate/test/test_pcc_2d.cpp diff --git a/source/Makefile.Objects b/source/Makefile.Objects index d3313138c34..9845c44ad73 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -273,6 +273,7 @@ OBJS_ELECSTAT=elecstate.o\ makov_payne.o\ charge_moments.o\ pcc_0d.o\ + pcc_2d.o\ pot_pcc.o\ elecstate_op.o\ efield.o\ diff --git a/source/source_cell/cell_geometry.cpp b/source/source_cell/cell_geometry.cpp index 01745e38551..896b1839b74 100644 --- a/source/source_cell/cell_geometry.cpp +++ b/source/source_cell/cell_geometry.cpp @@ -132,4 +132,103 @@ bool weighted_center(const std::vector>& positions, return true; } +bool make_slab_cell(const ModuleBase::Matrix3& lattice, + const double lattice_scale, + const int open_axis, + const double relative_tolerance, + SlabCell& cell, + std::string& error) +{ + error.clear(); + if (open_axis < 0 || open_axis > 2 || !std::isfinite(lattice_scale) || lattice_scale <= 0.0 + || !std::isfinite(relative_tolerance) || relative_tolerance <= 0.0) + { + error = "slab geometry requires open axis 0/1/2 and positive finite scale and tolerance"; + return false; + } + const ModuleBase::Vector3 a(lattice.e11, lattice.e12, lattice.e13); + const ModuleBase::Vector3 b(lattice.e21, lattice.e22, lattice.e23); + const ModuleBase::Vector3 c(lattice.e31, lattice.e32, lattice.e33); + const std::array, 3> vectors = {{a, b, c}}; + const int first_axis = (open_axis + 1) % 3; + const int second_axis = (open_axis + 2) % 3; + const ModuleBase::Vector3 open = vectors[open_axis] * lattice_scale; + const ModuleBase::Vector3 first = vectors[first_axis] * lattice_scale; + const ModuleBase::Vector3 second = vectors[second_axis] * lattice_scale; + const double length = open.norm(); + const double first_length = first.norm(); + const double second_length = second.norm(); + const ModuleBase::Vector3 plane_normal = first ^ second; + const double area = plane_normal.norm(); + if (!finite_vector(open) || !finite_vector(first) || !finite_vector(second) + || !std::isfinite(length) || length <= 0.0 || !std::isfinite(area) + || area <= relative_tolerance * first_length * second_length) + { + error = "slab geometry requires finite nonzero open and independent periodic lattice vectors"; + return false; + } + const double orientation = (plane_normal * open < 0.0) ? -1.0 : 1.0; + const double normal_scale = orientation / area; + const ModuleBase::Vector3 normal = plane_normal * normal_scale; + const ModuleBase::Vector3 alignment = open ^ normal; + if (alignment.norm() > relative_tolerance * length) + { + error = "slab open lattice vector must be perpendicular to the periodic plane"; + return false; + } + SlabCell candidate; + candidate.normal = normal; + candidate.length = length; + candidate.area = area; + candidate.origin = 0.5 * length; + cell = candidate; + return true; +} + +double relative_coordinate(const ModuleBase::Vector3& position, const SlabCell& cell) +{ + const double displacement = position * cell.normal - cell.origin; + const double image = std::floor(displacement / cell.length + 0.5); + return displacement - cell.length * image; +} + +bool weighted_center(const std::vector>& positions, + const std::vector& weights, + const SlabCell& cell, + double& center, + std::string& error) +{ + error.clear(); + if (positions.empty() || positions.size() != weights.size()) + { + error = "slab center requires matching non-empty positions and weights"; + return false; + } + SlabCell reference = cell; + reference.origin = positions.front() * cell.normal; + double total_weight = 0.0; + double weighted_displacement = 0.0; + for (std::size_t index = 0; index < positions.size(); ++index) + { + if (!finite_vector(positions[index]) || !std::isfinite(weights[index]) || weights[index] <= 0.0) + { + error = "slab center requires finite positions and positive finite weights"; + return false; + } + const double displacement = relative_coordinate(positions[index], reference); + weighted_displacement += weights[index] * displacement; + total_weight += weights[index]; + } + const double unwrapped = reference.origin + weighted_displacement / total_weight; + const double image = std::floor(unwrapped / cell.length); + const double candidate = unwrapped - image * cell.length; + if (!std::isfinite(total_weight) || !std::isfinite(weighted_displacement) || !std::isfinite(candidate)) + { + error = "slab center accumulation must be finite"; + return false; + } + center = candidate; + return true; +} + } // namespace unitcell diff --git a/source/source_cell/cell_geometry.h b/source/source_cell/cell_geometry.h index 6f35714056d..591476bdbd1 100644 --- a/source/source_cell/cell_geometry.h +++ b/source/source_cell/cell_geometry.h @@ -48,6 +48,35 @@ bool weighted_center(const std::vector>& positions, ModuleBase::Vector3& center, std::string& error); +/// A periodic plane and its perpendicular open lattice vector, in Bohr. +/// The two in-plane lattice vectors need not be orthogonal. +struct SlabCell +{ + ModuleBase::Vector3 normal; + double length = 0.0; + double area = 0.0; + double origin = 0.0; +}; + +bool make_slab_cell(const ModuleBase::Matrix3& lattice, + double lattice_scale, + int open_axis, + double relative_tolerance, + SlabCell& cell, + std::string& error); + +/// Normal displacement in [-L/2, L/2); requires a validated cell and position. +double relative_coordinate(const ModuleBase::Vector3& position, const SlabCell& cell); + +/// Mass/weight center along the normal, unwrapped about the first position. +/// Requires a localized slab of extent less than half the normal period. +/// On failure, leave center unchanged and set error. +bool weighted_center(const std::vector>& positions, + const std::vector& weights, + const SlabCell& cell, + double& center, + std::string& error); + } // namespace unitcell #endif diff --git a/source/source_cell/test/test_cell_geometry.cpp b/source/source_cell/test/test_cell_geometry.cpp index 7dec40d4437..df9703a67ad 100644 --- a/source/source_cell/test/test_cell_geometry.cpp +++ b/source/source_cell/test/test_cell_geometry.cpp @@ -163,3 +163,50 @@ TEST_F(CellGeometryTest, RejectsOverflowingCenterAccumulation) EXPECT_FALSE(unitcell::weighted_center(positions, weights, cell, center, error)); EXPECT_FALSE(error.empty()); } + +TEST(SlabGeometry, SupportsSkewPeriodicPlaneAndRotatedNormal) +{ + const ModuleBase::Matrix3 lattice(0.0, 2.0, 0.0, + 0.0, 1.0, 3.0, + 8.0, 0.0, 0.0); + unitcell::SlabCell cell; + std::string error; + ASSERT_TRUE(unitcell::make_slab_cell(lattice, 1.0, 2, 1.0e-6, cell, error)); + EXPECT_DOUBLE_EQ(cell.length, 8.0); + EXPECT_DOUBLE_EQ(cell.area, 6.0); + EXPECT_DOUBLE_EQ(cell.normal.x, 1.0); + const ModuleBase::Vector3 point(8.1, 100.0, -200.0); + EXPECT_NEAR(unitcell::relative_coordinate(point, cell), -3.9, 1.0e-14); + const std::vector> positions = { + ModuleBase::Vector3(7.8, 0.0, 0.0), + ModuleBase::Vector3(0.2, 40.0, 50.0)}; + const std::vector weights = {1.0, 3.0}; + double center = 5.0; + ASSERT_TRUE(unitcell::weighted_center(positions, weights, cell, center, error)); + EXPECT_NEAR(center, 0.1, 1.0e-14); +} + +TEST(SlabGeometry, SelectsAllAxesAndRejectsTiltWithoutChangingResult) +{ + const ModuleBase::Matrix3 lattice(2.0, 0.0, 0.0, + 0.0, 3.0, 0.0, + 0.0, 0.0, 8.0); + const double lengths[3] = {2.0, 3.0, 8.0}; + unitcell::SlabCell cell; + std::string error; + for (int axis = 0; axis < 3; ++axis) + { + ASSERT_TRUE(unitcell::make_slab_cell(lattice, 1.0, axis, 1.0e-6, cell, error)); + EXPECT_DOUBLE_EQ(cell.length, lengths[axis]); + const double expected_area = 48.0 / lengths[axis]; + EXPECT_DOUBLE_EQ(cell.area, expected_area); + } + const ModuleBase::Matrix3 tilted(2.0, 0.0, 0.0, + 0.0, 3.0, 0.0, + 0.1, 0.0, 8.0); + EXPECT_FALSE(unitcell::make_slab_cell(tilted, 1.0, 2, 1.0e-6, cell, error)); + EXPECT_DOUBLE_EQ(cell.length, 8.0); + EXPECT_FALSE(unitcell::make_slab_cell(lattice, 1.0, 3, 1.0e-6, cell, error)); + EXPECT_FALSE(unitcell::make_slab_cell(lattice, 0.0, 2, 1.0e-6, cell, error)); + EXPECT_FALSE(error.empty()); +} diff --git a/source/source_estate/CMakeLists.txt b/source/source_estate/CMakeLists.txt index cefee5e400b..c7b71d606c4 100644 --- a/source/source_estate/CMakeLists.txt +++ b/source/source_estate/CMakeLists.txt @@ -13,6 +13,7 @@ list(APPEND objects makov_payne.cpp charge_moments.cpp pcc_0d.cpp + pcc_2d.cpp module_pot/gatefield.cpp module_pot/efield.cpp module_pot/h_hartree_pw.cpp diff --git a/source/source_estate/pcc_2d.cpp b/source/source_estate/pcc_2d.cpp new file mode 100644 index 00000000000..33d40f47f2b --- /dev/null +++ b/source/source_estate/pcc_2d.cpp @@ -0,0 +1,66 @@ +#include "pcc_2d.h" + +#include "source_base/constants.h" +#include "source_cell/cell_geometry.h" + +#include + +namespace elecstate +{ +bool make_pcc_2d_parameters(const unitcell::SlabCell& cell, + Pcc2dParameters& parameters, + std::string& error) +{ + error.clear(); + if (!std::isfinite(cell.area) || cell.area <= 0.0 || !std::isfinite(cell.length) || cell.length <= 0.0) + { + error = "PCC 2D requires positive finite periodic area and normal length"; + return false; + } + parameters.area = cell.area; + parameters.length = cell.length; + return true; +} + +double pcc_2d_potential(const ChargeMoments& moments, + const double coordinate, + const ModuleBase::Vector3& normal, + const Pcc2dParameters& parameters) +{ + const double dipole = moments.dipole * normal; + const double parabolic = moments.charge * coordinate * coordinate + - 2.0 * dipole * coordinate + moments.second_moment; + const double constant = -ModuleBase::PI * parameters.length / (3.0 * parameters.area); + const double factor = 2.0 * ModuleBase::PI / (parameters.area * parameters.length); + return constant * moments.charge - factor * parabolic; +} + +ModuleBase::Vector3 pcc_2d_gradient(const ChargeMoments& moments, + const double coordinate, + const ModuleBase::Vector3& normal, + const Pcc2dParameters& parameters) +{ + const double dipole = moments.dipole * normal; + const double factor = -4.0 * ModuleBase::PI / (parameters.area * parameters.length); + const double derivative = factor * (moments.charge * coordinate - dipole); + return normal * derivative; +} + +double pcc_2d_energy(const ChargeMoments& moments, const Pcc2dParameters& parameters) +{ + const double constant = -ModuleBase::PI * parameters.length / (3.0 * parameters.area); + const double factor = 2.0 * ModuleBase::PI / (parameters.area * parameters.length); + const double multipole = 2.0 * (moments.charge * moments.second_moment - moments.dipole.norm2()); + return 0.5 * (constant * moments.charge * moments.charge - factor * multipole); +} + +ModuleBase::Vector3 pcc_2d_force(const ChargeMoments& moments, + const double ionic_charge, + const double coordinate, + const ModuleBase::Vector3& normal, + const Pcc2dParameters& parameters) +{ + const ModuleBase::Vector3 gradient = pcc_2d_gradient(moments, coordinate, normal, parameters); + return gradient * (-ionic_charge); +} +} // namespace elecstate diff --git a/source/source_estate/pcc_2d.h b/source/source_estate/pcc_2d.h new file mode 100644 index 00000000000..ed902148613 --- /dev/null +++ b/source/source_estate/pcc_2d.h @@ -0,0 +1,46 @@ +#ifndef PCC_2D_H +#define PCC_2D_H + +#include "charge_moments.h" + +namespace unitcell +{ +struct SlabCell; +} + +namespace elecstate +{ +struct Pcc2dParameters +{ + double area = 0.0; ///< Periodic area in Bohr^2 + double length = 0.0; ///< Normal period in Bohr +}; + +bool make_pcc_2d_parameters(const unitcell::SlabCell& cell, + Pcc2dParameters& parameters, + std::string& error); + +/// Hartree kernels. Parameters must be validated; moments use normal-projected +/// positions r = normal * u, so second_moment is the normal second moment. +/// The open planar kernel is zero on the source plane; its periodic counterpart +/// has zero mean. This gauge fixes the charged-slab constant to -pi*L/(3*A). +double pcc_2d_potential(const ChargeMoments& moments, + double coordinate, + const ModuleBase::Vector3& normal, + const Pcc2dParameters& parameters); + +ModuleBase::Vector3 pcc_2d_gradient(const ChargeMoments& moments, + double coordinate, + const ModuleBase::Vector3& normal, + const Pcc2dParameters& parameters); + +double pcc_2d_energy(const ChargeMoments& moments, const Pcc2dParameters& parameters); + +ModuleBase::Vector3 pcc_2d_force(const ChargeMoments& moments, + double ionic_charge, + double coordinate, + const ModuleBase::Vector3& normal, + const Pcc2dParameters& parameters); +} // namespace elecstate + +#endif diff --git a/source/source_estate/test/CMakeLists.txt b/source/source_estate/test/CMakeLists.txt index e9fbf59952b..d27c33051c0 100644 --- a/source/source_estate/test/CMakeLists.txt +++ b/source/source_estate/test/CMakeLists.txt @@ -19,6 +19,12 @@ AddTest( SOURCES test_pcc_0d.cpp ../pcc_0d.cpp ../charge_moments.cpp ../../source_cell/cell_geometry.cpp ) +AddTest( + TARGET MODULE_ESTATE_pcc_2d + LIBS base device + SOURCES test_pcc_2d.cpp ../pcc_2d.cpp ../charge_moments.cpp ../../source_cell/cell_geometry.cpp +) + if (ENABLE_MPI) set(pcc_test_sources diff --git a/source/source_estate/test/test_pcc_2d.cpp b/source/source_estate/test/test_pcc_2d.cpp new file mode 100644 index 00000000000..39b18590da1 --- /dev/null +++ b/source/source_estate/test/test_pcc_2d.cpp @@ -0,0 +1,84 @@ +#include "source_estate/pcc_2d.h" +#include "source_base/constants.h" +#include "source_cell/cell_geometry.h" + +#include + +TEST(Pcc2d, MatchesChargedSheetGaugeAndNeutralDipoleEnergy) +{ + unitcell::SlabCell cell; + cell.area = 6.0; + cell.length = 8.0; + elecstate::Pcc2dParameters parameters; + std::string error; + ASSERT_TRUE(elecstate::make_pcc_2d_parameters(cell, parameters, error)); + elecstate::ChargeMoments moments; + moments.charge = 2.0; + const double charged = elecstate::pcc_2d_energy(moments, parameters); + const double expected_charged = -ModuleBase::PI * 8.0 * 4.0 / 36.0; + EXPECT_NEAR(charged, expected_charged, 1.0e-14); + moments.charge = 0.0; + moments.dipole.x = 2.0; + const double neutral = elecstate::pcc_2d_energy(moments, parameters); + const double expected_neutral = 2.0 * ModuleBase::PI * 4.0 / 48.0; + EXPECT_NEAR(neutral, expected_neutral, 1.0e-14); + cell.area = 0.0; + EXPECT_FALSE(elecstate::make_pcc_2d_parameters(cell, parameters, error)); + EXPECT_DOUBLE_EQ(parameters.area, 6.0); +} + +TEST(Pcc2d, NormalForceMatchesEnergyDerivativeAndHasNoTangentialComponent) +{ + elecstate::Pcc2dParameters parameters; + parameters.area = 6.0; + parameters.length = 8.0; + const ModuleBase::Vector3 normal(0.0, 1.0, 0.0); + const double charges[2] = {1.3, -0.4}; + ModuleBase::Vector3 positions[2] = { + ModuleBase::Vector3(0.0, 0.2, 0.0), + ModuleBase::Vector3(0.0, -0.5, 0.0)}; + elecstate::ChargeMoments moments; + std::string error; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const double coordinate = positions[0].y; + const ModuleBase::Vector3 force = elecstate::pcc_2d_force(moments, charges[0], coordinate, normal, parameters); + EXPECT_DOUBLE_EQ(force.x, 0.0); + EXPECT_DOUBLE_EQ(force.z, 0.0); + const double step = 1.0e-5; + positions[0].y = coordinate + step; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const double plus = elecstate::pcc_2d_energy(moments, parameters); + positions[0].y = coordinate - step; + ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + const double minus = elecstate::pcc_2d_energy(moments, parameters); + const double numerical_force = -(plus - minus) / (2.0 * step); + EXPECT_NEAR(force.y, numerical_force, 1.0e-10); +} + +TEST(Pcc2d, ElectronicPotentialMatchesEnergyDerivative) +{ + elecstate::Pcc2dParameters parameters; + parameters.area = 6.0; + parameters.length = 8.0; + const ModuleBase::Vector3 normal(1.0, 0.0, 0.0); + const double coordinate = 0.3; + elecstate::ChargeMoments moments; + moments.charge = 1.2; + moments.dipole.x = 0.2; + moments.second_moment = 0.8; + const double positive_potential = elecstate::pcc_2d_potential(moments, coordinate, normal, parameters); + const double expected = -2.0 * positive_potential; + const double step = 1.0e-5; + elecstate::ChargeMoments plus = moments; + plus.charge -= step; + plus.dipole.x -= coordinate * step; + plus.second_moment -= coordinate * coordinate * step; + elecstate::ChargeMoments minus = moments; + minus.charge += step; + minus.dipole.x += coordinate * step; + minus.second_moment += coordinate * coordinate * step; + const double plus_energy = 2.0 * elecstate::pcc_2d_energy(plus, parameters); + const double minus_energy = 2.0 * elecstate::pcc_2d_energy(minus, parameters); + const double derivative = (plus_energy - minus_energy) / (2.0 * step); + EXPECT_NEAR(derivative, expected, 1.0e-10); +} From 70cc98ae0fbbc74e2ecdbf3ce09114f103ca7e19 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 11:44:41 -0600 Subject: [PATCH 08/41] feat(pcc): integrate vacuum slab correction through PotPcc Keep dimension and open axis immutable at construction. Reuse charge moments, pool reductions, energy, force and electrostatic output consumers. Require Gamma-only open-direction sampling and symmetry that preserves the slab. Add pcc_2d_axis INPUT metadata and regenerate both parameter documents. Report the actual failure stage and reject missing density storage via the standard ABACUS WARNING_QUIT path. INPUT errors identify conflicting settings. Verification: - Full executable and affected unit targets built successfully. - Ten targeted CTest targets passed in 0.22 s (OMP_NUM_THREADS=1). - Explicit C++11 syntax checks passed for geometry, kernel and PotPcc. - PW charged and LCAO neutral 2D SCFs converged on two MPI ranks; comparison with the saved original executable agrees within 2.7e-10 eV energy and 2.5e-5 eV/Angstrom force components (old run used one rank). - Metadata generation and pcc_2d_axis CLI help passed. - Staged governance and diff checks passed without blocking findings. Global references: added 2, removed 6, net -4. The existing potential factory reads the axis from PARAM and passes it explicitly into PotPcc; PW setup now shares one const INPUT reference for existing component selection. PotPcc and numerical helpers add no globals. The new header include owns Pcc2dParameters by value and thus needs the complete definition. --- docs/advanced/input_files/input-main.md | 9 + docs/parameters.yaml | 9 + source/source_estate/elecstate_energy.cpp | 2 +- source/source_estate/elecstate_print.cpp | 2 +- source/source_estate/module_pot/pot_pcc.cpp | 200 +++++++++++++++--- source/source_estate/module_pot/pot_pcc.h | 15 ++ .../module_pot/potential_types.cpp | 5 + source/source_estate/test/CMakeLists.txt | 2 +- .../test/elecstate_energy_test.cpp | 4 + source/source_estate/test/test_pot_pcc.cpp | 51 +++++ .../module_parameter/input_parameter.h | 3 +- .../module_parameter/read_inp_sys.cpp | 52 ++++- .../test_serial/read_input_item_test.cpp | 11 + source/source_lcao/hamilt_lcao.cpp | 7 + source/source_pw/module_pwdft/hamilt_pw.cpp | 19 +- tests/integrate/tools/catch_properties.sh | 2 +- 16 files changed, 344 insertions(+), 49 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 5478038a0e7..455ccd133cb 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -20,6 +20,7 @@ - [bndpar](#bndpar) - [latname](#latname) - [assume\_isolated](#assume_isolated) + - [pcc\_2d\_axis](#pcc_2d_axis) - [init\_wfc](#init_wfc) - [init\_chg](#init_chg) - [init\_vel](#init_vel) @@ -768,11 +769,19 @@ - none: regular periodic calculation without isolated-system correction. - makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). + - pcc_2d: self-consistent vacuum point-countercharge correction for a slab, including potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. The open lattice vector is selected by `pcc_2d_axis` and must be perpendicular to the periodic plane; the two periodic vectors need not be orthogonal. K-point sampling along the open vector must be Gamma only. Symmetry must preserve that vector and cannot fractionally translate the slab along it. Moments use the mass-weighted ionic center along the normal; the half-cell wrapping plane must lie in vacuum. The charged-slab gauge is the open planar kernel zero on the source plane minus the zero-mean periodic kernel, with constant -pi*L/(3*A). The CPU, solver and incompatibility restrictions are the same as pcc_0d. - pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014). - **Default**: none +### pcc_2d_axis + +- **Type**: Integer +- **Availability**: *[`assume_isolated`](#assume_isolated)==pcc_2d* +- **Description**: Index of the open lattice vector for assume_isolated=pcc_2d: 0, 1 or 2 select the first, second or third lattice vector. This vector must be perpendicular to the periodic plane, with Gamma-only k-point sampling along it. Symmetry operations must preserve the open axis and cannot fractionally translate the slab along it. +- **Default**: 2 + ### init_wfc - **Type**: Vector of string diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 2e329c503be..ae528e51714 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -179,12 +179,21 @@ parameters: Available options are: * none: regular periodic calculation without isolated-system correction. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). + * pcc_2d: self-consistent vacuum point-countercharge correction for a slab, including potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. The open lattice vector is selected by `pcc_2d_axis` and must be perpendicular to the periodic plane; the two periodic vectors need not be orthogonal. K-point sampling along the open vector must be Gamma only. Symmetry must preserve that vector and cannot fractionally translate the slab along it. Moments use the mass-weighted ionic center along the normal; the half-cell wrapping plane must lie in vacuum. The charged-slab gauge is the open planar kernel zero on the source plane minus the zero-mean periodic kernel, with constant -pi*L/(3*A). The CPU, solver and incompatibility restrictions are the same as pcc_0d. * pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014). default_value: none unit: "" availability: "" + - name: pcc_2d_axis + category: System variables + type: Integer + description: | + Index of the open lattice vector for assume_isolated=pcc_2d: 0, 1 or 2 select the first, second or third lattice vector. This vector must be perpendicular to the periodic plane, with Gamma-only k-point sampling along it. Symmetry operations must preserve the open axis and cannot fractionally translate the slab along it. + default_value: "2" + unit: "" + availability: assume_isolated==pcc_2d - name: init_wfc category: System variables type: Vector of string diff --git a/source/source_estate/elecstate_energy.cpp b/source/source_estate/elecstate_energy.cpp index 0c8e9687761..043a7057d8c 100644 --- a/source/source_estate/elecstate_energy.cpp +++ b/source/source_estate/elecstate_energy.cpp @@ -374,7 +374,7 @@ void ElecState::cal_energies(const int type, } this->f_en.correction_el = makov_payne_correction(*ucell, *this->charge, v_elecstat_ptr).total; } - else if (assume_isolated == "pcc_0d") + else if (assume_isolated == "pcc_0d" || assume_isolated == "pcc_2d") { this->f_en.correction_el = this->pot->pcc_energy_rydberg(); } diff --git a/source/source_estate/elecstate_print.cpp b/source/source_estate/elecstate_print.cpp index f87263e296a..4b978c65f70 100644 --- a/source/source_estate/elecstate_print.cpp +++ b/source/source_estate/elecstate_print.cpp @@ -287,7 +287,7 @@ void print_etot(const Magnetism& magnet, energies_Ry.push_back(elec.f_en.esol_cav); } - if (inp.assume_isolated == "pcc_0d") + if (inp.assume_isolated == "pcc_0d" || inp.assume_isolated == "pcc_2d") { titles.push_back("E_pcc"); energies_Ry.push_back(elec.f_en.correction_el); diff --git a/source/source_estate/module_pot/pot_pcc.cpp b/source/source_estate/module_pot/pot_pcc.cpp index 3bf45b9b2c5..14fdc69824e 100644 --- a/source/source_estate/module_pot/pot_pcc.cpp +++ b/source/source_estate/module_pot/pot_pcc.cpp @@ -7,10 +7,11 @@ #include "source_cell/cell_tools.h" #include +#include namespace { -void require_valid_on_pool(const bool valid, const std::string& error) +void require_valid_on_pool(const bool valid, const std::string& error, const std::string& function) { double invalid = valid ? 0.0 : 1.0; #ifdef __MPI @@ -23,7 +24,7 @@ void require_valid_on_pool(const bool valid, const std::string& error) { message = "invalid PCC input or result on another process in this pool"; } - ModuleBase::WARNING_QUIT("PotPcc::cal_v_eff", message); + ModuleBase::WARNING_QUIT(function, message); } } @@ -76,30 +77,157 @@ bool primitive_symmetry(const ModuleSymmetry::Symmetry& symmetry) } return true; } +bool slab_operation_valid(const ModuleBase::Matrix3& rotation, + const ModuleBase::Vector3& translation, + const int axis) +{ + const double entries[3][3] = {{rotation.e11, rotation.e12, rotation.e13}, + {rotation.e21, rotation.e22, rotation.e23}, + {rotation.e31, rotation.e32, rotation.e33}}; + const double translations[3] = {translation.x, translation.y, translation.z}; + for (int other = 0; other < 3; ++other) + { + if (other != axis && (std::abs(entries[axis][other]) > 1.0e-6 + || std::abs(entries[other][axis]) > 1.0e-6)) + { + return false; + } + } + const double fraction = translations[axis] - std::round(translations[axis]); + return entries[axis][axis] < 0.0 || std::abs(fraction) <= 1.0e-6; +} + +bool slab_symmetry_valid(const ModuleSymmetry::Symmetry& symmetry, const int axis) +{ + for (int operation = 0; operation < symmetry.nrotk; ++operation) + { + const bool valid = slab_operation_valid(symmetry.gmatrix[operation], symmetry.gtrans[operation], axis); + if (!valid) { return false; } + } + for (int operation = 0; operation < symmetry.nrotk_anti; ++operation) + { + const bool valid = slab_operation_valid(symmetry.gmatrix_anti[operation], symmetry.gtrans_anti[operation], axis); + if (!valid) { return false; } + } + const ModuleBase::Matrix3 identity; + for (const auto& translation : symmetry.ptrans) + { + const bool valid = slab_operation_valid(identity, translation, axis); + if (!valid) { return false; } + } + return true; +} } // namespace namespace elecstate { PotPcc::PotPcc(const ModulePW::PW_Basis* basis) + : dimension_(Dimension::molecule), open_axis_(2) { this->rho_basis_ = basis; this->dynamic_mode = true; } +PotPcc::PotPcc(const ModulePW::PW_Basis* basis, const Dimension dimension, const int open_axis) + : dimension_(dimension), open_axis_(open_axis) +{ + this->rho_basis_ = basis; + this->dynamic_mode = true; + if (open_axis < 0 || open_axis > 2) + { + ModuleBase::WARNING_QUIT("PotPcc", "pcc_2d_axis must be 0, 1 or 2"); + } +} + +void PotPcc::validate_kpoints(const std::vector>& points, + const int count, + const int open_axis) +{ + double maximum = 0.0; + if (open_axis < 0 || open_axis > 2 || count < 0 || count > static_cast(points.size())) + { + maximum = 1.0; + } + else + { + for (int ik = 0; ik < count; ++ik) + { + const double components[3] = {points[ik].x, points[ik].y, points[ik].z}; + const double coordinate = components[open_axis]; + const double absolute = std::abs(coordinate); + if (!std::isfinite(coordinate)) { maximum = 1.0; } + else { maximum = std::max(maximum, absolute); } + } + } +#ifdef __MPI + // All pools must reject a forbidden open-direction k point together. + Parallel_Reduce::reduce_max(maximum); +#endif + if (maximum > 1.0e-12) + { + ModuleBase::WARNING_QUIT("PotPcc::validate_kpoints", "pcc_2d requires Gamma-only sampling along pcc_2d_axis"); + } +} + +ModuleBase::Vector3 PotPcc::relative_position(const ModuleBase::Vector3& position) const +{ + if (dimension_ == Dimension::molecule) + { + return unitcell::relative_position(position, geometry_); + } + const double coordinate = unitcell::relative_coordinate(position, slab_); + return slab_.normal * coordinate; +} + +double PotPcc::correction_energy() const +{ + if (dimension_ == Dimension::molecule) { return pcc_0d_energy(moments_, parameters_); } + return pcc_2d_energy(moments_, slab_parameters_); +} + +double PotPcc::correction_potential(const ModuleBase::Vector3& position) const +{ + if (dimension_ == Dimension::molecule) { return pcc_0d_potential(moments_, position, parameters_); } + const double coordinate = position * slab_.normal; + return pcc_2d_potential(moments_, coordinate, slab_.normal, slab_parameters_); +} + +ModuleBase::Vector3 PotPcc::correction_force(const double charge, + const ModuleBase::Vector3& position) const +{ + if (dimension_ == Dimension::molecule) { return pcc_0d_force(moments_, charge, position, parameters_); } + const double coordinate = position * slab_.normal; + return pcc_2d_force(moments_, charge, coordinate, slab_.normal, slab_parameters_); +} + void PotPcc::prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments) { ModuleBase::timer::start("PotPcc", "prepare_ions"); std::string error; - const bool geometry_valid = unitcell::make_orthogonal_cell(cell.latvec, cell.lat0, 1.0e-10, geometry_, error); - require_valid_on_pool(geometry_valid, error); - const bool parameters_valid = make_pcc_0d_parameters(geometry_, 1.0e-10, parameters_, error); - require_valid_on_pool(parameters_valid, error); - const bool symmetry_valid = primitive_symmetry(cell.symm); - require_valid_on_pool(symmetry_valid, "PCC 0D requires a primitive cell without fractional-translation symmetry"); + if (dimension_ == Dimension::molecule) + { + const bool geometry_valid = unitcell::make_orthogonal_cell(cell.latvec, cell.lat0, 1.0e-10, geometry_, error); + require_valid_on_pool(geometry_valid, error, "PotPcc::prepare_ions"); + const bool parameters_valid = make_pcc_0d_parameters(geometry_, 1.0e-10, parameters_, error); + require_valid_on_pool(parameters_valid, error, "PotPcc::prepare_ions"); + const bool symmetry_valid = primitive_symmetry(cell.symm); + require_valid_on_pool(symmetry_valid, "PCC 0D requires a primitive cell without fractional-translation symmetry", "PotPcc::prepare_ions"); + } + else + { + const bool geometry_valid = unitcell::make_slab_cell(cell.latvec, cell.lat0, open_axis_, 1.0e-6, slab_, error); + require_valid_on_pool(geometry_valid, error, "PotPcc::prepare_ions"); + const bool parameters_valid = make_pcc_2d_parameters(slab_, slab_parameters_, error); + require_valid_on_pool(parameters_valid, error, "PotPcc::prepare_ions"); + const bool symmetry_valid = slab_symmetry_valid(cell.symm, open_axis_); + require_valid_on_pool(symmetry_valid, "PCC 2D symmetry must preserve the open axis without fractional translations along it", "PotPcc::prepare_ions"); + } const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); const int atom_count = static_cast(atoms.size()); + const bool atom_count_valid = atom_count == cell.nat; + require_valid_on_pool(atom_count_valid, "PCC atom count does not match UnitCell", "PotPcc::prepare_ions"); std::vector masses(atom_count); ionic_positions_.resize(atom_count); ionic_charges_.resize(atom_count); @@ -109,14 +237,24 @@ void PotPcc::prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments) ionic_charges_[atom] = atoms[atom].valence_charge; masses[atom] = atoms[atom].mass; } - ModuleBase::Vector3 center; - const bool center_valid = unitcell::weighted_center(ionic_positions_, masses, geometry_, center, error); - require_valid_on_pool(center_valid, error); - geometry_.origin = center; + if (dimension_ == Dimension::molecule) + { + ModuleBase::Vector3 center; + const bool center_valid = unitcell::weighted_center(ionic_positions_, masses, geometry_, center, error); + require_valid_on_pool(center_valid, error, "PotPcc::prepare_ions"); + geometry_.origin = center; + } + else + { + double center = 0.0; + const bool center_valid = unitcell::weighted_center(ionic_positions_, masses, slab_, center, error); + require_valid_on_pool(center_valid, error, "PotPcc::prepare_ions"); + slab_.origin = center; + } std::vector> relative_ions(atom_count); for (int atom = 0; atom < atom_count; ++atom) { - relative_ions[atom] = unitcell::relative_position(ionic_positions_[atom], geometry_); + relative_ions[atom] = this->relative_position(ionic_positions_[atom]); } const double* ionic_charge_data = ionic_charges_.data(); const ModuleBase::Vector3* ionic_position_data = relative_ions.data(); @@ -126,7 +264,7 @@ void PotPcc::prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments) 1.0, ionic_moments, error); - require_valid_on_pool(ions_valid, error); + require_valid_on_pool(ions_valid, error, "PotPcc::prepare_ions"); ModuleBase::timer::end("PotPcc", "prepare_ions"); } @@ -141,14 +279,14 @@ ChargeMoments PotPcc::collect_electrons( std::string error; std::vector electronic_charge(basis.nrxx, 0.0); const bool positions_valid = ModulePW::grid_positions(basis, cell.latvec, cell.lat0, positions, error); - require_valid_on_pool(positions_valid, error); + require_valid_on_pool(positions_valid, error, "PotPcc::collect_electrons"); for (int ir = 0; ir < basis.nrxx; ++ir) { for (int spin = 0; spin < charge.nspin; ++spin) { electronic_charge[ir] -= charge.rho[spin][ir]; } - positions[ir] = unitcell::relative_position(positions[ir], geometry_); + positions[ir] = this->relative_position(positions[ir]); } ChargeMoments electronic_moments; const double volume_element = cell.omega / basis.nxyz; @@ -160,7 +298,7 @@ ChargeMoments PotPcc::collect_electrons( volume_element, electronic_moments, error); - require_valid_on_pool(density_valid, error); + require_valid_on_pool(density_valid, error, "PotPcc::collect_electrons"); double reduced[5] = {electronic_moments.charge, electronic_moments.dipole.x, electronic_moments.dipole.y, electronic_moments.dipole.z, electronic_moments.second_moment}; @@ -179,30 +317,39 @@ void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::m ModuleBase::timer::start("PotPcc", "cal_v_eff"); result_valid_ = false; const bool storage_valid = charge != nullptr && cell != nullptr && this->rho_basis_ != nullptr; - require_valid_on_pool(storage_valid, "PCC requires charge, cell and PW basis storage"); + require_valid_on_pool(storage_valid, "PCC requires charge, cell and PW basis storage", "PotPcc::cal_v_eff"); const ModulePW::PW_Basis& basis = *this->rho_basis_; const bool grid_valid = basis.nx > 0 && basis.ny > 0 && basis.nz > 0 && basis.nxyz > 0 && basis.nplane >= 0 && basis.nrxx == basis.nx * basis.ny * basis.nplane && (charge->nspin == 1 || charge->nspin == 2) && potential.nr == charge->nspin && potential.nc == basis.nrxx && std::isfinite(cell->omega) && cell->omega > 0.0; - require_valid_on_pool(grid_valid, "PCC requires an initialized grid and nspin=1/2 potential"); + require_valid_on_pool(grid_valid, "PCC requires an initialized grid and nspin=1/2 potential", "PotPcc::cal_v_eff"); + bool density_storage_valid = charge->rho != nullptr; + if (density_storage_valid && basis.nrxx > 0) + { + for (int spin = 0; spin < charge->nspin; ++spin) + { + density_storage_valid = density_storage_valid && charge->rho[spin] != nullptr; + } + } + require_valid_on_pool(density_storage_valid, "PCC charge density is not available", "PotPcc::cal_v_eff"); ChargeMoments ionic_moments; this->prepare_ions(*cell, ionic_moments); std::vector> positions; const ChargeMoments electronic_moments = this->collect_electrons(*charge, *cell, positions); // Ions are replicated on every rank and must be added after the reduction. moments_ = add_charge_moments(ionic_moments, electronic_moments); - energy_rydberg_ = 2.0 * pcc_0d_energy(moments_, parameters_); + energy_rydberg_ = 2.0 * this->correction_energy(); electron_potential_.resize(basis.nrxx); bool result_finite = std::isfinite(energy_rydberg_); for (int ir = 0; ir < basis.nrxx; ++ir) { - const double positive_potential = pcc_0d_potential(moments_, positions[ir], parameters_); + const double positive_potential = this->correction_potential(positions[ir]); electron_potential_[ir] = -2.0 * positive_potential; result_finite = result_finite && std::isfinite(electron_potential_[ir]); } - require_valid_on_pool(result_finite, "PCC energy or potential is nonfinite"); + require_valid_on_pool(result_finite, "PCC energy or potential is nonfinite", "PotPcc::cal_v_eff"); for (int spin = 0; spin < charge->nspin; ++spin) { for (int ir = 0; ir < basis.nrxx; ++ir) @@ -240,7 +387,7 @@ void PotPcc::add_force(const UnitCell& cell, ModuleBase::matrix& force) const ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC force requires a current potential and nat-by-3 output"); } const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); - if (atoms.size() != ionic_positions_.size()) + if (atoms.size() != ionic_positions_.size() || atoms.size() != static_cast(cell.nat)) { ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC atom count changed since the potential update"); } @@ -251,11 +398,8 @@ void PotPcc::add_force(const UnitCell& cell, ModuleBase::matrix& force) const { ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC atoms changed since the potential update"); } - const ModuleBase::Vector3 relative = unitcell::relative_position(ionic_positions_[atom], geometry_); - const ModuleBase::Vector3 correction = pcc_0d_force(moments_, - ionic_charges_[atom], - relative, - parameters_); + const ModuleBase::Vector3 relative = this->relative_position(ionic_positions_[atom]); + const ModuleBase::Vector3 correction = this->correction_force(ionic_charges_[atom], relative); force(atom, 0) += 2.0 * correction.x; force(atom, 1) += 2.0 * correction.y; force(atom, 2) += 2.0 * correction.z; diff --git a/source/source_estate/module_pot/pot_pcc.h b/source/source_estate/module_pot/pot_pcc.h index a4a9fe5dd73..378809c2f8d 100644 --- a/source/source_estate/module_pot/pot_pcc.h +++ b/source/source_estate/module_pot/pot_pcc.h @@ -4,6 +4,7 @@ #include "pot_base.h" #include "source_cell/cell_geometry.h" #include "source_estate/pcc_0d.h" +#include "source_estate/pcc_2d.h" namespace elecstate { @@ -12,8 +13,13 @@ namespace elecstate class PotPcc : public PotBase { public: + enum class Dimension { molecule, slab }; + explicit PotPcc(const ModulePW::PW_Basis* basis); + PotPcc(const ModulePW::PW_Basis* basis, Dimension dimension, int open_axis); + static void validate_kpoints(const std::vector>& points, int count, int open_axis); + void cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) override; double get_energy() const override; const std::vector& electron_potential() const; @@ -25,6 +31,15 @@ class PotPcc : public PotBase const UnitCell& cell, std::vector>& positions) const; + ModuleBase::Vector3 relative_position(const ModuleBase::Vector3& position) const; + double correction_energy() const; + double correction_potential(const ModuleBase::Vector3& position) const; + ModuleBase::Vector3 correction_force(double charge, const ModuleBase::Vector3& position) const; + + const Dimension dimension_; + const int open_axis_; + unitcell::SlabCell slab_; + Pcc2dParameters slab_parameters_; unitcell::OrthogonalCell geometry_; Pcc0dParameters parameters_; ChargeMoments moments_; diff --git a/source/source_estate/module_pot/potential_types.cpp b/source/source_estate/module_pot/potential_types.cpp index ecbcc7243f1..748c2b03e99 100644 --- a/source/source_estate/module_pot/potential_types.cpp +++ b/source/source_estate/module_pot/potential_types.cpp @@ -40,6 +40,11 @@ PotBase* Potential::get_pot_type(const std::string& pot_type) { return new PotPcc(this->rho_basis_); } + else if (pot_type == "pcc_2d") + { + const int open_axis = PARAM.inp.pcc_2d_axis; + return new PotPcc(this->rho_basis_, PotPcc::Dimension::slab, open_axis); + } else if (pot_type == "surchem") { return new PotSurChem(this->rho_basis_, this->structure_factors_, this->v_eff_fixed.data(), this->solvent_); diff --git a/source/source_estate/test/CMakeLists.txt b/source/source_estate/test/CMakeLists.txt index d27c33051c0..a39b330c356 100644 --- a/source/source_estate/test/CMakeLists.txt +++ b/source/source_estate/test/CMakeLists.txt @@ -28,7 +28,7 @@ AddTest( if (ENABLE_MPI) set(pcc_test_sources - ../module_pot/pot_pcc.cpp ../pcc_0d.cpp ../charge_moments.cpp + ../module_pot/pot_pcc.cpp ../pcc_0d.cpp ../pcc_2d.cpp ../charge_moments.cpp ../../source_cell/cell_geometry.cpp ../../source_cell/cell_tools.cpp ../../source_cell/atom_spec.cpp ../../source_cell/atom_pseudo.cpp ../../source_cell/pseudo.cpp ) diff --git a/source/source_estate/test/elecstate_energy_test.cpp b/source/source_estate/test/elecstate_energy_test.cpp index 2c112c8a862..d59e0cce917 100644 --- a/source/source_estate/test/elecstate_energy_test.cpp +++ b/source/source_estate/test/elecstate_energy_test.cpp @@ -157,6 +157,10 @@ TEST_F(ElecStateEnergyTest, AddsPccEnergyOnceAndKeepsItSeparateFromSolvation) elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.25); EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.95); + assume_isolated = "pcc_2d"; + elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); + EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.25); + EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.95); assume_isolated = "none"; elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.0); diff --git a/source/source_estate/test/test_pot_pcc.cpp b/source/source_estate/test/test_pot_pcc.cpp index 1c44ca8ab89..a2a10f74978 100644 --- a/source/source_estate/test/test_pot_pcc.cpp +++ b/source/source_estate/test/test_pot_pcc.cpp @@ -150,3 +150,54 @@ TEST_F(PotPccTest, RejectsForceFromAnOlderGeometry) ModuleBase::matrix force(1, 3); EXPECT_EXIT(correction.add_force(cell, force), testing::ExitedWithCode(1), ""); } + +TEST_F(PotPccTest, SlabAxisControlsPotentialEnergyForceAndIgnoresInPlaneCoordinates) +{ + const double expected_energy = 4.0 * ModuleBase::PI * 0.25 / 1000.0; + const double expected_force = 8.0 * ModuleBase::PI * 0.5 / 1000.0; + for (int axis = 0; axis < 3; ++axis) + { + elecstate::PotPcc correction(&basis, elecstate::PotPcc::Dimension::slab, axis); + ModuleBase::matrix potential(1, 8); + correction.cal_v_eff(&charge, &cell, potential); + EXPECT_NEAR(correction.get_energy(), expected_energy, 1.0e-14); + EXPECT_NEAR(potential(0, 7), expected_energy, 1.0e-14); + ModuleBase::matrix force(1, 3); + correction.add_force(cell, force); + for (int component = 0; component < 3; ++component) + { + const double expected = (component == axis) ? expected_force : 0.0; + EXPECT_NEAR(force(0, component), expected, 1.0e-14); + } + } +} + +TEST_F(PotPccTest, SlabRejectsOpenDirectionKpointsAndSymmetryButAllowsInPlaneTranslations) +{ + const std::vector> points = {ModuleBase::Vector3(0.25, 0.0, 0.0)}; + elecstate::PotPcc::validate_kpoints(points, 1, 2); + EXPECT_EXIT(elecstate::PotPcc::validate_kpoints(points, 1, 0), testing::ExitedWithCode(1), ""); + elecstate::PotPcc correction(&basis, elecstate::PotPcc::Dimension::slab, 2); + cell.symm.ptrans = {ModuleBase::Vector3(0.5, 0.0, 0.0)}; + ModuleBase::matrix potential(1, 8); + correction.cal_v_eff(&charge, &cell, potential); + cell.symm.ptrans[0].z = 0.5; + EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); + cell.symm.ptrans.clear(); + cell.symm.nrotk = 1; + cell.symm.gmatrix[0] = ModuleBase::Matrix3(0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, + 1.0, 0.0, 0.0); + EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); +} + +TEST_F(PotPccTest, RejectsMissingDensityThroughStandardErrorHandler) +{ + elecstate::PotPcc correction(&basis); + ModuleBase::matrix potential(1, 8); + charge.rho = nullptr; + EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); + charge.rho = channels; + channels[0] = nullptr; + EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); +} diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index 5ce20d24ba5..16f8eab42c2 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -35,7 +35,8 @@ struct Input_para int kpar = 1; ///< ecch pool is for one k point int bndpar = 1; ///< parallel for stochastic/deterministic bands std::string latname = "user_defined_lattice"; ///< lattice name - std::string assume_isolated = "none"; ///< isolated-system correction: none or makov-payne + std::string assume_isolated = "none"; ///< isolated-system correction + int pcc_2d_axis = 2; ///< open lattice vector for PCC 2D double ecutwfc = 0; ///< energy cutoff for wavefunctions double ecutrho = 0; ///< energy cutoff for charge/potential diff --git a/source/source_io/module_parameter/read_inp_sys.cpp b/source/source_io/module_parameter/read_inp_sys.cpp index c37383a4177..14459c07e81 100644 --- a/source/source_io/module_parameter/read_inp_sys.cpp +++ b/source/source_io/module_parameter/read_inp_sys.cpp @@ -507,6 +507,7 @@ Available options are: Available options are: * none: regular periodic calculation without isolated-system correction. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). +* pcc_2d: self-consistent vacuum point-countercharge correction for a slab, including potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. The open lattice vector is selected by `pcc_2d_axis` and must be perpendicular to the periodic plane; the two periodic vectors need not be orthogonal. K-point sampling along the open vector must be Gamma only. Symmetry must preserve that vector and cannot fractionally translate the slab along it. Moments use the mass-weighted ionic center along the normal; the half-cell wrapping plane must lie in vacuum. The charged-slab gauge is the open planar kernel zero on the source plane minus the zero-mean periodic kernel, with constant -pi*L/(3*A). The CPU, solver and incompatibility restrictions are the same as pcc_0d. * pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014).)"; @@ -519,7 +520,7 @@ Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreuss } }; item.check_value = [](const Input_Item& item, const Parameter& para) { - const std::vector allowed = {"none", "makov-payne", "m-p", "mp", "pcc_0d"}; + const std::vector allowed = {"none", "makov-payne", "m-p", "mp", "pcc_0d", "pcc_2d"}; if (std::find(allowed.begin(), allowed.end(), para.input.assume_isolated) == allowed.end()) { ModuleBase::WARNING_QUIT("ReadInput", nofound_str(allowed, "assume_isolated")); @@ -532,15 +533,46 @@ Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreuss "Makov-Payne correction is available only for cubic lattices: latname = sc, fcc, or bcc."); } const Input_para& inp = para.input; - if (inp.assume_isolated == "pcc_0d" - && (inp.imp_sol || inp.nspin == 4 || inp.device == "gpu" || inp.esolver_type != "ksdft" - || (inp.basis_type != "pw" && inp.basis_type != "lcao") - || (inp.calculation != "scf" && inp.calculation != "relax") - || inp.efield_flag || inp.gate_flag || inp.cal_stress || inp.dfthalf_type != 0 - || inp.deepks_scf || inp.deepks_out_labels || inp.deepks_bandgap || inp.deepks_v_delta - || inp.dm_to_rho)) - { - ModuleBase::WARNING_QUIT("ReadInput", "PCC 0D requires CPU KS-DFT PW/LCAO scf or fixed-cell relax, nspin=1/2, without solvent, stress or other fields"); + if (inp.assume_isolated == "pcc_0d" || inp.assume_isolated == "pcc_2d") + { + std::string error; + if (inp.imp_sol) { error = "cannot be combined with imp_sol"; } + else if (inp.nspin != 1 && inp.nspin != 2) { error = "requires nspin 1 or 2"; } + else if (inp.device != "cpu") { error = "requires device cpu"; } + else if (inp.esolver_type != "ksdft") { error = "requires esolver_type ksdft"; } + else if (inp.basis_type != "pw" && inp.basis_type != "lcao") { error = "requires basis_type pw or lcao"; } + else if (inp.calculation != "scf" && inp.calculation != "relax") { error = "requires calculation scf or fixed-cell relax"; } + else if (inp.efield_flag) { error = "cannot be combined with efield_flag"; } + else if (inp.gate_flag) { error = "cannot be combined with gate_flag"; } + else if (inp.cal_stress) { error = "does not support cal_stress"; } + else if (inp.dfthalf_type != 0) { error = "cannot be combined with dfthalf_type"; } + else if (inp.deepks_scf || inp.deepks_out_labels || inp.deepks_bandgap || inp.deepks_v_delta) + { + error = "cannot be combined with DeePKS correction or output"; + } + else if (inp.dm_to_rho) { error = "cannot be combined with dm_to_rho"; } + if (!error.empty()) + { + const std::string message = inp.assume_isolated + " " + error; + ModuleBase::WARNING_QUIT("ReadInput", message); + } + } + }; + this->add_item(item); + } + { + Input_Item item("pcc_2d_axis"); + item.annotation = "open lattice vector for vacuum PCC 2D"; + item.category = "System variables"; + item.type = "Integer"; + item.description = "Index of the open lattice vector for assume_isolated=pcc_2d: 0, 1 or 2 select the first, second or third lattice vector. This vector must be perpendicular to the periodic plane, with Gamma-only k-point sampling along it. Symmetry operations must preserve the open axis and cannot fractionally translate the slab along it."; + item.default_value = "2"; + item.set_availability("assume_isolated==pcc_2d"); + read_sync_int(input.pcc_2d_axis); + item.check_value = [](const Input_Item&, const Parameter& para) { + if (para.input.pcc_2d_axis < 0 || para.input.pcc_2d_axis > 2) + { + ModuleBase::WARNING_QUIT("ReadInput", "pcc_2d_axis must be 0, 1 or 2"); } }; this->add_item(item); diff --git a/source/source_io/test_serial/read_input_item_test.cpp b/source/source_io/test_serial/read_input_item_test.cpp index 49b7ca4bbba..6f226a31e5d 100644 --- a/source/source_io/test_serial/read_input_item_test.cpp +++ b/source/source_io/test_serial/read_input_item_test.cpp @@ -134,6 +134,17 @@ TEST_F(InputTest, Pcc0dSelectionAndUnsupportedCombinations) EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); input = valid; input.assume_isolated = "pcc_2d"; + it->second.check_value(it->second, param); + auto axis = find_label("pcc_2d_axis", readinput.input_lists); + ASSERT_NE(axis, readinput.input_lists.end()); + for (int value = 0; value < 3; ++value) + { + input.pcc_2d_axis = value; + axis->second.check_value(axis->second, param); + } + input.pcc_2d_axis = 3; + EXPECT_EXIT(axis->second.check_value(axis->second, param), testing::ExitedWithCode(1), ""); + input.assume_isolated = "pcc_invalid"; EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); } diff --git a/source/source_lcao/hamilt_lcao.cpp b/source/source_lcao/hamilt_lcao.cpp index a12fa0ce528..6e07150bda1 100644 --- a/source/source_lcao/hamilt_lcao.cpp +++ b/source/source_lcao/hamilt_lcao.cpp @@ -7,6 +7,7 @@ #include "source_lcao/setup_deepks.h" #include "source_estate/module_dm/density_matrix.h" #include "source_estate/module_pot/potential_new.h" +#include "source_estate/module_pot/pot_pcc.h" #include "source_hamilt/module_hcontainer/hcontainer_funcs.h" #include #ifdef __MLALGO @@ -88,6 +89,12 @@ HamiltLCAO::HamiltLCAO(const UnitCell& ucell, pot_register_in.push_back("xc"); if (inp.imp_sol) { pot_register_in.push_back("surchem"); } if (inp.assume_isolated == "pcc_0d") { pot_register_in.push_back("pcc"); } + if (inp.assume_isolated == "pcc_2d") + { + const int count = this->kv->get_nks(); + elecstate::PotPcc::validate_kpoints(this->kv->kvec_d, count, inp.pcc_2d_axis); + pot_register_in.push_back("pcc_2d"); + } if (inp.efield_flag) { pot_register_in.push_back("efield"); } if (inp.gate_flag) { pot_register_in.push_back("gatefield"); } if (inp.esolver_type == "tddft") { pot_register_in.push_back("tddft"); } diff --git a/source/source_pw/module_pwdft/hamilt_pw.cpp b/source/source_pw/module_pwdft/hamilt_pw.cpp index 7b72627768b..13977cd9f6e 100644 --- a/source/source_pw/module_pwdft/hamilt_pw.cpp +++ b/source/source_pw/module_pwdft/hamilt_pw.cpp @@ -13,6 +13,7 @@ #include "source_base/parallel_reduce.h" #include "source_hamilt/module_xc/general_exx_info.h" // for General_Exx_Info type #include "source_io/module_parameter/parameter.h" +#include "source_estate/module_pot/pot_pcc.h" namespace hamilt { @@ -68,20 +69,20 @@ HamiltPW::HamiltPW(elecstate::Potential* pot_in, } } } - if (PARAM.inp.vl_in_h) + const Input_para& input = PARAM.inp; + if (input.vl_in_h) { std::vector pot_register_in; - if (PARAM.inp.vion_in_h) + if (input.vion_in_h) { pot_register_in.push_back("local"); } - if (PARAM.inp.vh_in_h) + if (input.vh_in_h) { pot_register_in.push_back("hartree"); } // no variable can choose xc, maybe it is necessary pot_register_in.push_back("xc"); - const Input_para& input = PARAM.inp; if (input.imp_sol) { pot_register_in.push_back("surchem"); @@ -90,11 +91,17 @@ HamiltPW::HamiltPW(elecstate::Potential* pot_in, { pot_register_in.push_back("pcc"); } - if (PARAM.inp.efield_flag) + if (input.assume_isolated == "pcc_2d") + { + const int count = pkv->get_nks(); + elecstate::PotPcc::validate_kpoints(pkv->kvec_d, count, input.pcc_2d_axis); + pot_register_in.push_back("pcc_2d"); + } + if (input.efield_flag) { pot_register_in.push_back("efield"); } - if (PARAM.inp.gate_flag) + if (input.gate_flag) { pot_register_in.push_back("gatefield"); } diff --git a/tests/integrate/tools/catch_properties.sh b/tests/integrate/tools/catch_properties.sh index 4bf8a375c01..68fadf7a147 100755 --- a/tests/integrate/tools/catch_properties.sh +++ b/tests/integrate/tools/catch_properties.sh @@ -807,7 +807,7 @@ fi #-------------------------------------------- # vacuum point-countercharge correction #-------------------------------------------- -if [[ "$assume_isolated" == "pcc_0d" ]]; then +if [[ "$assume_isolated" == "pcc_0d" || "$assume_isolated" == "pcc_2d" ]]; then epcc=$(grep E_pcc "$running_path" | tail -1 | awk '{print $3}') echo "epccref $epcc" >> "$1" fi From e9176dd00e90e246983eddef793dd55ca955557c Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 11:49:27 -0600 Subject: [PATCH 09/41] test(pcc): add focused charged PW and neutral LCAO slab regressions Add only two small vacuum PCC 2D integration cases: charged H3O+ PW, open axis 2, and neutral water LCAO, open axis 1. Generate references with two MPI ranks and one OpenMP thread. Keep all old references and thresholds unchanged. Verification with OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1: - PW Autotest.sh -n 2 -o 1 -j 2 selecting both PCC0D/PCC2D H3O+: passed. - LCAO Autotest.sh -n 2 -o 1 -j 2 selecting both PCC0D/PCC2D water: passed. - New tests compare total energy, force magnitude and E_pcc. - Staged governance and git diff --cached --check: passed. - Two-rank runtime rejection of tilted open vector and non-Gamma open sampling passed; kpar=2 invalid sampling also exits 1 without hanging. Only calculation inputs and registration changed; no rebuild needed. --- tests/01_PW/222_PW_PCC2D_H3O/INPUT | 22 ++++++++++++++++ tests/01_PW/222_PW_PCC2D_H3O/KPT | 4 +++ tests/01_PW/222_PW_PCC2D_H3O/README.md | 6 +++++ tests/01_PW/222_PW_PCC2D_H3O/STRU | 26 +++++++++++++++++++ tests/01_PW/222_PW_PCC2D_H3O/result.ref | 5 ++++ tests/01_PW/CASES_CPU.txt | 1 + tests/02_NAO_Gamma/CASES_CPU.txt | 1 + tests/02_NAO_Gamma/scf_pcc2d_water/INPUT | 23 ++++++++++++++++ tests/02_NAO_Gamma/scf_pcc2d_water/KPT | 4 +++ tests/02_NAO_Gamma/scf_pcc2d_water/README.md | 6 +++++ tests/02_NAO_Gamma/scf_pcc2d_water/STRU | 24 +++++++++++++++++ tests/02_NAO_Gamma/scf_pcc2d_water/result.ref | 5 ++++ 12 files changed, 127 insertions(+) create mode 100644 tests/01_PW/222_PW_PCC2D_H3O/INPUT create mode 100644 tests/01_PW/222_PW_PCC2D_H3O/KPT create mode 100644 tests/01_PW/222_PW_PCC2D_H3O/README.md create mode 100644 tests/01_PW/222_PW_PCC2D_H3O/STRU create mode 100644 tests/01_PW/222_PW_PCC2D_H3O/result.ref create mode 100644 tests/02_NAO_Gamma/scf_pcc2d_water/INPUT create mode 100644 tests/02_NAO_Gamma/scf_pcc2d_water/KPT create mode 100644 tests/02_NAO_Gamma/scf_pcc2d_water/README.md create mode 100644 tests/02_NAO_Gamma/scf_pcc2d_water/STRU create mode 100644 tests/02_NAO_Gamma/scf_pcc2d_water/result.ref diff --git a/tests/01_PW/222_PW_PCC2D_H3O/INPUT b/tests/01_PW/222_PW_PCC2D_H3O/INPUT new file mode 100644 index 00000000000..06ed5f9b9d0 --- /dev/null +++ b/tests/01_PW/222_PW_PCC2D_H3O/INPUT @@ -0,0 +1,22 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type pw +ks_solver cg +pseudo_dir ../../PP_ORB +ntype 2 +nbands 8 +ecutwfc 40 +ecutrho 160 +scf_thr 1e-10 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 1 +init_wfc atomic +imp_sol 0 +assume_isolated pcc_2d +nelec_delta -1 +pcc_2d_axis 2 diff --git a/tests/01_PW/222_PW_PCC2D_H3O/KPT b/tests/01_PW/222_PW_PCC2D_H3O/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/01_PW/222_PW_PCC2D_H3O/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/01_PW/222_PW_PCC2D_H3O/README.md b/tests/01_PW/222_PW_PCC2D_H3O/README.md new file mode 100644 index 00000000000..7924bf4081c --- /dev/null +++ b/tests/01_PW/222_PW_PCC2D_H3O/README.md @@ -0,0 +1,6 @@ +Standalone vacuum PCC 2D regression. Compare total energy, ionic forces +and E_pcc against a two-rank reference with one OpenMP thread. The PW case +checks a charged periodic layer with open axis 2; the LCAO case checks a +neutral water layer with open axis 1. Analytic sheet/dipole, normal-force and +electronic-potential derivative tests provide independent kernel references. +The saved original PCC executable was also used for numerical comparison. diff --git a/tests/01_PW/222_PW_PCC2D_H3O/STRU b/tests/01_PW/222_PW_PCC2D_H3O/STRU new file mode 100644 index 00000000000..75b5d3b13e0 --- /dev/null +++ b/tests/01_PW/222_PW_PCC2D_H3O/STRU @@ -0,0 +1,26 @@ +ATOMIC_SPECIES +O 15.9994 O_ONCV_PBE-1.0.upf +H 1.008 H_ONCV_PBE-1.0.upf + +LATTICE_CONSTANT +1.0 + +LATTICE_VECTORS +20.0 0.0 0.0 +0.0 20.0 0.0 +0.0 0.0 20.0 + +ATOMIC_POSITIONS +Cartesian + +O +0.0 +1 +10.0000000000 10.0000000000 10.0000000000 0 0 0 + +H +0.0 +3 +11.8077500000 9.6969000000 10.2918300000 0 0 0 +8.9439100000 8.5571900000 10.4978700000 0 0 0 +9.6429200000 10.5858300000 8.2760700000 0 0 0 diff --git a/tests/01_PW/222_PW_PCC2D_H3O/result.ref b/tests/01_PW/222_PW_PCC2D_H3O/result.ref new file mode 100644 index 00000000000..ac11b6d7ceb --- /dev/null +++ b/tests/01_PW/222_PW_PCC2D_H3O/result.ref @@ -0,0 +1,5 @@ +etotref -473.2841989053661678 +etotperatomref -118.3210497263 +totalforceref 3.084479 +epccref -0.6473709495 +totaltimeref 11.78 diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index 1df4d1fc743..324309c3fb2 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -139,3 +139,4 @@ scf_out_chg_tau scf_deltaspin2 scf_deltaspin4 221_PW_PCC0D_H3O +222_PW_PCC2D_H3O diff --git a/tests/02_NAO_Gamma/CASES_CPU.txt b/tests/02_NAO_Gamma/CASES_CPU.txt index 6ec0573256b..9ba3fa93de9 100644 --- a/tests/02_NAO_Gamma/CASES_CPU.txt +++ b/tests/02_NAO_Gamma/CASES_CPU.txt @@ -39,3 +39,4 @@ scf_u_spin2_old scf_upf100 test.sum scf_pcc0d_water +scf_pcc2d_water diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT b/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT new file mode 100644 index 00000000000..3a38dee53d1 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT @@ -0,0 +1,23 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type lcao +ks_solver genelpa +pseudo_dir ../../PP_ORB +ntype 2 +nbands 8 +ecutwfc 40 +ecutrho 160 +scf_thr 1e-10 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 1 +init_wfc atomic +imp_sol 0 +assume_isolated pcc_2d +orbital_dir ../../PP_ORB +gamma_only 1 +pcc_2d_axis 1 diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/KPT b/tests/02_NAO_Gamma/scf_pcc2d_water/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/README.md b/tests/02_NAO_Gamma/scf_pcc2d_water/README.md new file mode 100644 index 00000000000..7924bf4081c --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/README.md @@ -0,0 +1,6 @@ +Standalone vacuum PCC 2D regression. Compare total energy, ionic forces +and E_pcc against a two-rank reference with one OpenMP thread. The PW case +checks a charged periodic layer with open axis 2; the LCAO case checks a +neutral water layer with open axis 1. Analytic sheet/dipole, normal-force and +electronic-potential derivative tests provide independent kernel references. +The saved original PCC executable was also used for numerical comparison. diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/STRU b/tests/02_NAO_Gamma/scf_pcc2d_water/STRU new file mode 100644 index 00000000000..05933dd2b32 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/STRU @@ -0,0 +1,24 @@ +ATOMIC_SPECIES +O 15.999 O_ONCV_PBE-1.0.upf +H 1.008 H_ONCV_PBE-1.0.upf +NUMERICAL_ORBITAL +O_gga_7au_100Ry_2s2p1d.orb +H_gga_8au_100Ry_2s1p.orb + +LATTICE_CONSTANT +1.8897261258369282 +LATTICE_VECTORS +12.0 0 0 +0 12.0 0 +0 0 12.0 +ATOMIC_POSITIONS +Cartesian +O +0 +1 +6.0 6.0 6.0 1 1 1 +H +0 +2 +6.75716000000000 6.58626 6.0 1 1 1 +5.24284 6.58626 6.0 1 1 1 diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/result.ref b/tests/02_NAO_Gamma/scf_pcc2d_water/result.ref new file mode 100644 index 00000000000..30c7e2cc981 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/result.ref @@ -0,0 +1,5 @@ +etotref -466.4749036749371 +etotperatomref -155.4916345583 +totalforceref 3.785586 +epccref 0.0097392947 +totaltimeref 10.24 From d0ebbc1a051325f5e09959e1a955e83b0d795f1a Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 15:37:16 -0600 Subject: [PATCH 10/41] docs(pcc): restore original INPUT wording without solvent references Restore assume_isolated and pcc_2d_axis descriptions from abacus_sccs, removing only solvent clauses. Regenerate parameter YAML and input-main. Remove the PCC solvent-combination test, solvent wording in the added energy test name/readmes, and explicit imp_sol=0 from four PCC cases. Keep the INPUT mutual-exclusion check as requested; physics is unchanged. Verification: - Full executable, INPUT test and energy test rebuilt successfully. - Both affected CTest targets passed in 0.18 s with OMP_NUM_THREADS=1. - CLI parameter help and --check-input passed for all four PCC cases. - Script comparison confirms exact original descriptions except the removed solvent clauses and confirms the mutual-exclusion check remains. - Generated docs contain 567 parameters. - Staged governance and git diff --cached --check passed. --- docs/advanced/input_files/input-main.md | 8 +++++--- docs/parameters.yaml | 8 +++++--- source/source_estate/test/elecstate_energy_test.cpp | 2 +- source/source_io/module_parameter/read_inp_sys.cpp | 10 ++++++---- source/source_io/test_serial/read_input_item_test.cpp | 5 +---- tests/01_PW/221_PW_PCC0D_H3O/INPUT | 1 - tests/01_PW/221_PW_PCC0D_H3O/README.md | 2 +- tests/01_PW/222_PW_PCC2D_H3O/INPUT | 1 - tests/02_NAO_Gamma/scf_pcc0d_water/INPUT | 1 - tests/02_NAO_Gamma/scf_pcc0d_water/README.md | 2 +- tests/02_NAO_Gamma/scf_pcc2d_water/INPUT | 1 - 11 files changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 455ccd133cb..8a2f5813a3e 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -768,9 +768,11 @@ Available options are: - none: regular periodic calculation without isolated-system correction. + - pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. The cell must be primitive: with symmetry 1, an analyzed fractional translation stops the run. + - pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The multipoles are taken about the mass-weighted ionic center along the normal, and the correction is cut half a cell length from that center, so this plane must lie in the vacuum. With symmetry 1, every analyzed operation, including the primitive-cell translations used to symmetrize the density, must map the open vector onto plus or minus itself; an operation that keeps its direction must not translate by a fraction of it, which would copy the slab within the cell, whereas a mirror normal to it may. Otherwise the run stops; use a primitive cell with vacuum only along the open vector, or symmetry 0 or -1. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. - makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). - - pcc_2d: self-consistent vacuum point-countercharge correction for a slab, including potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. The open lattice vector is selected by `pcc_2d_axis` and must be perpendicular to the periodic plane; the two periodic vectors need not be orthogonal. K-point sampling along the open vector must be Gamma only. Symmetry must preserve that vector and cannot fractionally translate the slab along it. Moments use the mass-weighted ionic center along the normal; the half-cell wrapping plane must lie in vacuum. The charged-slab gauge is the open planar kernel zero on the source plane minus the zero-mean periodic kernel, with constant -pi*L/(3*A). The CPU, solver and incompatibility restrictions are the same as pcc_0d. - - pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. + + pcc_0d and pcc_2d contribute to the energy, the potential and the fixed-cell forces; the correction energy is printed as E_pcc. They require CPU KS-DFT (esolver_type ksdft) with basis_type pw or lcao, calculation scf or relax, and nspin 1 or 2, without efield_flag, gate_flag, cal_stress, DFT-1/2, deepks output or dm_to_rho. Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014). - **Default**: none @@ -779,7 +781,7 @@ - **Type**: Integer - **Availability**: *[`assume_isolated`](#assume_isolated)==pcc_2d* -- **Description**: Index of the open lattice vector for assume_isolated=pcc_2d: 0, 1 or 2 select the first, second or third lattice vector. This vector must be perpendicular to the periodic plane, with Gamma-only k-point sampling along it. Symmetry operations must preserve the open axis and cannot fractionally translate the slab along it. +- **Description**: Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only, and symmetry operations must keep it (see assume_isolated). For LCAO the real-space grid integration is distributed over planes along the third lattice vector, so an open axis of 0 or 1 keeps the vacuum out of that distribution and balances the load better. - **Default**: 2 ### init_wfc diff --git a/docs/parameters.yaml b/docs/parameters.yaml index ae528e51714..db9f18a5e0c 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -178,9 +178,11 @@ parameters: Available options are: * none: regular periodic calculation without isolated-system correction. + * pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. The cell must be primitive: with symmetry 1, an analyzed fractional translation stops the run. + * pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The multipoles are taken about the mass-weighted ionic center along the normal, and the correction is cut half a cell length from that center, so this plane must lie in the vacuum. With symmetry 1, every analyzed operation, including the primitive-cell translations used to symmetrize the density, must map the open vector onto plus or minus itself; an operation that keeps its direction must not translate by a fraction of it, which would copy the slab within the cell, whereas a mirror normal to it may. Otherwise the run stops; use a primitive cell with vacuum only along the open vector, or symmetry 0 or -1. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). - * pcc_2d: self-consistent vacuum point-countercharge correction for a slab, including potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. The open lattice vector is selected by `pcc_2d_axis` and must be perpendicular to the periodic plane; the two periodic vectors need not be orthogonal. K-point sampling along the open vector must be Gamma only. Symmetry must preserve that vector and cannot fractionally translate the slab along it. Moments use the mass-weighted ionic center along the normal; the half-cell wrapping plane must lie in vacuum. The charged-slab gauge is the open planar kernel zero on the source plane minus the zero-mean periodic kernel, with constant -pi*L/(3*A). The CPU, solver and incompatibility restrictions are the same as pcc_0d. - * pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. + + pcc_0d and pcc_2d contribute to the energy, the potential and the fixed-cell forces; the correction energy is printed as E_pcc. They require CPU KS-DFT (esolver_type ksdft) with basis_type pw or lcao, calculation scf or relax, and nspin 1 or 2, without efield_flag, gate_flag, cal_stress, DFT-1/2, deepks output or dm_to_rho. Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014). default_value: none @@ -190,7 +192,7 @@ parameters: category: System variables type: Integer description: | - Index of the open lattice vector for assume_isolated=pcc_2d: 0, 1 or 2 select the first, second or third lattice vector. This vector must be perpendicular to the periodic plane, with Gamma-only k-point sampling along it. Symmetry operations must preserve the open axis and cannot fractionally translate the slab along it. + Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only, and symmetry operations must keep it (see assume_isolated). For LCAO the real-space grid integration is distributed over planes along the third lattice vector, so an open axis of 0 or 1 keeps the vacuum out of that distribution and balances the load better. default_value: "2" unit: "" availability: assume_isolated==pcc_2d diff --git a/source/source_estate/test/elecstate_energy_test.cpp b/source/source_estate/test/elecstate_energy_test.cpp index d59e0cce917..86f1334d7c3 100644 --- a/source/source_estate/test/elecstate_energy_test.cpp +++ b/source/source_estate/test/elecstate_energy_test.cpp @@ -150,7 +150,7 @@ TEST_F(ElecStateEnergyTest, CalEnergiesEtotDFTU) EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 1.3); } -TEST_F(ElecStateEnergyTest, AddsPccEnergyOnceAndKeepsItSeparateFromSolvation) +TEST_F(ElecStateEnergyTest, AddsPccEnergyOnce) { elecstate->f_en.deband = 0.1; assume_isolated = "pcc_0d"; diff --git a/source/source_io/module_parameter/read_inp_sys.cpp b/source/source_io/module_parameter/read_inp_sys.cpp index 14459c07e81..856eaecd7ff 100644 --- a/source/source_io/module_parameter/read_inp_sys.cpp +++ b/source/source_io/module_parameter/read_inp_sys.cpp @@ -506,9 +506,11 @@ Available options are: Available options are: * none: regular periodic calculation without isolated-system correction. +* pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. The cell must be primitive: with symmetry 1, an analyzed fractional translation stops the run. +* pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The multipoles are taken about the mass-weighted ionic center along the normal, and the correction is cut half a cell length from that center, so this plane must lie in the vacuum. With symmetry 1, every analyzed operation, including the primitive-cell translations used to symmetrize the density, must map the open vector onto plus or minus itself; an operation that keeps its direction must not translate by a fraction of it, which would copy the slab within the cell, whereas a mirror normal to it may. Otherwise the run stops; use a primitive cell with vacuum only along the open vector, or symmetry 0 or -1. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). -* pcc_2d: self-consistent vacuum point-countercharge correction for a slab, including potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. The open lattice vector is selected by `pcc_2d_axis` and must be perpendicular to the periodic plane; the two periodic vectors need not be orthogonal. K-point sampling along the open vector must be Gamma only. Symmetry must preserve that vector and cannot fractionally translate the slab along it. Moments use the mass-weighted ionic center along the normal; the half-cell wrapping plane must lie in vacuum. The charged-slab gauge is the open planar kernel zero on the source plane minus the zero-mean periodic kernel, with constant -pi*L/(3*A). The CPU, solver and incompatibility restrictions are the same as pcc_0d. -* pcc_0d: self-consistent point-counter-charge correction for a molecule in an orthogonal equal-edge cubic cell, including rotated cells. It contributes to the potential, energy (E_pcc), fixed-cell ionic forces and electrostatic potential output. Multipoles use the mass-weighted ionic center; the correction wraps coordinates half a cell from that center, so this boundary must lie in vacuum. With symmetry 1, fractional translations are rejected because the cell must be primitive. Available for CPU KS-DFT, PW or LCAO, scf or fixed-cell relax, nspin 1 or 2, without implicit solvent, stress, external fields, DFT-1/2, DeePKS output or dm_to_rho. + +pcc_0d and pcc_2d contribute to the energy, the potential and the fixed-cell forces; the correction energy is printed as E_pcc. They require CPU KS-DFT (esolver_type ksdft) with basis_type pw or lcao, calculation scf or relax, and nspin 1 or 2, without efield_flag, gate_flag, cal_stress, DFT-1/2, deepks output or dm_to_rho. Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014).)"; item.default_value = "none"; @@ -562,10 +564,10 @@ Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreuss } { Input_Item item("pcc_2d_axis"); - item.annotation = "open lattice vector for vacuum PCC 2D"; + item.annotation = "open lattice vector of assume_isolated pcc_2d"; item.category = "System variables"; item.type = "Integer"; - item.description = "Index of the open lattice vector for assume_isolated=pcc_2d: 0, 1 or 2 select the first, second or third lattice vector. This vector must be perpendicular to the periodic plane, with Gamma-only k-point sampling along it. Symmetry operations must preserve the open axis and cannot fractionally translate the slab along it."; + item.description = "Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only, and symmetry operations must keep it (see assume_isolated). For LCAO the real-space grid integration is distributed over planes along the third lattice vector, so an open axis of 0 or 1 keeps the vacuum out of that distribution and balances the load better."; item.default_value = "2"; item.set_availability("assume_isolated==pcc_2d"); read_sync_int(input.pcc_2d_axis); diff --git a/source/source_io/test_serial/read_input_item_test.cpp b/source/source_io/test_serial/read_input_item_test.cpp index 6f226a31e5d..4ace2f6f5a1 100644 --- a/source/source_io/test_serial/read_input_item_test.cpp +++ b/source/source_io/test_serial/read_input_item_test.cpp @@ -98,7 +98,7 @@ TEST_F(InputTest, RelaxMethod) EXPECT_EQ(find_label("relax_new", readinput.input_lists), readinput.input_lists.end()); } -TEST_F(InputTest, Pcc0dSelectionAndUnsupportedCombinations) +TEST_F(InputTest, PccSelectionAndUnsupportedCombinations) { ModuleIO::ReadInput readinput(0); Parameter param; @@ -118,9 +118,6 @@ TEST_F(InputTest, Pcc0dSelectionAndUnsupportedCombinations) it->second.check_value(it->second, param); const Input_para valid = input; - input.imp_sol = true; - EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); - input = valid; input.nspin = 4; EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); input = valid; diff --git a/tests/01_PW/221_PW_PCC0D_H3O/INPUT b/tests/01_PW/221_PW_PCC0D_H3O/INPUT index d84a6039c12..f5590d1ce7e 100644 --- a/tests/01_PW/221_PW_PCC0D_H3O/INPUT +++ b/tests/01_PW/221_PW_PCC0D_H3O/INPUT @@ -16,6 +16,5 @@ smearing_method fixed symmetry 0 cal_force 1 init_wfc atomic -imp_sol 0 assume_isolated pcc_0d nelec_delta -1 diff --git a/tests/01_PW/221_PW_PCC0D_H3O/README.md b/tests/01_PW/221_PW_PCC0D_H3O/README.md index 42f90a8fac1..218f62738eb 100644 --- a/tests/01_PW/221_PW_PCC0D_H3O/README.md +++ b/tests/01_PW/221_PW_PCC0D_H3O/README.md @@ -1,4 +1,4 @@ -Vacuum PCC 0D regression with `assume_isolated pcc_0d` and no solvent. +Vacuum PCC 0D regression with `assume_isolated pcc_0d`. The integration harness compares total energy, ionic force magnitudes and `E_pcc` (in eV). References were generated with two MPI ranks, one OpenMP thread and diff --git a/tests/01_PW/222_PW_PCC2D_H3O/INPUT b/tests/01_PW/222_PW_PCC2D_H3O/INPUT index 06ed5f9b9d0..0f580c8a4f4 100644 --- a/tests/01_PW/222_PW_PCC2D_H3O/INPUT +++ b/tests/01_PW/222_PW_PCC2D_H3O/INPUT @@ -16,7 +16,6 @@ smearing_method fixed symmetry 0 cal_force 1 init_wfc atomic -imp_sol 0 assume_isolated pcc_2d nelec_delta -1 pcc_2d_axis 2 diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT b/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT index d86e4840392..6ca856be5c2 100644 --- a/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT @@ -16,7 +16,6 @@ smearing_method fixed symmetry 0 cal_force 1 init_wfc atomic -imp_sol 0 assume_isolated pcc_0d orbital_dir ../../PP_ORB gamma_only 1 diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/README.md b/tests/02_NAO_Gamma/scf_pcc0d_water/README.md index 42f90a8fac1..218f62738eb 100644 --- a/tests/02_NAO_Gamma/scf_pcc0d_water/README.md +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/README.md @@ -1,4 +1,4 @@ -Vacuum PCC 0D regression with `assume_isolated pcc_0d` and no solvent. +Vacuum PCC 0D regression with `assume_isolated pcc_0d`. The integration harness compares total energy, ionic force magnitudes and `E_pcc` (in eV). References were generated with two MPI ranks, one OpenMP thread and diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT b/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT index 3a38dee53d1..f84f69c2278 100644 --- a/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT @@ -16,7 +16,6 @@ smearing_method fixed symmetry 0 cal_force 1 init_wfc atomic -imp_sol 0 assume_isolated pcc_2d orbital_dir ../../PP_ORB gamma_only 1 From d820103eb0f711857d13c7f4847dabb7c8417666 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 16:25:08 -0600 Subject: [PATCH 11/41] test: reduce PCC integration cutoffs and SCF cost --- tests/01_PW/221_PW_PCC0D_H3O/INPUT | 5 ++--- tests/01_PW/221_PW_PCC0D_H3O/result.ref | 10 +++++----- tests/01_PW/222_PW_PCC2D_H3O/INPUT | 5 ++--- tests/01_PW/222_PW_PCC2D_H3O/result.ref | 10 +++++----- tests/02_NAO_Gamma/scf_pcc0d_water/INPUT | 5 ++--- tests/02_NAO_Gamma/scf_pcc0d_water/result.ref | 10 +++++----- tests/02_NAO_Gamma/scf_pcc2d_water/INPUT | 5 ++--- tests/02_NAO_Gamma/scf_pcc2d_water/result.ref | 10 +++++----- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/tests/01_PW/221_PW_PCC0D_H3O/INPUT b/tests/01_PW/221_PW_PCC0D_H3O/INPUT index f5590d1ce7e..00d83ad0364 100644 --- a/tests/01_PW/221_PW_PCC0D_H3O/INPUT +++ b/tests/01_PW/221_PW_PCC0D_H3O/INPUT @@ -6,9 +6,8 @@ ks_solver cg pseudo_dir ../../PP_ORB ntype 2 nbands 8 -ecutwfc 40 -ecutrho 160 -scf_thr 1e-10 +ecutwfc 20 +scf_thr 1e-9 scf_nmax 200 mixing_type pulay mixing_beta 0.2 diff --git a/tests/01_PW/221_PW_PCC0D_H3O/result.ref b/tests/01_PW/221_PW_PCC0D_H3O/result.ref index 2ad5a347f6d..c2b35c6f8c4 100644 --- a/tests/01_PW/221_PW_PCC0D_H3O/result.ref +++ b/tests/01_PW/221_PW_PCC0D_H3O/result.ref @@ -1,5 +1,5 @@ -etotref -470.6438272229737549 -etotperatomref -117.6609568057 -totalforceref 3.084305 -epccref 1.9931032737 -totaltimeref 12.31 +etotref -448.4562759200063624 +etotperatomref -112.1140689800 +totalforceref 20.653424 +epccref 2.0044958312 +totaltimeref 1.80 diff --git a/tests/01_PW/222_PW_PCC2D_H3O/INPUT b/tests/01_PW/222_PW_PCC2D_H3O/INPUT index 0f580c8a4f4..b93c000c7a4 100644 --- a/tests/01_PW/222_PW_PCC2D_H3O/INPUT +++ b/tests/01_PW/222_PW_PCC2D_H3O/INPUT @@ -6,9 +6,8 @@ ks_solver cg pseudo_dir ../../PP_ORB ntype 2 nbands 8 -ecutwfc 40 -ecutrho 160 -scf_thr 1e-10 +ecutwfc 20 +scf_thr 1e-9 scf_nmax 200 mixing_type pulay mixing_beta 0.2 diff --git a/tests/01_PW/222_PW_PCC2D_H3O/result.ref b/tests/01_PW/222_PW_PCC2D_H3O/result.ref index ac11b6d7ceb..4be028f8eb1 100644 --- a/tests/01_PW/222_PW_PCC2D_H3O/result.ref +++ b/tests/01_PW/222_PW_PCC2D_H3O/result.ref @@ -1,5 +1,5 @@ -etotref -473.2841989053661678 -etotperatomref -118.3210497263 -totalforceref 3.084479 -epccref -0.6473709495 -totaltimeref 11.78 +etotref -451.0967403180009114 +etotperatomref -112.7741850795 +totalforceref 20.659971 +epccref -0.6360808366 +totaltimeref 1.91 diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT b/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT index 6ca856be5c2..600f548e27b 100644 --- a/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/INPUT @@ -6,9 +6,8 @@ ks_solver genelpa pseudo_dir ../../PP_ORB ntype 2 nbands 8 -ecutwfc 40 -ecutrho 160 -scf_thr 1e-10 +ecutwfc 20 +scf_thr 1e-7 scf_nmax 200 mixing_type pulay mixing_beta 0.2 diff --git a/tests/02_NAO_Gamma/scf_pcc0d_water/result.ref b/tests/02_NAO_Gamma/scf_pcc0d_water/result.ref index f15d83c334e..cb1d9c0a986 100644 --- a/tests/02_NAO_Gamma/scf_pcc0d_water/result.ref +++ b/tests/02_NAO_Gamma/scf_pcc0d_water/result.ref @@ -1,5 +1,5 @@ -etotref -466.4814265138713 -etotperatomref -155.4938088380 -totalforceref 3.801512 -epccref 0.0032764842 -totaltimeref 10.76 +etotref -466.5443393467753 +etotperatomref -155.5147797823 +totalforceref 4.003482 +epccref 0.0033441637 +totaltimeref 1.44 diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT b/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT index f84f69c2278..59ee882807c 100644 --- a/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/INPUT @@ -6,9 +6,8 @@ ks_solver genelpa pseudo_dir ../../PP_ORB ntype 2 nbands 8 -ecutwfc 40 -ecutrho 160 -scf_thr 1e-10 +ecutwfc 20 +scf_thr 1e-7 scf_nmax 200 mixing_type pulay mixing_beta 0.2 diff --git a/tests/02_NAO_Gamma/scf_pcc2d_water/result.ref b/tests/02_NAO_Gamma/scf_pcc2d_water/result.ref index 30c7e2cc981..11f6923b1f4 100644 --- a/tests/02_NAO_Gamma/scf_pcc2d_water/result.ref +++ b/tests/02_NAO_Gamma/scf_pcc2d_water/result.ref @@ -1,5 +1,5 @@ -etotref -466.4749036749371 -etotperatomref -155.4916345583 -totalforceref 3.785586 -epccref 0.0097392947 -totaltimeref 10.24 +etotref -466.5376820791657 +etotperatomref -155.5125606931 +totalforceref 3.987612 +epccref 0.0099396021 +totaltimeref 1.45 From 56c3745cbad7c4e36ea5f8c2adbf0438c5f826dd Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 17:12:40 -0600 Subject: [PATCH 12/41] feat: add SCCS cavity and preset parameter foundations --- source/CMakeLists.txt | 1 + source/Makefile.Objects | 5 + source/source_hamilt/CMakeLists.txt | 1 + .../source_hamilt/module_sccs/CMakeLists.txt | 9 ++ .../source_hamilt/module_sccs/sccs_cavity.cpp | 81 ++++++++++ .../source_hamilt/module_sccs/sccs_cavity.h | 33 ++++ .../module_sccs/sccs_parameters.cpp | 126 +++++++++++++++ .../module_sccs/sccs_parameters.h | 26 +++ .../module_sccs/test/CMakeLists.txt | 4 + .../module_sccs/test/test_sccs_cavity.cpp | 148 ++++++++++++++++++ .../module_sccs/test/test_sccs_parameters.cpp | 95 +++++++++++ 11 files changed, 529 insertions(+) create mode 100644 source/source_hamilt/module_sccs/CMakeLists.txt create mode 100644 source/source_hamilt/module_sccs/sccs_cavity.cpp create mode 100644 source/source_hamilt/module_sccs/sccs_cavity.h create mode 100644 source/source_hamilt/module_sccs/sccs_parameters.cpp create mode 100644 source/source_hamilt/module_sccs/sccs_parameters.h create mode 100644 source/source_hamilt/module_sccs/test/CMakeLists.txt create mode 100644 source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp create mode 100644 source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 41491ffff2e..31c730682c7 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -730,6 +730,7 @@ target_link_libraries( io_basic io_input surchem + sccs neighbor_search domain_decomposition neighbor diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 9845c44ad73..d5d8cb18db9 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -49,6 +49,7 @@ VPATH=./src_global:\ ./source_hamilt:\ ./source_hamilt/module_ewald:\ ./source_hamilt/module_surchem:\ +./source_hamilt/module_sccs:\ ./source_hamilt/module_xc:\ ./source_hamilt/module_xc/kernels:\ ./source_pw/module_proj:\ @@ -103,6 +104,7 @@ ${OBJS_PSI_INITIALIZER}\ ${OBJS_PW}\ ${OBJS_RELAXATION}\ ${OBJS_SURCHEM}\ +${OBJS_SCCS}\ ${OBJS_SYMMETRY}\ ${OBJS_XC}\ ${OBJS_IO}\ @@ -610,6 +612,9 @@ OBJS_SURCHEM=surchem.o\ minimize_cg.o\ sol_force.o\ +OBJS_SCCS=sccs_cavity.o\ + sccs_parameters.o + OBJS_SYMMETRY=symm_other.o\ symm_rot_spin.o\ symm_basic.o\ diff --git a/source/source_hamilt/CMakeLists.txt b/source/source_hamilt/CMakeLists.txt index 90fe2257c77..d0cdba7a1ef 100644 --- a/source/source_hamilt/CMakeLists.txt +++ b/source/source_hamilt/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(module_vdw) add_subdirectory(module_surchem) +add_subdirectory(module_sccs) add_subdirectory(module_xc) add_subdirectory(module_hcontainer) diff --git a/source/source_hamilt/module_sccs/CMakeLists.txt b/source/source_hamilt/module_sccs/CMakeLists.txt new file mode 100644 index 00000000000..49ee0926d51 --- /dev/null +++ b/source/source_hamilt/module_sccs/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(sccs OBJECT sccs_cavity.cpp sccs_parameters.cpp) + +if(ENABLE_COVERAGE) + add_coverage(sccs) +endif() + +if(BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/source/source_hamilt/module_sccs/sccs_cavity.cpp b/source/source_hamilt/module_sccs/sccs_cavity.cpp new file mode 100644 index 00000000000..ac9c1287d15 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_cavity.cpp @@ -0,0 +1,81 @@ +#include "sccs_cavity.h" +#include "source_base/constants.h" + +#include + +namespace ModuleSccs +{ +bool validate_cavity_parameters(const CavityParameters& parameters, std::string& error) +{ + error.clear(); + if (!std::isfinite(parameters.density_min) || !std::isfinite(parameters.density_max) + || parameters.density_min <= 0.0 || parameters.density_max <= parameters.density_min) + { + error = "SCCS requires finite density thresholds with 0 < density_min < density_max"; + return false; + } + if (!std::isfinite(parameters.epsilon_bulk) || parameters.epsilon_bulk < 1.0) + { + error = "SCCS requires a finite bulk dielectric constant >= 1"; + return false; + } + return true; +} + +bool evaluate_cavity(double density, + const CavityParameters& parameters, + CavityPoint& result, + std::string& error) +{ + if (!validate_cavity_parameters(parameters, error)) + { + return false; + } + if (!std::isfinite(density)) + { + error = "SCCS cavity requires a finite electronic density"; + return false; + } + + CavityPoint point; + if (density <= parameters.density_min) + { + point.epsilon = parameters.epsilon_bulk; + } + else if (density >= parameters.density_max) + { + point.solute = 1.0; + point.epsilon = 1.0; + } + else + { + const double density_ratio = parameters.density_max / parameters.density_min; + if (!std::isfinite(density_ratio)) + { + error = "SCCS cavity density ratio exceeds the numerical range"; + return false; + } + const double log_width = std::log(density_ratio); + const double relative_density = parameters.density_max / density; + const double log_relative_density = std::log(relative_density); + const double x = log_relative_density / log_width; + const double angle = ModuleBase::TWO_PI * x; + const double solvent = x - std::sin(angle) / ModuleBase::TWO_PI; + const double dsolvent_drho = -(1.0 - std::cos(angle)) / (log_width * density); + const double log_epsilon = std::log(parameters.epsilon_bulk); + const double log_epsilon_point = log_epsilon * solvent; + point.solute = 1.0 - solvent; + point.dsolute_drho = -dsolvent_drho; + point.epsilon = std::exp(log_epsilon_point); + point.depsilon_drho = point.epsilon * log_epsilon * dsolvent_drho; + if (!std::isfinite(point.solute) || !std::isfinite(point.dsolute_drho) + || !std::isfinite(point.epsilon) || !std::isfinite(point.depsilon_drho)) + { + error = "SCCS cavity transition exceeds the numerical range"; + return false; + } + } + result = point; + return true; +} +} // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_cavity.h b/source/source_hamilt/module_sccs/sccs_cavity.h new file mode 100644 index 00000000000..bca86d19993 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_cavity.h @@ -0,0 +1,33 @@ +#ifndef SCCS_CAVITY_H +#define SCCS_CAVITY_H + +#include + +namespace ModuleSccs +{ +// Densities are in electrons/Bohr^3. The cavity uses the electronic density. +struct CavityParameters +{ + double density_min = 0.0; + double density_max = 0.0; + double epsilon_bulk = 0.0; +}; + +struct CavityPoint +{ + double solute = 0.0; + double dsolute_drho = 0.0; + double epsilon = 0.0; + double depsilon_drho = 0.0; +}; + +bool validate_cavity_parameters(const CavityParameters& parameters, std::string& error); + +// Failure leaves result unchanged. Negative Fourier ringing uses the bulk limit. +bool evaluate_cavity(double density, + const CavityParameters& parameters, + CavityPoint& result, + std::string& error); +} // namespace ModuleSccs + +#endif diff --git a/source/source_hamilt/module_sccs/sccs_parameters.cpp b/source/source_hamilt/module_sccs/sccs_parameters.cpp new file mode 100644 index 00000000000..8471bf75a8d --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_parameters.cpp @@ -0,0 +1,126 @@ +#include "sccs_parameters.h" + +#include +#include + +namespace ModuleSccs +{ +namespace +{ +// CODATA 2018 SI values retain the original SCCS preset conversion. +// ModuleBase's older SI values differ, so using them would change these presets. +const double hartree_joule = 4.3597447222071e-18; +const double bohr_metre = 5.29177210903e-11; +} + +double dyn_per_cm_to_hartree_per_bohr2(double value) +{ + const double joule_per_square_metre = value * 1e-3; + return joule_per_square_metre * bohr_metre * bohr_metre / hartree_joule; +} + +double gpa_to_hartree_per_bohr3(double value) +{ + const double pascal = value * 1e9; + return pascal * bohr_metre * bohr_metre * bohr_metre / hartree_joule; +} + +bool parse_preset(const std::string& name, Preset& preset, std::string& error) +{ + error.clear(); + std::string normalized = name; + for (char& character : normalized) + { + const unsigned char byte = static_cast(character); + const int lower = std::tolower(byte); + character = static_cast(lower); + } + if (normalized == "custom") + { + preset = Preset::Custom; + } + else if (normalized == "vacuum") + { + preset = Preset::Vacuum; + } + else if (normalized == "water-neutral") + { + preset = Preset::WaterNeutral; + } + else if (normalized == "water-cation") + { + preset = Preset::WaterCation; + } + else if (normalized == "water-anion") + { + preset = Preset::WaterAnion; + } + else + { + error = "Unknown SCCS preset: " + name; + return false; + } + return true; +} + +bool make_sccs_config(Preset preset, SccsConfig& config, std::string& error) +{ + error.clear(); + SccsConfig candidate; + candidate.cavity.epsilon_bulk = 78.3; + candidate.surface_regularization = 1e-8; + if (preset == Preset::Vacuum || preset == Preset::WaterNeutral) + { + candidate.cavity.density_min = 1e-4; + candidate.cavity.density_max = 5e-3; + if (preset == Preset::Vacuum) + { + candidate.cavity.epsilon_bulk = 1.0; + } + else + { + candidate.surface_tension = dyn_per_cm_to_hartree_per_bohr2(47.9); + candidate.pressure = gpa_to_hartree_per_bohr3(-0.36); + } + } + else if (preset == Preset::WaterCation) + { + candidate.cavity.density_min = 2e-4; + candidate.cavity.density_max = 3.5e-3; + candidate.surface_tension = dyn_per_cm_to_hartree_per_bohr2(5.0); + candidate.pressure = gpa_to_hartree_per_bohr3(0.125); + } + else if (preset == Preset::WaterAnion) + { + candidate.cavity.density_min = 2.4e-3; + candidate.cavity.density_max = 1.55e-2; + candidate.pressure = gpa_to_hartree_per_bohr3(0.45); + } + else + { + error = "SCCS custom configurations must be supplied explicitly"; + return false; + } + config = candidate; + return true; +} + +bool validate_config(const SccsConfig& config, std::string& error) +{ + if (!validate_cavity_parameters(config.cavity, error)) + { + return false; + } + if (!std::isfinite(config.surface_tension) || !std::isfinite(config.pressure)) + { + error = "SCCS requires finite surface tension and pressure"; + return false; + } + if (!std::isfinite(config.surface_regularization) || config.surface_regularization <= 0.0) + { + error = "SCCS requires finite positive surface regularization"; + return false; + } + return true; +} +} // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_parameters.h b/source/source_hamilt/module_sccs/sccs_parameters.h new file mode 100644 index 00000000000..e45ad4093c5 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_parameters.h @@ -0,0 +1,26 @@ +#ifndef SCCS_PARAMETERS_H +#define SCCS_PARAMETERS_H + +#include "sccs_cavity.h" + +namespace ModuleSccs +{ +enum class Preset { Custom, Vacuum, WaterNeutral, WaterCation, WaterAnion }; + +struct SccsConfig +{ + CavityParameters cavity; + double surface_tension = 0.0; // Hartree/Bohr^2 + double pressure = 0.0; // Hartree/Bohr^3 + double surface_regularization = 0.0; +}; + +// Failure leaves the output unchanged. Custom configurations are supplied by callers. +bool parse_preset(const std::string& name, Preset& preset, std::string& error); +bool make_sccs_config(Preset preset, SccsConfig& config, std::string& error); +bool validate_config(const SccsConfig& config, std::string& error); +double dyn_per_cm_to_hartree_per_bohr2(double value); +double gpa_to_hartree_per_bohr3(double value); +} // namespace ModuleSccs + +#endif diff --git a/source/source_hamilt/module_sccs/test/CMakeLists.txt b/source/source_hamilt/module_sccs/test/CMakeLists.txt new file mode 100644 index 00000000000..f3f5616e7d9 --- /dev/null +++ b/source/source_hamilt/module_sccs/test/CMakeLists.txt @@ -0,0 +1,4 @@ +AddTest(TARGET MODULE_HAMILT_sccs_cavity + SOURCES test_sccs_cavity.cpp ../sccs_cavity.cpp) +AddTest(TARGET MODULE_HAMILT_sccs_parameters + SOURCES test_sccs_parameters.cpp ../sccs_parameters.cpp ../sccs_cavity.cpp) diff --git a/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp b/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp new file mode 100644 index 00000000000..72050a2299f --- /dev/null +++ b/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp @@ -0,0 +1,148 @@ +#include "../sccs_cavity.h" +#include + +#include +#include + +namespace +{ +class SccsCavityTest : public ::testing::Test +{ +protected: + ModuleSccs::CavityParameters parameters; + std::string error; + + void SetUp() override + { + parameters.density_min = 1e-4; + parameters.density_max = 5e-3; + parameters.epsilon_bulk = 78.3; + } +}; + +TEST_F(SccsCavityTest, BulkAndSoluteLimits) +{ + const double bulk_densities[] = {-1e-6, 0.0, 1e-4}; + for (double density : bulk_densities) + { + ModuleSccs::CavityPoint point; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; + EXPECT_DOUBLE_EQ(point.solute, 0.0); + EXPECT_DOUBLE_EQ(point.epsilon, 78.3); + EXPECT_DOUBLE_EQ(point.dsolute_drho, 0.0); + EXPECT_DOUBLE_EQ(point.depsilon_drho, 0.0); + } + const double solute_densities[] = {5e-3, 0.1}; + for (double density : solute_densities) + { + ModuleSccs::CavityPoint point; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; + EXPECT_DOUBLE_EQ(point.solute, 1.0); + EXPECT_DOUBLE_EQ(point.epsilon, 1.0); + EXPECT_DOUBLE_EQ(point.dsolute_drho, 0.0); + EXPECT_DOUBLE_EQ(point.depsilon_drho, 0.0); + } +} + +TEST_F(SccsCavityTest, LogarithmicMidpoint) +{ + const double product = parameters.density_min * parameters.density_max; + const double density = std::sqrt(product); + const double ratio = parameters.density_max / parameters.density_min; + const double width = std::log(ratio); + const double epsilon = std::sqrt(parameters.epsilon_bulk); + const double log_epsilon = std::log(parameters.epsilon_bulk); + const double dsolute = 2.0 / (width * density); + const double depsilon = -epsilon * log_epsilon * dsolute; + ModuleSccs::CavityPoint point; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; + EXPECT_NEAR(point.solute, 0.5, 1e-14); + EXPECT_NEAR(point.epsilon, epsilon, 1e-13); + EXPECT_NEAR(point.dsolute_drho, dsolute, 1e-10); + EXPECT_NEAR(point.depsilon_drho, depsilon, 1e-9); +} + +TEST_F(SccsCavityTest, DerivativesAcrossTransition) +{ + const double fractions[] = {0.2, 0.5, 0.8}; + const double ratio = parameters.density_max / parameters.density_min; + for (double fraction : fractions) + { + const double scale = std::pow(ratio, fraction); + const double density = parameters.density_min * scale; + const double step = density * 1e-5; + const double lower_density = density - step; + const double upper_density = density + step; + ModuleSccs::CavityPoint point; + ModuleSccs::CavityPoint lower; + ModuleSccs::CavityPoint upper; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(lower_density, parameters, lower, error)) << error; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(upper_density, parameters, upper, error)) << error; + const double solute_fd = (upper.solute - lower.solute) / (2.0 * step); + const double epsilon_fd = (upper.epsilon - lower.epsilon) / (2.0 * step); + const double solute_ratio = solute_fd / point.dsolute_drho; + const double epsilon_ratio = epsilon_fd / point.depsilon_drho; + EXPECT_NEAR(solute_ratio, 1.0, 1e-8); + EXPECT_NEAR(epsilon_ratio, 1.0, 1e-8); + EXPECT_GT(point.dsolute_drho, 0.0); + EXPECT_LT(point.depsilon_drho, 0.0); + } +} + +TEST_F(SccsCavityTest, SmoothThresholds) +{ + const double near_min = parameters.density_min * (1.0 + 1e-8); + const double near_max = parameters.density_max * (1.0 - 1e-8); + ModuleSccs::CavityPoint lower; + ModuleSccs::CavityPoint upper; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(near_min, parameters, lower, error)) << error; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(near_max, parameters, upper, error)) << error; + EXPECT_NEAR(lower.solute, 0.0, 1e-14); + EXPECT_NEAR(upper.solute, 1.0, 1e-14); + EXPECT_NEAR(lower.epsilon, 78.3, 1e-12); + EXPECT_NEAR(upper.epsilon, 1.0, 1e-12); + EXPECT_NEAR(lower.dsolute_drho, 0.0, 1e-10); + EXPECT_NEAR(upper.dsolute_drho, 0.0, 1e-10); + EXPECT_NEAR(lower.depsilon_drho, 0.0, 1e-8); + EXPECT_NEAR(upper.depsilon_drho, 0.0, 1e-8); +} + +TEST_F(SccsCavityTest, VacuumHasNoDielectricResponse) +{ + parameters.epsilon_bulk = 1.0; + ModuleSccs::CavityPoint point; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(1e-3, parameters, point, error)) << error; + EXPECT_GT(point.solute, 0.0); + EXPECT_LT(point.solute, 1.0); + EXPECT_DOUBLE_EQ(point.epsilon, 1.0); + EXPECT_DOUBLE_EQ(point.depsilon_drho, 0.0); +} + +TEST_F(SccsCavityTest, InvalidInputPreservesResult) +{ + ModuleSccs::CavityPoint point; + point.solute = 0.25; + point.epsilon = 12.0; + const double nan = std::numeric_limits::quiet_NaN(); + EXPECT_FALSE(ModuleSccs::evaluate_cavity(nan, parameters, point, error)); + EXPECT_FALSE(error.empty()); + EXPECT_DOUBLE_EQ(point.solute, 0.25); + EXPECT_DOUBLE_EQ(point.epsilon, 12.0); + parameters.density_min = parameters.density_max; + EXPECT_FALSE(ModuleSccs::evaluate_cavity(1e-3, parameters, point, error)); + EXPECT_DOUBLE_EQ(point.epsilon, 12.0); + parameters.density_min = 1e-4; + parameters.epsilon_bulk = 0.5; + EXPECT_FALSE(ModuleSccs::validate_cavity_parameters(parameters, error)); + parameters.epsilon_bulk = nan; + EXPECT_FALSE(ModuleSccs::validate_cavity_parameters(parameters, error)); + parameters.epsilon_bulk = 78.3; + parameters.density_min = std::numeric_limits::min(); + parameters.density_max = std::numeric_limits::max(); + EXPECT_FALSE(ModuleSccs::evaluate_cavity(1.0, parameters, point, error)); + EXPECT_FALSE(error.empty()); + EXPECT_DOUBLE_EQ(point.solute, 0.25); + EXPECT_DOUBLE_EQ(point.epsilon, 12.0); +} +} // namespace diff --git a/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp b/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp new file mode 100644 index 00000000000..c120d8bd053 --- /dev/null +++ b/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp @@ -0,0 +1,95 @@ +#include "../sccs_parameters.h" +#include + +#include + +TEST(SccsParameters, PresetNamesAndFailure) +{ + std::string error; + ModuleSccs::Preset preset = ModuleSccs::Preset::Custom; + ASSERT_TRUE(ModuleSccs::parse_preset("Water-Neutral", preset, error)); + EXPECT_EQ(preset, ModuleSccs::Preset::WaterNeutral); + ASSERT_TRUE(ModuleSccs::parse_preset("WATER-CATION", preset, error)); + EXPECT_EQ(preset, ModuleSccs::Preset::WaterCation); + ASSERT_TRUE(ModuleSccs::parse_preset("water-anion", preset, error)); + EXPECT_EQ(preset, ModuleSccs::Preset::WaterAnion); + EXPECT_FALSE(ModuleSccs::parse_preset("unknown", preset, error)); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(preset, ModuleSccs::Preset::WaterAnion); + ASSERT_TRUE(ModuleSccs::parse_preset("vacuum", preset, error)); + EXPECT_EQ(preset, ModuleSccs::Preset::Vacuum); + EXPECT_TRUE(error.empty()); + ASSERT_TRUE(ModuleSccs::parse_preset("custom", preset, error)); + EXPECT_EQ(preset, ModuleSccs::Preset::Custom); +} + +TEST(SccsParameters, OriginalWaterPresets) +{ + const ModuleSccs::Preset presets[] = {ModuleSccs::Preset::WaterNeutral, + ModuleSccs::Preset::WaterCation, + ModuleSccs::Preset::WaterAnion}; + const double minima[] = {1e-4, 2e-4, 2.4e-3}; + const double maxima[] = {5e-3, 3.5e-3, 1.55e-2}; + const double tensions[] = {3.076640259577260e-5, 3.211524279308205e-6, 0.0}; + const double pressures[] = {-1.223615131827540e-5, 4.248663652178958e-6, 1.529518914784425e-5}; + std::string error; + for (int index = 0; index < 3; ++index) + { + ModuleSccs::SccsConfig config; + ASSERT_TRUE(ModuleSccs::make_sccs_config(presets[index], config, error)) << error; + ASSERT_TRUE(ModuleSccs::validate_config(config, error)) << error; + EXPECT_DOUBLE_EQ(config.cavity.density_min, minima[index]); + EXPECT_DOUBLE_EQ(config.cavity.density_max, maxima[index]); + EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 78.3); + EXPECT_DOUBLE_EQ(config.surface_regularization, 1e-8); + EXPECT_NEAR(config.surface_tension, tensions[index], 1e-18); + EXPECT_NEAR(config.pressure, pressures[index], 1e-18); + } +} + +TEST(SccsParameters, VacuumAndExplicitCustom) +{ + ModuleSccs::SccsConfig config; + std::string error; + ASSERT_TRUE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum, config, error)); + ASSERT_TRUE(ModuleSccs::validate_config(config, error)); + EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 1.0); + EXPECT_DOUBLE_EQ(config.surface_tension, 0.0); + EXPECT_DOUBLE_EQ(config.pressure, 0.0); + config.cavity.epsilon_bulk = 12.0; + EXPECT_FALSE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::Custom, config, error)); + EXPECT_FALSE(error.empty()); + EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 12.0); + EXPECT_TRUE(ModuleSccs::validate_config(config, error)); +} + +TEST(SccsParameters, PhysicalUnitConversions) +{ + const double tension = ModuleSccs::dyn_per_cm_to_hartree_per_bohr2(1.0); + const double pressure = ModuleSccs::gpa_to_hartree_per_bohr3(1.0); + EXPECT_NEAR(tension, 6.423048558616410e-7, 1e-20); + EXPECT_NEAR(pressure, 3.398930921743166e-5, 1e-18); +} + +TEST(SccsParameters, InvalidConfiguration) +{ + ModuleSccs::SccsConfig config; + std::string error; + ASSERT_TRUE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::WaterNeutral, config, error)); + config.surface_regularization = 0.0; + EXPECT_FALSE(ModuleSccs::validate_config(config, error)); + EXPECT_FALSE(error.empty()); + config.surface_regularization = 1e-8; + const double nan = std::numeric_limits::quiet_NaN(); + config.pressure = nan; + EXPECT_FALSE(ModuleSccs::validate_config(config, error)); + config.pressure = -0.1; + config.surface_tension = nan; + EXPECT_FALSE(ModuleSccs::validate_config(config, error)); + config.surface_tension = 0.0; + config.cavity.density_min = -1.0; + EXPECT_FALSE(ModuleSccs::validate_config(config, error)); + config.cavity.density_min = 1e-4; + EXPECT_TRUE(ModuleSccs::validate_config(config, error)); + EXPECT_TRUE(error.empty()); +} From 709ab18989a49ceba9639fcc251bdc1fa6ea0caf Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 17:44:57 -0600 Subject: [PATCH 13/41] feat: add Gaussian ionic source and periodic SCCS solver --- source/Makefile.Objects | 5 +- .../source_hamilt/module_sccs/CMakeLists.txt | 7 +- .../module_sccs/sccs_ionic_charge.cpp | 67 ++++ .../module_sccs/sccs_ionic_charge.h | 31 ++ .../module_sccs/sccs_pw_coulomb.cpp | 94 +++++ .../module_sccs/sccs_pw_coulomb.h | 39 ++ .../module_sccs/sccs_response.cpp | 334 ++++++++++++++++++ .../source_hamilt/module_sccs/sccs_response.h | 60 ++++ .../module_sccs/test/CMakeLists.txt | 16 + .../module_sccs/test/sccs_test.h | 60 ++++ .../test/test_sccs_ionic_charge.cpp | 76 ++++ .../module_sccs/test/test_sccs_pw_coulomb.cpp | 92 +++++ .../module_sccs/test/test_sccs_response.cpp | 190 ++++++++++ 13 files changed, 1069 insertions(+), 2 deletions(-) create mode 100644 source/source_hamilt/module_sccs/sccs_ionic_charge.cpp create mode 100644 source/source_hamilt/module_sccs/sccs_ionic_charge.h create mode 100644 source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp create mode 100644 source/source_hamilt/module_sccs/sccs_pw_coulomb.h create mode 100644 source/source_hamilt/module_sccs/sccs_response.cpp create mode 100644 source/source_hamilt/module_sccs/sccs_response.h create mode 100644 source/source_hamilt/module_sccs/test/sccs_test.h create mode 100644 source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp create mode 100644 source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp create mode 100644 source/source_hamilt/module_sccs/test/test_sccs_response.cpp diff --git a/source/Makefile.Objects b/source/Makefile.Objects index d5d8cb18db9..48f7665a723 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -613,7 +613,10 @@ OBJS_SURCHEM=surchem.o\ sol_force.o\ OBJS_SCCS=sccs_cavity.o\ - sccs_parameters.o + sccs_parameters.o\ + sccs_ionic_charge.o\ + sccs_pw_coulomb.o\ + sccs_response.o OBJS_SYMMETRY=symm_other.o\ symm_rot_spin.o\ diff --git a/source/source_hamilt/module_sccs/CMakeLists.txt b/source/source_hamilt/module_sccs/CMakeLists.txt index 49ee0926d51..67bb6a65170 100644 --- a/source/source_hamilt/module_sccs/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/CMakeLists.txt @@ -1,4 +1,9 @@ -add_library(sccs OBJECT sccs_cavity.cpp sccs_parameters.cpp) +add_library(sccs OBJECT + sccs_cavity.cpp + sccs_parameters.cpp + sccs_ionic_charge.cpp + sccs_pw_coulomb.cpp + sccs_response.cpp) if(ENABLE_COVERAGE) add_coverage(sccs) diff --git a/source/source_hamilt/module_sccs/sccs_ionic_charge.cpp b/source/source_hamilt/module_sccs/sccs_ionic_charge.cpp new file mode 100644 index 00000000000..3d0f01d8cac --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_ionic_charge.cpp @@ -0,0 +1,67 @@ +#include "sccs_ionic_charge.h" +#include "sccs_pw_coulomb.h" + +#include "source_base/constants.h" +#include "source_base/parallel_reduce.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_cell/cell_tools.h" + +#include +#include + +namespace ModuleSccs +{ +bool gaussian_ionic_density(const std::vector& atoms, + const ModulePW::PW_Basis& basis, + double tpiba, + double spread, + std::vector& density, + std::string& error) +{ + if (!validate_pw_grid(basis, tpiba, error)) + { + return false; + } + double invalid = 0.0; + if (!std::isfinite(spread) || spread <= 0.0) + { + invalid = 1.0; + } + for (const unitcell::AtomData& atom : atoms) + { + if (!std::isfinite(atom.valence_charge) || atom.valence_charge < 0.0 + || !std::isfinite(atom.position.x) || !std::isfinite(atom.position.y) + || !std::isfinite(atom.position.z)) + { + invalid = 1.0; + } + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + if (invalid != 0.0) + { + error = "SCCS Gaussian ions require finite positions, nonnegative valence charges and positive finite spread"; + return false; + } + + std::vector> charge(basis.npw, 0.0); + const double tpiba2 = tpiba * tpiba; + for (const unitcell::AtomData& atom : atoms) + { + for (int ig = 0; ig < basis.npw; ++ig) + { + const double exponent = -0.25 * spread * spread * tpiba2 * basis.gg[ig]; + const double phase = tpiba * (basis.gcar[ig] * atom.position); + const std::complex phase_factor = ModuleBase::NEG_IMAG_UNIT * phase; + charge[ig] += atom.valence_charge / basis.omega * std::exp(exponent) * std::exp(phase_factor); + } + } + std::vector candidate(basis.nrxx); + basis.recip2real(charge.data(), candidate.data()); + if (!validate_grid_values(candidate, basis, error)) + { + return false; + } + density.swap(candidate); + return true; +} +} // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_ionic_charge.h b/source/source_hamilt/module_sccs/sccs_ionic_charge.h new file mode 100644 index 00000000000..3bba3ff565c --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_ionic_charge.h @@ -0,0 +1,31 @@ +#ifndef SCCS_IONIC_CHARGE_H +#define SCCS_IONIC_CHARGE_H + +#include +#include + +namespace ModulePW +{ +class PW_Basis; +} +namespace unitcell +{ +struct AtomData; +} + +namespace ModuleSccs +{ +// rho ~ exp(-r^2/spread^2), in Bohr; preserves the original SCCS ionic spread. +const double gaussian_ion_spread = 0.5; + +// AtomData is extracted by source_cell; positions are Cartesian Bohr. +// Collective over the PW pool. Failure leaves density unchanged. +bool gaussian_ionic_density(const std::vector& atoms, + const ModulePW::PW_Basis& basis, + double tpiba, + double spread, + std::vector& density, + std::string& error); +} // namespace ModuleSccs + +#endif diff --git a/source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp b/source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp new file mode 100644 index 00000000000..7879baac064 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp @@ -0,0 +1,94 @@ +#include "sccs_pw_coulomb.h" + +#include "source_base/constants.h" +#include "source_base/parallel_reduce.h" +#include "source_basis/module_pw/pw_basis.h" + +#include + +namespace ModuleSccs +{ +bool validate_pw_grid(const ModulePW::PW_Basis& basis, double tpiba, std::string& error) +{ + error.clear(); + double invalid = 0.0; + if (!std::isfinite(tpiba) || tpiba <= 0.0 || !std::isfinite(basis.omega) || basis.omega <= 0.0 + || basis.gamma_only || basis.nxyz <= 0 || basis.nrxx < 0 || basis.npw < 0 + || basis.nmaxgr < basis.nrxx || basis.nmaxgr < basis.npw + || (basis.npw > 0 && (basis.gg == nullptr || basis.gcar == nullptr))) + { + invalid = 1.0; + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + if (invalid != 0.0) + { + error = "SCCS requires an initialized full-G PW grid and positive finite tpiba and volume on every pool rank"; + return false; + } + return true; +} + +bool validate_grid_values(const std::vector& values, + const ModulePW::PW_Basis& basis, + std::string& error) +{ + error.clear(); + const std::size_t local_size = basis.nrxx; + double invalid = 0.0; + if (values.size() != local_size) + { + invalid = 1.0; + } + for (double value : values) + { + if (!std::isfinite(value)) + { + invalid = 1.0; + } + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + if (invalid != 0.0) + { + error = "SCCS requires finite values matching the local PW grid on every pool rank"; + return false; + } + return true; +} + +PeriodicCoulombOperator::PeriodicCoulombOperator(const ModulePW::PW_Basis& basis, double tpiba) + : basis_(basis), tpiba_(tpiba) +{ +} + +bool PeriodicCoulombOperator::apply_potential(const std::vector& charge, + std::vector& potential, + std::string& error) +{ + if (!validate_pw_grid(basis_, tpiba_, error) || !validate_grid_values(charge, basis_, error)) + { + return false; + } + reciprocal_work_.resize(basis_.npw); + basis_.real2recip(charge.data(), reciprocal_work_.data()); + const double tpiba2 = tpiba_ * tpiba_; + for (int ig = 0; ig < basis_.npw; ++ig) + { + if (ig == basis_.ig_gge0 || basis_.gg[ig] == 0.0) + { + reciprocal_work_[ig] = 0.0; + } + else + { + reciprocal_work_[ig] = ModuleBase::FOUR_PI * reciprocal_work_[ig] / (tpiba2 * basis_.gg[ig]); + } + } + std::vector candidate(basis_.nrxx); + basis_.recip2real(reciprocal_work_.data(), candidate.data()); + if (!validate_grid_values(candidate, basis_, error)) + { + return false; + } + potential.swap(candidate); + return true; +} +} // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_pw_coulomb.h b/source/source_hamilt/module_sccs/sccs_pw_coulomb.h new file mode 100644 index 00000000000..5368fa7624e --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_pw_coulomb.h @@ -0,0 +1,39 @@ +#ifndef SCCS_PW_COULOMB_H +#define SCCS_PW_COULOMB_H + +#include +#include +#include + +namespace ModulePW +{ +class PW_Basis; +} + +namespace ModuleSccs +{ +// All calls are collective over the ABACUS pool used to initialize basis. +bool validate_pw_grid(const ModulePW::PW_Basis& basis, double tpiba, std::string& error); +bool validate_grid_values(const std::vector& values, + const ModulePW::PW_Basis& basis, + std::string& error); + +// Potential in Hartree for a signed charge density in e/Bohr^3. +// G=0 is removed: this is the periodic Coulomb inverse with a uniform background. +class PeriodicCoulombOperator +{ +public: + PeriodicCoulombOperator(const ModulePW::PW_Basis& basis, double tpiba); + bool apply_potential(const std::vector& charge, + std::vector& potential, + std::string& error); + +private: + const ModulePW::PW_Basis& basis_; + const double tpiba_; + // Scratch only, overwritten on each call; PW_Basis FFTs are also non-reentrant. + std::vector> reciprocal_work_; +}; +} // namespace ModuleSccs + +#endif diff --git a/source/source_hamilt/module_sccs/sccs_response.cpp b/source/source_hamilt/module_sccs/sccs_response.cpp new file mode 100644 index 00000000000..039ca1643aa --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_response.cpp @@ -0,0 +1,334 @@ +#include "sccs_response.h" +#include "sccs_pw_coulomb.h" + +#include "source_base/constants.h" +#include "source_base/parallel_reduce.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_hamilt/module_xc/xc_functional.h" + +#include +#include +#include +#include + +namespace ModuleSccs +{ +namespace +{ +bool prepare_cavity(const std::vector& density, + const CavityParameters& cavity, + const ModulePW::PW_Basis& basis, + double tpiba, + SccsResponse& response, + std::vector& coefficient, + std::string& error) +{ + const std::size_t size = density.size(); + response.solute.resize(size); + response.dsolute_drho.resize(size); + response.epsilon.resize(size); + response.depsilon_drho.resize(size); + response.grad_log_epsilon.resize(size); + double invalid = 0.0; + for (std::size_t i = 0; i < size; ++i) + { + CavityPoint point; + if (!evaluate_cavity(density[i], cavity, point, error)) + { + invalid = 1.0; + continue; + } + response.solute[i] = point.solute; + response.dsolute_drho[i] = point.dsolute_drho; + response.epsilon[i] = point.epsilon; + response.depsilon_drho[i] = point.depsilon_drho; + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + if (invalid != 0.0) + { + error = "SCCS cavity evaluation failed on a pool rank"; + return false; + } + + std::vector> density_g(basis.npw); + basis.real2recip(density.data(), density_g.data()); + std::vector> gradient(size); + std::vector laplacian(size); + // These XC helpers use G in units of tpiba and ABACUS's normalized FFT. + XC_Functional::grad_rho(density_g.data(), gradient.data(), &basis, tpiba); + XC_Functional::laplacian_rho(density_g.data(), laplacian.data(), &basis, tpiba); + const double density_ratio = cavity.density_max / cavity.density_min; + const double width = std::log(density_ratio); + const double log_bulk = std::log(cavity.epsilon_bulk); + coefficient.resize(size); + for (std::size_t i = 0; i < size; ++i) + { + double second_log = 0.0; + if (density[i] > cavity.density_min && density[i] < cavity.density_max) + { + const double local_ratio = cavity.density_max / density[i]; + const double x = std::log(local_ratio) / width; + const double angle = ModuleBase::TWO_PI * x; + second_log = log_bulk * (1.0 - std::cos(angle) + ModuleBase::TWO_PI * std::sin(angle) / width) + / (width * density[i] * density[i]); + } + const double first_log = response.depsilon_drho[i] / response.epsilon[i]; + const ModuleBase::Vector3& local_gradient = gradient[i]; + const double gradient_square = local_gradient * local_gradient; + response.grad_log_epsilon[i] = local_gradient * first_log; + const double lap_log = first_log * laplacian[i] + second_log * gradient_square; + coefficient[i] = response.epsilon[i] * (0.5 * lap_log + 0.25 * first_log * first_log * gradient_square) + / ModuleBase::FOUR_PI; + } + return validate_grid_values(coefficient, basis, error); +} + +bool residual_norms(const std::vector& values, + const ModulePW::PW_Basis& basis, + double& rms, + double& maximum, + std::string& error) +{ + double square = 0.0; + maximum = 0.0; + double invalid = 0.0; + for (double value : values) + { + if (!std::isfinite(value)) + { + invalid = 1.0; + } + square += value * value; + const double magnitude = std::abs(value); + maximum = std::max(maximum, magnitude); + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + Parallel_Reduce::reduce_pool(square); + Parallel_Reduce::reduce_max_pool(basis.poolnproc, maximum); + if (invalid != 0.0 || !std::isfinite(square)) + { + error = "SCCS sqrt-CG residual is not finite"; + return false; + } + const double mean_square = square / basis.nxyz; + rms = std::sqrt(mean_square); + return true; +} + +bool converged(const PolarizationResult& result, const PolarizationSolverParameters& solver) +{ + return result.residual_rms <= solver.tolerance_rms && result.residual_max <= solver.tolerance_max; +} + +double grid_dot(const std::vector& left, + const std::vector& right, + const ModulePW::PW_Basis& basis) +{ + double value = 0.0; + for (std::size_t i = 0; i < left.size(); ++i) + { + value += left[i] * right[i]; + } + Parallel_Reduce::reduce_pool(value); + return value * basis.omega / basis.nxyz; +} + +// P r = eps^-1/2 G eps^-1/2 r; only FFT scratch survives an application. +bool apply_preconditioner(const std::vector& rhs, + const std::vector& invsqrt, + PeriodicCoulombOperator& coulomb, + std::vector& weighted, + std::vector& value, + std::string& error) +{ + const std::size_t size = rhs.size(); + weighted.resize(size); + for (std::size_t i = 0; i < size; ++i) + { + weighted[i] = rhs[i] * invsqrt[i]; + } + if (!coulomb.apply_potential(weighted, value, error)) + { + return false; + } + for (std::size_t i = 0; i < size; ++i) + { + value[i] *= invsqrt[i]; + } + return true; +} +} // namespace + +bool solve_sccs_response(const std::vector& density, + const std::vector& charge, + const CavityParameters& cavity, + const PolarizationSolverParameters& solver, + const std::vector& initial_potential, + const ModulePW::PW_Basis& basis, + double tpiba, + SccsResponse& result, + std::string& error) +{ + if (!validate_pw_grid(basis, tpiba, error) || !validate_grid_values(density, basis, error) + || !validate_grid_values(charge, basis, error)) + { + return false; + } + double invalid = 0.0; + if (!validate_cavity_parameters(cavity, error) || solver.max_iterations <= 0 + || !std::isfinite(solver.tolerance_rms) || solver.tolerance_rms <= 0.0 + || !std::isfinite(solver.tolerance_max) || solver.tolerance_max <= 0.0) + { + invalid = 1.0; + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + if (invalid != 0.0) + { + error = "SCCS requires valid cavity parameters, a positive iteration limit and finite positive tolerances"; + return false; + } + + // Determine start mode collectively, including ranks with no real-space points. + double initial_count = initial_potential.size(); + Parallel_Reduce::reduce_pool(initial_count); + const bool warm_start = initial_count != 0.0; + if (warm_start && !validate_grid_values(initial_potential, basis, error)) + { + return false; + } + + SccsResponse candidate; + std::vector coefficient; + if (!prepare_cavity(density, cavity, basis, tpiba, candidate, coefficient, error)) + { + return false; + } + const std::size_t size = density.size(); + std::vector invsqrt(size); + for (std::size_t i = 0; i < size; ++i) + { + invsqrt[i] = 1.0 / std::sqrt(candidate.epsilon[i]); + } + PeriodicCoulombOperator coulomb(basis, tpiba); + std::vector residual = charge; + std::vector potential(size, 0.0); + std::vector direction(size, 0.0); + std::vector image(size, 0.0); + std::vector weighted; + std::vector z; + PolarizationResult& polarization = candidate.polarization; + if (!residual_norms(residual, basis, polarization.residual_rms, polarization.residual_max, error)) + { + return false; + } + // Retain the old fixed-point warm start only when it reduces the charge residual. + if (!converged(polarization, solver) && warm_start) + { + std::vector guess_residual(size); + for (std::size_t i = 0; i < size; ++i) + { + guess_residual[i] = charge[i] - coefficient[i] * initial_potential[i]; + } + if (!apply_preconditioner(guess_residual, invsqrt, coulomb, weighted, z, error)) + { + return false; + } + for (std::size_t i = 0; i < size; ++i) + { + guess_residual[i] = coefficient[i] * (initial_potential[i] - z[i]); + } + double guess_rms = 0.0; + double guess_max = 0.0; + if (!residual_norms(guess_residual, basis, guess_rms, guess_max, error)) + { + return false; + } + if (guess_rms < polarization.residual_rms) + { + potential.swap(z); + residual.swap(guess_residual); + polarization.warm_started = true; + polarization.residual_rms = guess_rms; + polarization.residual_max = guess_max; + } + } + double old_rz = 0.0; + for (int iteration = 1; !converged(polarization, solver) && iteration <= solver.max_iterations; ++iteration) + { + if (!apply_preconditioner(residual, invsqrt, coulomb, weighted, z, error)) + { + return false; + } + const double rz = grid_dot(residual, z, basis); + if (!std::isfinite(rz) || std::abs(rz) < 1e-30) + { + error = "SCCS sqrt-CG has a null or nonfinite preconditioned residual"; + return false; + } + const double beta = std::abs(old_rz) > 1e-30 ? rz / old_rz : 0.0; + old_rz = rz; + for (std::size_t i = 0; i < size; ++i) + { + direction[i] = z[i] + beta * direction[i]; + image[i] = coefficient[i] * z[i] + residual[i] + beta * image[i]; + } + const double curvature = grid_dot(direction, image, basis); + if (!std::isfinite(curvature) || curvature == 0.0) + { + error = "SCCS sqrt-CG has invalid curvature"; + return false; + } + const double alpha = rz / curvature; + for (std::size_t i = 0; i < size; ++i) + { + potential[i] += alpha * direction[i]; + residual[i] -= alpha * image[i]; + } + polarization.iterations = iteration; + if (!residual_norms(residual, basis, polarization.residual_rms, polarization.residual_max, error)) + { + return false; + } + } + if (!converged(polarization, solver)) + { + error = "SCCS sqrt-CG did not reach both residual tolerances within the iteration limit"; + return false; + } + + if (!validate_grid_values(potential, basis, error)) + { + return false; + } + candidate.restart_potential = potential; + double mean = 0.0; + for (double value : potential) + { + mean += value; + } + Parallel_Reduce::reduce_pool(mean); + mean /= basis.nxyz; + for (double& value : potential) + { + value -= mean; + } + std::vector> potential_g(basis.npw); + basis.real2recip(potential.data(), potential_g.data()); + polarization.gradient.resize(size); + XC_Functional::grad_rho(potential_g.data(), polarization.gradient.data(), &basis, tpiba); + polarization.potential.swap(potential); + candidate.cavity_potential.resize(size); + for (std::size_t i = 0; i < size; ++i) + { + const ModuleBase::Vector3& gradient = polarization.gradient[i]; + const double gradient_square = gradient * gradient; + candidate.cavity_potential[i] = -candidate.depsilon_drho[i] * gradient_square / (8.0 * ModuleBase::PI); + } + if (!validate_grid_values(candidate.cavity_potential, basis, error)) + { + return false; + } + result = std::move(candidate); + return true; +} +} // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_response.h b/source/source_hamilt/module_sccs/sccs_response.h new file mode 100644 index 00000000000..a8060a9d3a1 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_response.h @@ -0,0 +1,60 @@ +#ifndef SCCS_RESPONSE_H +#define SCCS_RESPONSE_H + +#include "sccs_cavity.h" +#include "source_base/vector3.h" + +#include + +namespace ModulePW +{ +class PW_Basis; +} + +namespace ModuleSccs +{ +struct PolarizationSolverParameters +{ + int max_iterations = 200; + double tolerance_rms = 1e-10; + double tolerance_max = 1e-8; +}; + +struct PolarizationResult +{ + int iterations = 0; + double residual_rms = 0.0; + double residual_max = 0.0; + bool warm_started = false; + std::vector potential; // Hartree, shifted to periodic zero mean + std::vector> gradient; // Hartree/Bohr +}; + +struct SccsResponse +{ + std::vector solute; + std::vector dsolute_drho; + std::vector epsilon; + std::vector depsilon_drho; + std::vector> grad_log_epsilon; + PolarizationResult polarization; + std::vector cavity_potential; // -epsilon' |grad v|^2/(8 pi), Hartree + std::vector restart_potential; // unshifted solution for explicit warm starts +}; + +// Original chain-derivative sqrt-CG periodic response; charge is ions minus electrons. +// All ranks in the PW pool must call together with the same configuration. +// initial_potential is empty on all ranks for a cold start, otherwise a local grid. +// Failure leaves result unchanged. No state or INPUT globals are read or retained. +bool solve_sccs_response(const std::vector& cavity_density, + const std::vector& solute_charge, + const CavityParameters& cavity, + const PolarizationSolverParameters& solver, + const std::vector& initial_potential, + const ModulePW::PW_Basis& basis, + double tpiba, + SccsResponse& result, + std::string& error); +} // namespace ModuleSccs + +#endif diff --git a/source/source_hamilt/module_sccs/test/CMakeLists.txt b/source/source_hamilt/module_sccs/test/CMakeLists.txt index f3f5616e7d9..926c7c0fa49 100644 --- a/source/source_hamilt/module_sccs/test/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/test/CMakeLists.txt @@ -2,3 +2,19 @@ AddTest(TARGET MODULE_HAMILT_sccs_cavity SOURCES test_sccs_cavity.cpp ../sccs_cavity.cpp) AddTest(TARGET MODULE_HAMILT_sccs_parameters SOURCES test_sccs_parameters.cpp ../sccs_parameters.cpp ../sccs_cavity.cpp) + +AddTest(TARGET MODULE_HAMILT_sccs_poisson + LIBS parameter planewave device base + SOURCES test_sccs_pw_coulomb.cpp test_sccs_ionic_charge.cpp test_sccs_response.cpp + ../sccs_ionic_charge.cpp ../sccs_pw_coulomb.cpp ../sccs_response.cpp ../sccs_cavity.cpp + ../../module_xc/xc_grad_utils.cpp) +set_tests_properties(MODULE_HAMILT_sccs_poisson PROPERTIES + ENVIRONMENT "OMP_NUM_THREADS=1" TIMEOUT 60) + +if(ENABLE_MPI) + add_test(NAME MODULE_HAMILT_sccs_poisson_mpi + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 2 ${MPIEXEC_PREFLAGS} + $ ${MPIEXEC_POSTFLAGS}) + set_tests_properties(MODULE_HAMILT_sccs_poisson_mpi PROPERTIES + ENVIRONMENT "OMP_NUM_THREADS=1" TIMEOUT 60 PROCESSORS 2) +endif() diff --git a/source/source_hamilt/module_sccs/test/sccs_test.h b/source/source_hamilt/module_sccs/test/sccs_test.h new file mode 100644 index 00000000000..6a1570cca4f --- /dev/null +++ b/source/source_hamilt/module_sccs/test/sccs_test.h @@ -0,0 +1,60 @@ +#ifndef SCCS_TEST_H +#define SCCS_TEST_H + +#include "source_base/constants.h" +#include "source_base/matrix3.h" +#include "source_base/parallel_comm.h" +#include "source_basis/module_pw/pw_basis.h" +#include + +#include + +namespace SccsTest +{ +extern int pool_size; +extern int pool_rank; + +class PwTest : public testing::Test +{ +protected: + PwTest() : basis("cpu", "double") {} + + void SetUp() override + { +#ifdef __MPI + basis.initmpi(pool_size, pool_rank, POOL_WORLD); +#endif + const ModuleBase::Matrix3 lattice(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + basis.initgrids(length, lattice, 20.0); + basis.initparameters(false, 20.0, 1, false); + basis.setuptransform(); + basis.collect_local_pw(); + tpiba = ModuleBase::TWO_PI / length; + } + + // A cosine along x or y, in the PW local real-grid ordering. + std::vector cosine_mode(int direction) const + { + std::vector values(basis.nrxx); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + const int ix = ir / (basis.ny * basis.nplane); + const int iy = ir / basis.nplane - ix * basis.ny; + const int coordinate = direction == 0 ? ix : iy; + const int count = direction == 0 ? basis.nx : basis.ny; + const double angle = ModuleBase::TWO_PI * coordinate / count; + values[ir] = std::cos(angle); + } + return values; + } + + const double length = 10.0; + double tpiba = 0.0; + ModulePW::PW_Basis basis; + std::string error; +}; +} // namespace SccsTest + +#endif diff --git a/source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp b/source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp new file mode 100644 index 00000000000..64a0a2cc617 --- /dev/null +++ b/source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp @@ -0,0 +1,76 @@ +#include "sccs_test.h" +#include "../sccs_ionic_charge.h" + +#include "source_base/parallel_reduce.h" +#include "source_cell/cell_tools.h" + +using SccsIonicChargeTest = SccsTest::PwTest; + +TEST_F(SccsIonicChargeTest, ValenceNormalizationAndFourierPhase) +{ + std::vector atoms(2); + atoms[0].position.x = length / 4.0; + atoms[0].valence_charge = 8.0; + atoms[1].valence_charge = 1.0; + const double spread = ModuleSccs::gaussian_ion_spread; + std::vector density; + ASSERT_TRUE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, spread, density, error)) << error; + double integral = 0.0; + for (double value : density) + { + integral += value; + } + Parallel_Reduce::reduce_pool(integral); + integral *= basis.omega / basis.nxyz; + EXPECT_NEAR(integral, 9.0, 1e-12); + + std::vector> charge_g(basis.npw); + basis.real2recip(density.data(), charge_g.data()); + const double exponent = -spread * spread * tpiba * tpiba / 4.0; + const double amplitude = 8.0 / basis.omega * std::exp(exponent); + const double second_amplitude = 1.0 / basis.omega * std::exp(exponent); + double selected = 0.0; + for (int ig = 0; ig < basis.npw; ++ig) + { + const ModuleBase::Vector3& g = basis.gdirect[ig]; + if (g.x == 1.0 && g.y == 0.0 && g.z == 0.0) + { + selected = 1.0; + EXPECT_NEAR(charge_g[ig].real(), second_amplitude, 1e-14); + const double expected_imaginary = -amplitude; + EXPECT_NEAR(charge_g[ig].imag(), expected_imaginary, 1e-14); + } + } + Parallel_Reduce::reduce_pool(selected); + EXPECT_DOUBLE_EQ(selected, 1.0); + atoms[0].position.x += length; + std::vector translated; + ASSERT_TRUE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, spread, translated, error)) << error; + for (int ir = 0; ir < basis.nrxx; ++ir) + { + EXPECT_NEAR(translated[ir], density[ir], 1e-13); + } +} + +TEST_F(SccsIonicChargeTest, InvalidIonAndSpreadPreserveOutput) +{ + std::vector atoms(1); + atoms[0].valence_charge = 1.0; + if (basis.poolrank == 0) + { + atoms[0].valence_charge = -1.0; + } + std::vector density(1, 12.0); + EXPECT_FALSE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, 0.5, density, error)); + EXPECT_FALSE(error.empty()); + EXPECT_DOUBLE_EQ(density[0], 12.0); + atoms[0].valence_charge = 1.0; + EXPECT_FALSE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, 0.0, density, error)); + EXPECT_DOUBLE_EQ(density[0], 12.0); + atoms.clear(); + ASSERT_TRUE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, 0.5, density, error)) << error; + for (double value : density) + { + EXPECT_DOUBLE_EQ(value, 0.0); + } +} diff --git a/source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp b/source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp new file mode 100644 index 00000000000..40e3d3c4c98 --- /dev/null +++ b/source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp @@ -0,0 +1,92 @@ +#include "sccs_test.h" +#include "../sccs_pw_coulomb.h" + +#include "source_base/parallel_global.h" + +#include + +namespace SccsTest +{ +int pool_size = 1; +int pool_rank = 0; +} + +using SccsPwCoulombTest = SccsTest::PwTest; + +TEST_F(SccsPwCoulombTest, FourierModeAndConstantBackground) +{ + const std::vector mode = cosine_mode(0); + std::vector charge = mode; + for (double& value : charge) + { + value += 0.3; + } + ModuleSccs::PeriodicCoulombOperator coulomb(basis, tpiba); + std::vector potential; + ASSERT_TRUE(coulomb.apply_potential(charge, potential, error)) << error; + const double kernel = ModuleBase::FOUR_PI / (tpiba * tpiba); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + const double expected = kernel * mode[ir]; + EXPECT_NEAR(potential[ir], expected, 1e-11); + } + charge.assign(basis.nrxx, 1.0); + ASSERT_TRUE(coulomb.apply_potential(charge, potential, error)) << error; + for (double value : potential) + { + EXPECT_NEAR(value, 0.0, 1e-12); + } +} + +TEST_F(SccsPwCoulombTest, RankLocalInvalidInputReturnsCollectively) +{ + std::vector charge(basis.nrxx, 0.0); + if (basis.poolrank == 0) + { + charge[0] = std::numeric_limits::quiet_NaN(); + } + ModuleSccs::PeriodicCoulombOperator coulomb(basis, tpiba); + std::vector potential(1, 12.0); + EXPECT_FALSE(coulomb.apply_potential(charge, potential, error)); + EXPECT_FALSE(error.empty()); + ASSERT_EQ(potential.size(), 1u); + EXPECT_DOUBLE_EQ(potential[0], 12.0); + charge.assign(basis.nrxx, 0.0); + if (basis.poolrank == 0) + { + charge.pop_back(); + } + EXPECT_FALSE(coulomb.apply_potential(charge, potential, error)); + EXPECT_DOUBLE_EQ(potential[0], 12.0); +} + +int main(int argc, char** argv) +{ + int threads = 1; + Parallel_Global::read_pal_param(argc, argv, SccsTest::pool_size, threads, SccsTest::pool_rank); + int band_size = 1; + int band_rank = 0; + int band_group = 0; + int pool = 0; +#ifdef __MPI + // These tests create only pool/band groups, not diagonalization/grid groups. + GRID_WORLD = MPI_COMM_NULL; + DIAG_WORLD = MPI_COMM_NULL; + Parallel_Global::init_pools(SccsTest::pool_size, + SccsTest::pool_rank, + 1, + 1, + band_size, + band_rank, + band_group, + SccsTest::pool_size, + SccsTest::pool_rank, + pool); +#endif + testing::InitGoogleTest(&argc, argv); + const int status = RUN_ALL_TESTS(); +#ifdef __MPI + Parallel_Global::finalize_mpi(); +#endif + return status; +} diff --git a/source/source_hamilt/module_sccs/test/test_sccs_response.cpp b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp new file mode 100644 index 00000000000..0c36591345d --- /dev/null +++ b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp @@ -0,0 +1,190 @@ +#include "sccs_test.h" +#include "../sccs_response.h" + +#include "source_base/parallel_reduce.h" + +#include + +using SccsResponseTest = SccsTest::PwTest; + +TEST_F(SccsResponseTest, VacuumAndUniformDielectricAnalyticLimits) +{ + const double dielectrics[] = {1.0, 5.0}; + const std::vector charge = cosine_mode(0); + const std::vector density(basis.nrxx, 0.0); + const std::vector cold_start; + ModuleSccs::CavityParameters cavity; + cavity.density_min = 1e-4; + cavity.density_max = 5e-3; + ModuleSccs::PolarizationSolverParameters solver; + solver.tolerance_rms = 1e-12; + solver.tolerance_max = 1e-12; + for (double epsilon : dielectrics) + { + cavity.epsilon_bulk = epsilon; + ModuleSccs::SccsResponse response; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, response, error)) << error; + const double kernel = ModuleBase::FOUR_PI / (epsilon * tpiba * tpiba); + EXPECT_EQ(response.polarization.iterations, 1); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + const double expected = kernel * charge[ir]; + EXPECT_NEAR(response.polarization.potential[ir], expected, 1e-11); + EXPECT_DOUBLE_EQ(response.cavity_potential[ir], 0.0); + } + } +} + +TEST_F(SccsResponseTest, ManufacturedNonuniformDielectricSolution) +{ + const std::vector mode_x = cosine_mode(0); + const std::vector mode_y = cosine_mode(1); + ModuleSccs::CavityParameters cavity; + cavity.density_min = 1e-4; + cavity.density_max = 5e-3; + cavity.epsilon_bulk = 78.3; + std::vector density(basis.nrxx); + std::vector charge(basis.nrxx); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + density[ir] = 1e-3 + 2e-5 * mode_x[ir]; + ModuleSccs::CavityPoint point; + ASSERT_TRUE(ModuleSccs::evaluate_cavity(density[ir], cavity, point, error)); + // v=cos(ky), eps=eps(x): -div(eps grad v)/(4 pi)=eps k^2 cos(ky)/(4 pi). + charge[ir] = point.epsilon * tpiba * tpiba * mode_y[ir] / ModuleBase::FOUR_PI; + } + ModuleSccs::PolarizationSolverParameters solver; + solver.tolerance_rms = 1e-12; + solver.tolerance_max = 1e-12; + const std::vector cold_start; + ModuleSccs::SccsResponse response; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, response, error)) << error; + double maximum_error = 0.0; + for (int ir = 0; ir < basis.nrxx; ++ir) + { + const double difference = response.polarization.potential[ir] - mode_y[ir]; + const double magnitude = std::abs(difference); + maximum_error = std::max(maximum_error, magnitude); + EXPECT_NEAR(response.polarization.gradient[ir].x, 0.0, 1e-9); + EXPECT_NEAR(response.polarization.gradient[ir].z, 0.0, 1e-9); + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, maximum_error); + EXPECT_LT(maximum_error, 1e-9); + EXPECT_GT(response.polarization.iterations, 1); +} + +TEST_F(SccsResponseTest, ConvergenceFailureAndExplicitWarmStart) +{ + const std::vector mode_x = cosine_mode(0); + std::vector charge = cosine_mode(1); + std::vector density(basis.nrxx); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + density[ir] = 0.009 + 0.008 * mode_x[ir]; + charge[ir] *= 1e-3; + } + ModuleSccs::CavityParameters cavity; + cavity.density_min = 0.0024; + cavity.density_max = 0.0155; + cavity.epsilon_bulk = 78.3; + ModuleSccs::PolarizationSolverParameters solver; + solver.tolerance_rms = 1e-11; + solver.tolerance_max = 1e-10; + const std::vector cold_start; + ModuleSccs::SccsResponse response; + solver.max_iterations = 1; + response.polarization.potential.assign(1, 12.0); + EXPECT_FALSE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, response, error)); + EXPECT_NE(error.find("iteration limit"), std::string::npos); + EXPECT_DOUBLE_EQ(response.polarization.potential[0], 12.0); + solver.max_iterations = 200; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, response, error)) << error; + EXPECT_LE(response.polarization.residual_rms, solver.tolerance_rms); + EXPECT_LE(response.polarization.residual_max, solver.tolerance_max); + for (double& value : density) + { + value *= 1.001; + } + ModuleSccs::SccsResponse cold; + ModuleSccs::SccsResponse warm; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, cold, error)) << error; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, response.restart_potential, + basis, tpiba, warm, error)) << error; + EXPECT_TRUE(warm.polarization.warm_started); + EXPECT_LT(warm.polarization.iterations, cold.polarization.iterations); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + EXPECT_NEAR(warm.polarization.potential[ir], cold.polarization.potential[ir], 1e-9); + } +} + +TEST_F(SccsResponseTest, RankLocalInvalidParametersAndRestartReturnCollectively) +{ + ModuleSccs::CavityParameters cavity; + cavity.density_min = 1e-4; + cavity.density_max = 5e-3; + cavity.epsilon_bulk = 5.0; + ModuleSccs::PolarizationSolverParameters solver; + const std::vector density(basis.nrxx, 0.0); + const std::vector charge = cosine_mode(0); + const std::vector cold_start; + ModuleSccs::SccsResponse response; + response.polarization.potential.assign(1, 12.0); + if (basis.poolrank == 0) + { + solver.max_iterations = 0; + } + EXPECT_FALSE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, response, error)); + EXPECT_DOUBLE_EQ(response.polarization.potential[0], 12.0); + solver.max_iterations = 200; + std::vector initial(basis.nrxx, 0.0); + if (basis.poolrank == 0) + { + initial.pop_back(); + } + EXPECT_FALSE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, initial, + basis, tpiba, response, error)); + EXPECT_DOUBLE_EQ(response.polarization.potential[0], 12.0); +} + +TEST_F(SccsResponseTest, RequiresBothResidualTolerances) +{ + const std::vector mode_x = cosine_mode(0); + std::vector charge = cosine_mode(1); + std::vector density(basis.nrxx); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + density[ir] = 0.009 + 0.008 * mode_x[ir]; + charge[ir] *= 1e-3; + } + ModuleSccs::CavityParameters cavity; + cavity.density_min = 0.0024; + cavity.density_max = 0.0155; + cavity.epsilon_bulk = 78.3; + ModuleSccs::PolarizationSolverParameters solver; + solver.tolerance_rms = 1e-5; + solver.tolerance_max = 1e-5; + const std::vector cold_start; + ModuleSccs::SccsResponse loose; + ModuleSccs::SccsResponse tight_maximum; + ModuleSccs::SccsResponse tight_rms; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, loose, error)) << error; + solver.tolerance_max = 1e-11; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, tight_maximum, error)) << error; + solver.tolerance_rms = 1e-11; + solver.tolerance_max = 1.0; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, tight_rms, error)) << error; + EXPECT_LE(tight_maximum.polarization.residual_max, 1e-11); + EXPECT_LE(tight_rms.polarization.residual_rms, 1e-11); + EXPECT_GT(tight_maximum.polarization.iterations, loose.polarization.iterations); + EXPECT_GT(tight_rms.polarization.iterations, loose.polarization.iterations); +} From aa42152911dd26f6d187b3cbbfe2c7889d8b4c01 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 17:54:11 -0600 Subject: [PATCH 14/41] feat: add periodic SCCS energy functional --- source/Makefile.Objects | 1 + .../source_hamilt/module_sccs/CMakeLists.txt | 1 + .../module_sccs/sccs_functional.cpp | 101 ++++++++++++++++++ .../module_sccs/sccs_functional.h | 30 ++++++ .../module_sccs/test/CMakeLists.txt | 4 +- .../module_sccs/test/test_sccs_functional.cpp | 83 ++++++++++++++ 6 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 source/source_hamilt/module_sccs/sccs_functional.cpp create mode 100644 source/source_hamilt/module_sccs/sccs_functional.h create mode 100644 source/source_hamilt/module_sccs/test/test_sccs_functional.cpp diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 48f7665a723..1fa7d5f1ed9 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -613,6 +613,7 @@ OBJS_SURCHEM=surchem.o\ sol_force.o\ OBJS_SCCS=sccs_cavity.o\ + sccs_functional.o\ sccs_parameters.o\ sccs_ionic_charge.o\ sccs_pw_coulomb.o\ diff --git a/source/source_hamilt/module_sccs/CMakeLists.txt b/source/source_hamilt/module_sccs/CMakeLists.txt index 67bb6a65170..cd6c73238f3 100644 --- a/source/source_hamilt/module_sccs/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(sccs OBJECT sccs_cavity.cpp sccs_parameters.cpp + sccs_functional.cpp sccs_ionic_charge.cpp sccs_pw_coulomb.cpp sccs_response.cpp) diff --git a/source/source_hamilt/module_sccs/sccs_functional.cpp b/source/source_hamilt/module_sccs/sccs_functional.cpp new file mode 100644 index 00000000000..eceab111234 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_functional.cpp @@ -0,0 +1,101 @@ +#include "sccs_functional.h" +#include "sccs_pw_coulomb.h" + +#include "source_base/parallel_reduce.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_hamilt/module_xc/xc_functional.h" + +#include +#include +#include + +namespace ModuleSccs +{ +bool evaluate_functional(const std::vector& charge, + const SccsResponse& response, + const SccsConfig& config, + const ModulePW::PW_Basis& basis, + double tpiba, + FunctionalResult& result, + std::string& error) +{ + if (!validate_pw_grid(basis, tpiba, error) || !validate_grid_values(charge, basis, error) + || !validate_grid_values(response.polarization.potential, basis, error) + || !validate_grid_values(response.cavity_potential, basis, error) + || !validate_grid_values(response.solute, basis, error) + || !validate_grid_values(response.dsolute_drho, basis, error)) + { + return false; + } + double invalid = 0.0; + if (!validate_config(config, error)) + { + invalid = 1.0; + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + if (invalid != 0.0) + { + error = "SCCS functional requires valid parameters on every pool rank"; + return false; + } + const std::size_t size = charge.size(); + const double dv = basis.omega / basis.nxyz; + PeriodicCoulombOperator coulomb(basis, tpiba); + std::vector vacuum; + if (!coulomb.apply_potential(charge, vacuum, error)) + { + return false; + } + FunctionalResult candidate; + candidate.reaction_potential.resize(size); + candidate.electron_potential.resize(size); + for (std::size_t i = 0; i < size; ++i) + { + const double reaction = response.polarization.potential[i] - vacuum[i]; + candidate.reaction_potential[i] = reaction; + candidate.reaction_energy += 0.5 * charge[i] * reaction * dv; + candidate.electron_potential[i] = -reaction + response.cavity_potential[i]; + candidate.volume += response.solute[i] * dv; + } + Parallel_Reduce::reduce_pool(candidate.reaction_energy); + Parallel_Reduce::reduce_pool(candidate.volume); + candidate.volume_energy = config.pressure * candidate.volume; + + // Spectral surface derivative: dE/dn = (p - gamma div(grad s/|grad s|)) s'. + std::vector> solute_g(basis.npw); + basis.real2recip(response.solute.data(), solute_g.data()); + std::vector> unit_gradient(size); + XC_Functional::grad_rho(solute_g.data(), unit_gradient.data(), &basis, tpiba); + const double eta = config.surface_regularization; + for (std::size_t i = 0; i < size; ++i) + { + const double norm_squared = unit_gradient[i] * unit_gradient[i] + eta * eta; + const double norm = std::sqrt(norm_squared); + candidate.surface += (norm - eta) * dv; + unit_gradient[i] /= norm; + } + Parallel_Reduce::reduce_pool(candidate.surface); + candidate.surface_energy = config.surface_tension * candidate.surface; + std::vector divergence(size); + XC_Functional::grad_dot(unit_gradient.data(), divergence.data(), &basis, tpiba); + for (std::size_t i = 0; i < size; ++i) + { + const double nonel = (config.pressure - config.surface_tension * divergence[i]) + * response.dsolute_drho[i]; + candidate.electron_potential[i] += nonel; + } + if (!validate_grid_values(candidate.reaction_potential, basis, error) + || !validate_grid_values(candidate.electron_potential, basis, error)) + { + return false; + } + if (!std::isfinite(candidate.reaction_energy) || !std::isfinite(candidate.surface_energy) + || !std::isfinite(candidate.volume_energy)) + { + error = "SCCS functional energy is not finite"; + return false; + } + result = std::move(candidate); + return true; +} +} // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_functional.h b/source/source_hamilt/module_sccs/sccs_functional.h new file mode 100644 index 00000000000..7a2f5b70046 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_functional.h @@ -0,0 +1,30 @@ +#ifndef SCCS_FUNCTIONAL_H +#define SCCS_FUNCTIONAL_H + +#include "sccs_parameters.h" +#include "sccs_response.h" + +namespace ModuleSccs +{ +struct FunctionalResult +{ + double reaction_energy = 0.0; // Hartree + double surface_energy = 0.0; + double volume_energy = 0.0; + double surface = 0.0; // Bohr^2 + double volume = 0.0; // Bohr^3 + std::vector reaction_potential; // dielectric minus vacuum, Hartree + std::vector electron_potential; // derivative of all three energy terms +}; + +// Collective over the PW pool; failure leaves result unchanged. +bool evaluate_functional(const std::vector& charge, + const SccsResponse& response, + const SccsConfig& config, + const ModulePW::PW_Basis& basis, + double tpiba, + FunctionalResult& result, + std::string& error); +} // namespace ModuleSccs + +#endif diff --git a/source/source_hamilt/module_sccs/test/CMakeLists.txt b/source/source_hamilt/module_sccs/test/CMakeLists.txt index 926c7c0fa49..20b4b7b7b59 100644 --- a/source/source_hamilt/module_sccs/test/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/test/CMakeLists.txt @@ -5,7 +5,9 @@ AddTest(TARGET MODULE_HAMILT_sccs_parameters AddTest(TARGET MODULE_HAMILT_sccs_poisson LIBS parameter planewave device base - SOURCES test_sccs_pw_coulomb.cpp test_sccs_ionic_charge.cpp test_sccs_response.cpp + SOURCES test_sccs_pw_coulomb.cpp test_sccs_ionic_charge.cpp test_sccs_response.cpp test_sccs_functional.cpp + ../sccs_functional.cpp + ../sccs_parameters.cpp ../sccs_ionic_charge.cpp ../sccs_pw_coulomb.cpp ../sccs_response.cpp ../sccs_cavity.cpp ../../module_xc/xc_grad_utils.cpp) set_tests_properties(MODULE_HAMILT_sccs_poisson PROPERTIES diff --git a/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp b/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp new file mode 100644 index 00000000000..21487b422e6 --- /dev/null +++ b/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp @@ -0,0 +1,83 @@ +#include "sccs_test.h" +#include "../sccs_functional.h" +#include "source_base/parallel_reduce.h" + +using SccsFunctionalTest = SccsTest::PwTest; + +TEST_F(SccsFunctionalTest, UniformDielectricEnergyAndElectronDerivative) +{ + const std::vector mode = cosine_mode(0); + std::vector charge = mode; + for (double& value : charge) + { + value *= 1e-3; + } + ModuleSccs::SccsConfig config; + config.cavity.density_min = 1e-4; + config.cavity.density_max = 5e-3; + config.cavity.epsilon_bulk = 5.0; + config.surface_regularization = 1e-8; + ModuleSccs::PolarizationSolverParameters solver; + const std::vector density(basis.nrxx, 0.0); + const std::vector cold; + ModuleSccs::SccsResponse response; + ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, config.cavity, solver, cold, + basis, tpiba, response, error)) << error; + ModuleSccs::FunctionalResult result; + ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, result, error)) << error; + const double kernel = ModuleBase::FOUR_PI / (tpiba * tpiba); + const double expected = 0.25 * basis.omega * 1e-6 * kernel * (0.2 - 1.0); + EXPECT_NEAR(result.reaction_energy, expected, 1e-13); + EXPECT_DOUBLE_EQ(result.surface_energy, 0.0); + EXPECT_DOUBLE_EQ(result.volume_energy, 0.0); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + const double potential = 0.8 * kernel * charge[ir]; + EXPECT_NEAR(result.electron_potential[ir], potential, 1e-13); + } +} + +TEST_F(SccsFunctionalTest, NonElectrostaticDiscreteEnergyDerivative) +{ + ModuleSccs::SccsConfig config; + ASSERT_TRUE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum, config, error)); + config.surface_tension = 0.01; + config.pressure = -0.001; + config.surface_regularization = 0.02; + const std::vector mode = cosine_mode(0); + ModuleSccs::SccsResponse response; + response.solute.resize(basis.nrxx); + response.dsolute_drho.assign(basis.nrxx, 1.0); + response.polarization.potential.assign(basis.nrxx, 0.0); + response.cavity_potential.assign(basis.nrxx, 0.0); + const std::vector charge(basis.nrxx, 0.0); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + response.solute[ir] = 0.5 + 0.2 * mode[ir]; + } + ModuleSccs::FunctionalResult center; + ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, center, error)); + // Avoid cancellation of grid-integrated energies at excessively small steps. + const double step = 1e-5; + double predicted = 0.0; + for (int ir = 0; ir < basis.nrxx; ++ir) + { + predicted += center.electron_potential[ir] * mode[ir]; + response.solute[ir] += step * mode[ir]; + } + Parallel_Reduce::reduce_pool(predicted); + predicted *= basis.omega / basis.nxyz; + ModuleSccs::FunctionalResult plus; + ModuleSccs::FunctionalResult minus; + ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, plus, error)); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + response.solute[ir] -= 2.0 * step * mode[ir]; + } + ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, minus, error)); + const double fd = (plus.surface_energy + plus.volume_energy - minus.surface_energy - minus.volume_energy) + / (2.0 * step); + EXPECT_NEAR(fd, predicted, 1e-8); + const double expected_volume = 0.5 * basis.omega; + EXPECT_NEAR(center.volume, expected_volume, 1e-10); +} From 0276d3f12b7426eb9f58f454f4e64b7c5fcd9f6d Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 18:20:02 -0600 Subject: [PATCH 15/41] feat(sccs): integrate periodic neutral solvent into SCF --- docs/advanced/input_files/input-main.md | 95 +++++++- docs/parameters.yaml | 88 ++++++- source/Makefile.Objects | 2 + source/source_estate/CMakeLists.txt | 1 + source/source_estate/elecstate.h | 2 +- source/source_estate/elecstate_energy.cpp | 13 +- source/source_estate/module_pot/pot_base.h | 11 +- source/source_estate/module_pot/pot_sccs.cpp | 157 +++++++++++++ source/source_estate/module_pot/pot_sccs.h | 36 +++ .../module_pot/potential_new.cpp | 31 ++- .../source_estate/module_pot/potential_new.h | 8 +- .../module_pot/potential_types.cpp | 18 +- .../test/elecstate_energy_test.cpp | 31 ++- .../source_estate/test/potential_new_test.cpp | 2 +- source/source_estate/test/test_pot_sccs.cpp | 91 ++++++++ source/source_estate/write_elecstat_pot.cpp | 14 +- source/source_estate/write_elecstat_pot.h | 1 + .../module_sccs/test/CMakeLists.txt | 4 + source/source_io/CMakeLists.txt | 1 + .../source_io/module_ctrl/ctrl_output_fp.cpp | 2 + .../module_parameter/input_parameter.h | 13 +- .../module_parameter/read_inp_model.cpp | 16 +- .../module_parameter/read_inp_sccs.cpp | 220 ++++++++++++++++++ .../module_parameter/read_inp_sccs.h | 10 + .../source_io/module_parameter/read_input.h | 1 + source/source_io/test_serial/CMakeLists.txt | 3 +- .../test_serial/test_read_inp_sccs.cpp | 53 +++++ source/source_lcao/hamilt_lcao.cpp | 3 +- source/source_lcao/hamilt_lcao_factory.cpp | 4 +- source/source_pw/module_pwdft/hamilt_pw.cpp | 5 +- tests/01_PW/CASES_CPU.txt | 1 + tests/01_PW/scf_sccs_h2/INPUT | 22 ++ tests/01_PW/scf_sccs_h2/KPT | 4 + tests/01_PW/scf_sccs_h2/README.md | 5 + tests/01_PW/scf_sccs_h2/STRU | 20 ++ tests/01_PW/scf_sccs_h2/result.ref | 5 + tests/02_NAO_Gamma/CASES_CPU.txt | 1 + tests/02_NAO_Gamma/scf_sccs_h2/INPUT | 23 ++ tests/02_NAO_Gamma/scf_sccs_h2/KPT | 4 + tests/02_NAO_Gamma/scf_sccs_h2/README.md | 5 + tests/02_NAO_Gamma/scf_sccs_h2/STRU | 23 ++ tests/02_NAO_Gamma/scf_sccs_h2/result.ref | 5 + tests/integrate/tools/catch_properties.sh | 2 +- 43 files changed, 1012 insertions(+), 44 deletions(-) create mode 100644 source/source_estate/module_pot/pot_sccs.cpp create mode 100644 source/source_estate/module_pot/pot_sccs.h create mode 100644 source/source_estate/test/test_pot_sccs.cpp create mode 100644 source/source_io/module_parameter/read_inp_sccs.cpp create mode 100644 source/source_io/module_parameter/read_inp_sccs.h create mode 100644 source/source_io/test_serial/test_read_inp_sccs.cpp create mode 100644 tests/01_PW/scf_sccs_h2/INPUT create mode 100644 tests/01_PW/scf_sccs_h2/KPT create mode 100644 tests/01_PW/scf_sccs_h2/README.md create mode 100644 tests/01_PW/scf_sccs_h2/STRU create mode 100644 tests/01_PW/scf_sccs_h2/result.ref create mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/INPUT create mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/KPT create mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/README.md create mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/STRU create mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/result.ref diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 8a2f5813a3e..121e8f88580 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -531,6 +531,16 @@ - [cond\_nonlocal](#cond_nonlocal) - [Implicit solvation model](#implicit-solvation-model) - [imp\_sol](#imp_sol) + - [sccs\_preset](#sccs_preset) + - [sccs\_epsilon](#sccs_epsilon) + - [sccs\_rho\_min](#sccs_rho_min) + - [sccs\_rho\_max](#sccs_rho_max) + - [sccs\_gamma](#sccs_gamma) + - [sccs\_pressure](#sccs_pressure) + - [sccs\_maxiter](#sccs_maxiter) + - [sccs\_tol\_rms](#sccs_tol_rms) + - [sccs\_tol\_max](#sccs_tol_max) + - [sccs\_surface\_eta](#sccs_surface_eta) - [eb\_k](#eb_k) - [tau](#tau) - [sigma\_k](#sigma_k) @@ -4906,14 +4916,91 @@ ### imp_sol -- **Type**: Boolean -- **Description**: Calculate implicit solvation correction -- **Default**: False +- **Type**: Integer +- **Description**: Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without forces, stress, external fields or other correction models. +- **Default**: 0 + +### sccs_preset + +- **Type**: String +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: SCCS parameter preset. Allowed values: custom, vacuum, water-neutral, water-cation, water-anion. Non-custom presets override sccs_epsilon, sccs_rho_min, sccs_rho_max, sccs_gamma and sccs_pressure; solver controls and surface regularization remain user-controlled. +- **Default**: custom + +### sccs_epsilon + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Bulk dielectric constant >= 1 for the custom preset. Water presets use 78.3 and vacuum uses 1. +- **Default**: 78.3 + +### sccs_rho_min + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Lower electronic-density cavity threshold for the custom preset. Water-neutral/vacuum: 1e-4; water-cation: 2e-4; water-anion: 2.4e-3. +- **Default**: 1e-4 +- **Unit**: e/bohr^3 + +### sccs_rho_max + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Upper electronic-density cavity threshold for the custom preset. Water-neutral/vacuum: 5e-3; water-cation: 3.5e-3; water-anion: 1.55e-2. +- **Default**: 5e-3 +- **Unit**: e/bohr^3 + +### sccs_gamma + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Surface coefficient for the custom preset. Water-neutral: 47.9; water-cation: 5; water-anion/vacuum: 0. +- **Default**: 0.0 +- **Unit**: dyn/cm + +### sccs_pressure + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Volume coefficient for the custom preset. Water-neutral: -0.36; water-cation: 0.125; water-anion: 0.45; vacuum: 0. +- **Default**: 0.0 +- **Unit**: GPa + +### sccs_maxiter + +- **Type**: Integer +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Positive maximum number of SCCS sqrt-CG iterations. Failure to satisfy both residual tolerances terminates the calculation. +- **Default**: 200 + +### sccs_tol_rms + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Positive RMS charge-residual tolerance for the SCCS sqrt-CG solver. +- **Default**: 1e-10 +- **Unit**: e/bohr^3 + +### sccs_tol_max + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Positive maximum charge-residual tolerance for the SCCS sqrt-CG solver. +- **Default**: 1e-8 +- **Unit**: e/bohr^3 + +### sccs_surface_eta + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==2* +- **Description**: Positive regularization of the SCCS surface gradient norm. +- **Default**: 1e-8 +- **Unit**: bohr^-1 ### eb_k - **Type**: Real -- **Availability**: *[`imp_sol`](#imp_sol)==true* +- **Availability**: *[`imp_sol`](#imp_sol)==1* - **Description**: The relative permittivity of the bulk solvent, 80 for water - **Default**: 80 diff --git a/docs/parameters.yaml b/docs/parameters.yaml index db9f18a5e0c..fad4670ae1f 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -4332,12 +4332,92 @@ parameters: availability: "" - name: imp_sol category: Implicit solvation model - type: Boolean + type: Integer description: | - Calculate implicit solvation correction - default_value: "False" + Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without forces, stress, external fields or other correction models. + default_value: "0" unit: "" availability: "" + - name: sccs_preset + category: Implicit solvation model + type: String + description: | + SCCS parameter preset. Allowed values: custom, vacuum, water-neutral, water-cation, water-anion. Non-custom presets override sccs_epsilon, sccs_rho_min, sccs_rho_max, sccs_gamma and sccs_pressure; solver controls and surface regularization remain user-controlled. + default_value: custom + unit: "" + availability: imp_sol==2 + - name: sccs_epsilon + category: Implicit solvation model + type: Real + description: | + Bulk dielectric constant >= 1 for the custom preset. Water presets use 78.3 and vacuum uses 1. + default_value: "78.3" + unit: "" + availability: imp_sol==2 + - name: sccs_rho_min + category: Implicit solvation model + type: Real + description: | + Lower electronic-density cavity threshold for the custom preset. Water-neutral/vacuum: 1e-4; water-cation: 2e-4; water-anion: 2.4e-3. + default_value: "1e-4" + unit: e/bohr^3 + availability: imp_sol==2 + - name: sccs_rho_max + category: Implicit solvation model + type: Real + description: | + Upper electronic-density cavity threshold for the custom preset. Water-neutral/vacuum: 5e-3; water-cation: 3.5e-3; water-anion: 1.55e-2. + default_value: "5e-3" + unit: e/bohr^3 + availability: imp_sol==2 + - name: sccs_gamma + category: Implicit solvation model + type: Real + description: | + Surface coefficient for the custom preset. Water-neutral: 47.9; water-cation: 5; water-anion/vacuum: 0. + default_value: "0.0" + unit: dyn/cm + availability: imp_sol==2 + - name: sccs_pressure + category: Implicit solvation model + type: Real + description: | + Volume coefficient for the custom preset. Water-neutral: -0.36; water-cation: 0.125; water-anion: 0.45; vacuum: 0. + default_value: "0.0" + unit: GPa + availability: imp_sol==2 + - name: sccs_maxiter + category: Implicit solvation model + type: Integer + description: | + Positive maximum number of SCCS sqrt-CG iterations. Failure to satisfy both residual tolerances terminates the calculation. + default_value: "200" + unit: "" + availability: imp_sol==2 + - name: sccs_tol_rms + category: Implicit solvation model + type: Real + description: | + Positive RMS charge-residual tolerance for the SCCS sqrt-CG solver. + default_value: "1e-10" + unit: e/bohr^3 + availability: imp_sol==2 + - name: sccs_tol_max + category: Implicit solvation model + type: Real + description: | + Positive maximum charge-residual tolerance for the SCCS sqrt-CG solver. + default_value: "1e-8" + unit: e/bohr^3 + availability: imp_sol==2 + - name: sccs_surface_eta + category: Implicit solvation model + type: Real + description: | + Positive regularization of the SCCS surface gradient norm. + default_value: "1e-8" + unit: bohr^-1 + availability: imp_sol==2 - name: eb_k category: Implicit solvation model type: Real @@ -4345,7 +4425,7 @@ parameters: The relative permittivity of the bulk solvent, 80 for water default_value: "80" unit: "" - availability: imp_sol==true + availability: imp_sol==1 - name: tau category: Implicit solvation model type: Real diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 1fa7d5f1ed9..48bad78ceea 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -277,6 +277,7 @@ OBJS_ELECSTAT=elecstate.o\ pcc_0d.o\ pcc_2d.o\ pot_pcc.o\ + pot_sccs.o\ elecstate_op.o\ efield.o\ gatefield.o\ @@ -745,6 +746,7 @@ OBJS_IO=module_parameter/input_conv.o\ module_parameter/read_inp_bse.o\ module_parameter/read_inp_deepks.o\ module_parameter/read_inp_model.o\ + module_parameter/read_inp_sccs.o\ module_parameter/read_inp_postproc.o\ module_parameter/read_inp_exx_dftu.o\ module_parameter/read_inp_dfpt.o\ diff --git a/source/source_estate/CMakeLists.txt b/source/source_estate/CMakeLists.txt index c7b71d606c4..93bf96325cf 100644 --- a/source/source_estate/CMakeLists.txt +++ b/source/source_estate/CMakeLists.txt @@ -21,6 +21,7 @@ list(APPEND objects module_pot/pot_xc.cpp module_pot/pot_local.cpp module_pot/pot_pcc.cpp + module_pot/pot_sccs.cpp module_pot/potential_new.cpp module_pot/potential_types.cpp module_pot/pot_ml_exx.cpp diff --git a/source/source_estate/elecstate.h b/source/source_estate/elecstate.h index 64a616f5db1..c308533ea13 100644 --- a/source/source_estate/elecstate.h +++ b/source/source_estate/elecstate.h @@ -152,7 +152,7 @@ class ElecState /// @param dft_plus_u 0 disables the DFT+U energy term /// @param assume_isolated "makov-payne" adds the isolated-system correction void cal_energies(const int type, - const bool imp_sol, + const int imp_sol, const bool sc_mag_switch, const int dft_plus_u, const std::string& assume_isolated); diff --git a/source/source_estate/elecstate_energy.cpp b/source/source_estate/elecstate_energy.cpp index 043a7057d8c..af77832329c 100644 --- a/source/source_estate/elecstate_energy.cpp +++ b/source/source_estate/elecstate_energy.cpp @@ -316,7 +316,7 @@ void ElecState::cal_converged() * @param type: 2 means Kohn-Sham functional; */ void ElecState::cal_energies(const int type, - const bool imp_sol, + const int imp_sol, const bool sc_mag_switch, const int dft_plus_u, const std::string& assume_isolated) @@ -331,11 +331,20 @@ void ElecState::cal_energies(const int type, this->f_en.gatefield = get_etot_gatefield(); //! energy from implicit solvation model - if (imp_sol) + if (imp_sol == 1) { this->f_en.esol_el = get_solvent_model_Ael(); this->f_en.esol_cav = get_solvent_model_Acav(); } + else if (imp_sol == 2) + { + this->pot->get_solvation_energy(this->f_en.esol_el, this->f_en.esol_cav); + } + else + { + this->f_en.esol_el = 0.0; + this->f_en.esol_cav = 0.0; + } //! spin constrained energy if (sc_mag_switch) diff --git a/source/source_estate/module_pot/pot_base.h b/source/source_estate/module_pot/pot_base.h index aaa578bc5bf..f87b442df93 100644 --- a/source/source_estate/module_pot/pot_base.h +++ b/source/source_estate/module_pot/pot_base.h @@ -28,7 +28,16 @@ class PotBase virtual void cal_fixed_v(double* vl_pseudo){} - virtual double get_energy() const { return 0.0; } + virtual double get_energy() const { return 0.0; } + + // Solvation contributions in Ry, added once after the band-energy potential subtraction. + virtual void get_solvation_energy(double& electrostatic, double& non_electrostatic) const + { + electrostatic = 0.0; + non_electrostatic = 0.0; + } + // Electronic electrostatic correction only; excludes cavity/surface derivatives. + virtual const std::vector* solvent_electrostatic_potential() const { return nullptr; } bool fixed_mode = 0; bool dynamic_mode = 0; diff --git a/source/source_estate/module_pot/pot_sccs.cpp b/source/source_estate/module_pot/pot_sccs.cpp new file mode 100644 index 00000000000..104c6df0439 --- /dev/null +++ b/source/source_estate/module_pot/pot_sccs.cpp @@ -0,0 +1,157 @@ +#include "pot_sccs.h" + +#include "source_base/parallel_reduce.h" +#include "source_base/timer.h" +#include "source_base/tool_quit.h" +#include "source_cell/cell_tools.h" +#include "source_hamilt/module_sccs/sccs_functional.h" +#include "source_hamilt/module_sccs/sccs_ionic_charge.h" +#include "source_io/module_parameter/input_parameter.h" + +#include +#include + +namespace +{ +void require_valid_on_pool(bool valid, const std::string& error) +{ + double invalid = valid ? 0.0 : 1.0; + Parallel_Reduce::reduce_pool(invalid); + if (invalid != 0.0) + { + const std::string message = error.empty() ? "Invalid SCCS input on another pool rank" : error; + ModuleBase::WARNING_QUIT("PotSccs", message); + } +} +} + +namespace elecstate +{ +bool make_sccs_config_from_input(const Input_para& input, + ModuleSccs::SccsConfig& config, + ModuleSccs::PolarizationSolverParameters& solver, + std::string& error) +{ + ModuleSccs::Preset preset; + if (!ModuleSccs::parse_preset(input.sccs_preset, preset, error)) { return false; } + ModuleSccs::SccsConfig candidate; + if (preset == ModuleSccs::Preset::Custom) + { + candidate.cavity.density_min = input.sccs_rho_min; + candidate.cavity.density_max = input.sccs_rho_max; + candidate.cavity.epsilon_bulk = input.sccs_epsilon; + candidate.surface_tension = ModuleSccs::dyn_per_cm_to_hartree_per_bohr2(input.sccs_gamma); + candidate.pressure = ModuleSccs::gpa_to_hartree_per_bohr3(input.sccs_pressure); + } + else if (!ModuleSccs::make_sccs_config(preset, candidate, error)) { return false; } + candidate.surface_regularization = input.sccs_surface_eta; + if (!ModuleSccs::validate_config(candidate, error)) { return false; } + if (input.sccs_maxiter <= 0 || !std::isfinite(input.sccs_tol_rms) || input.sccs_tol_rms <= 0.0 + || !std::isfinite(input.sccs_tol_max) || input.sccs_tol_max <= 0.0) + { + error = "SCCS requires a positive iteration limit and finite positive residual tolerances"; + return false; + } + config = candidate; + solver.max_iterations = input.sccs_maxiter; + solver.tolerance_rms = input.sccs_tol_rms; + solver.tolerance_max = input.sccs_tol_max; + return true; +} + +PotSccs::PotSccs(const ModulePW::PW_Basis* basis, + const ModuleSccs::SccsConfig& config, + const ModuleSccs::PolarizationSolverParameters& solver) + : config_(config), solver_(solver) +{ + this->rho_basis_ = basis; + this->dynamic_mode = true; +} + +void PotSccs::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) +{ + ModuleBase::timer::start("PotSccs", "cal_v_eff"); + const bool storage_valid = charge != nullptr && cell != nullptr && this->rho_basis_ != nullptr; + require_valid_on_pool(storage_valid, "SCCS requires charge, cell and PW basis storage"); + const ModulePW::PW_Basis& basis = *this->rho_basis_; + const bool grid_valid = (charge->nspin == 1 || charge->nspin == 2) + && potential.nr == charge->nspin && potential.nc == basis.nrxx + && charge->rho != nullptr && cell->atoms != nullptr && cell->ntype > 0 + && std::isfinite(cell->lat0) && cell->lat0 > 0.0; + require_valid_on_pool(grid_valid, "SCCS requires initialized atom/density storage and an nspin=1/2 potential"); + bool density_valid = true; + for (int spin = 0; spin < charge->nspin; ++spin) + { + if (basis.nrxx > 0 && charge->rho[spin] == nullptr) { density_valid = false; } + } + require_valid_on_pool(density_valid, "SCCS charge density is not available"); + const std::vector atoms = unitcell::get_atom_data(cell->atoms, cell->ntype, cell->lat0); + const int atom_count = atoms.size(); + const bool count_valid = atom_count == cell->nat; + require_valid_on_pool(count_valid, "SCCS atom count does not match UnitCell"); + std::vector ions; + std::string error; + const bool ions_valid = ModuleSccs::gaussian_ionic_density(atoms, basis, cell->tpiba, + ModuleSccs::gaussian_ion_spread, ions, error); + require_valid_on_pool(ions_valid, error); + std::vector density(basis.nrxx, 0.0); + std::vector solute_charge(basis.nrxx); + double ionic_sum = 0.0; + double net_charge = 0.0; + for (int ir = 0; ir < basis.nrxx; ++ir) + { + for (int spin = 0; spin < charge->nspin; ++spin) { density[ir] += charge->rho[spin][ir]; } + solute_charge[ir] = ions[ir] - density[ir]; + ionic_sum += ions[ir]; + net_charge += solute_charge[ir]; + } + Parallel_Reduce::reduce_pool(ionic_sum); + Parallel_Reduce::reduce_pool(net_charge); + const double dv = basis.omega / basis.nxyz; + ionic_sum *= dv; + net_charge *= dv; + double expected_ionic_charge = 0.0; + for (const unitcell::AtomData& atom : atoms) { expected_ionic_charge += atom.valence_charge; } + const double normalization_error = ionic_sum - expected_ionic_charge; + const bool normalization_valid = std::isfinite(normalization_error) && std::abs(normalization_error) < 1e-6; + require_valid_on_pool(normalization_valid, "SCCS Gaussian ionic charge normalization failed"); + const bool neutral = std::isfinite(net_charge) && std::abs(net_charge) < 1e-6; + require_valid_on_pool(neutral, "Periodic SCCS currently requires a neutral cell"); + ModuleSccs::SccsResponse response; + const bool response_valid = ModuleSccs::solve_sccs_response(density, solute_charge, config_.cavity, + solver_, restart_potential_, basis, cell->tpiba, + response, error); + require_valid_on_pool(response_valid, error); + ModuleSccs::FunctionalResult functional; + const bool functional_valid = ModuleSccs::evaluate_functional(solute_charge, response, config_, basis, + cell->tpiba, functional, error); + require_valid_on_pool(functional_valid, error); + electrostatic_rydberg_ = 2.0 * functional.reaction_energy; + non_electrostatic_rydberg_ = 2.0 * (functional.surface_energy + functional.volume_energy); + electrostatic_potential_.resize(basis.nrxx); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + electrostatic_potential_[ir] = -2.0 * functional.reaction_potential[ir]; + const double value = 2.0 * functional.electron_potential[ir]; + for (int spin = 0; spin < charge->nspin; ++spin) { potential(spin, ir) += value; } + } + restart_potential_ = std::move(response.restart_potential); + ModuleBase::timer::end("PotSccs", "cal_v_eff"); +} + +double PotSccs::get_energy() const +{ + return electrostatic_rydberg_ + non_electrostatic_rydberg_; +} + +void PotSccs::get_solvation_energy(double& electrostatic, double& non_electrostatic) const +{ + electrostatic = electrostatic_rydberg_; + non_electrostatic = non_electrostatic_rydberg_; +} + +const std::vector* PotSccs::solvent_electrostatic_potential() const +{ + return &electrostatic_potential_; +} +} // namespace elecstate diff --git a/source/source_estate/module_pot/pot_sccs.h b/source/source_estate/module_pot/pot_sccs.h new file mode 100644 index 00000000000..327445b8a08 --- /dev/null +++ b/source/source_estate/module_pot/pot_sccs.h @@ -0,0 +1,36 @@ +#ifndef POT_SCCS_H +#define POT_SCCS_H + +#include "pot_base.h" +#include "source_hamilt/module_sccs/sccs_parameters.h" +#include "source_hamilt/module_sccs/sccs_response.h" + +struct Input_para; +namespace elecstate +{ +bool make_sccs_config_from_input(const Input_para& input, + ModuleSccs::SccsConfig& config, + ModuleSccs::PolarizationSolverParameters& solver, + std::string& error); + +class PotSccs : public PotBase +{ +public: + PotSccs(const ModulePW::PW_Basis* basis, + const ModuleSccs::SccsConfig& config, + const ModuleSccs::PolarizationSolverParameters& solver); + void cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) override; + double get_energy() const override; + void get_solvation_energy(double& electrostatic, double& non_electrostatic) const override; + const std::vector* solvent_electrostatic_potential() const override; + +private: + const ModuleSccs::SccsConfig config_; + const ModuleSccs::PolarizationSolverParameters solver_; + double electrostatic_rydberg_ = 0.0; + double non_electrostatic_rydberg_ = 0.0; + std::vector electrostatic_potential_; + std::vector restart_potential_; +}; +} // namespace elecstate +#endif diff --git a/source/source_estate/module_pot/potential_new.cpp b/source/source_estate/module_pot/potential_new.cpp index 201af74e730..0949100824e 100644 --- a/source/source_estate/module_pot/potential_new.cpp +++ b/source/source_estate/module_pot/potential_new.cpp @@ -15,6 +15,30 @@ namespace elecstate { +void Potential::get_solvation_energy(double& electrostatic, double& non_electrostatic) const +{ + electrostatic = 0.0; + non_electrostatic = 0.0; + for (const PotBase* component : this->components) + { + double el = 0.0; + double nonel = 0.0; + component->get_solvation_energy(el, nonel); + electrostatic += el; + non_electrostatic += nonel; + } +} + +const std::vector* Potential::solvent_electrostatic_potential() const +{ + for (const PotBase* component : this->components) + { + const std::vector* value = component->solvent_electrostatic_potential(); + if (value != nullptr) { return value; } + } + return nullptr; +} + const PotPcc* Potential::pcc_component() const { for (const PotBase* component : this->components) @@ -105,6 +129,11 @@ Potential::~Potential() } void Potential::pot_register(const std::vector& components_list) +{ + this->pot_register(components_list, PARAM.inp); +} + +void Potential::pot_register(const std::vector& components_list, const Input_para& input) { ModuleBase::TITLE("Potential", "pot_register"); // delete old components first. @@ -123,7 +152,7 @@ void Potential::pot_register(const std::vector& components_list) //--------------------------- for (auto comp: components_list) { - PotBase* tmp = this->get_pot_type(comp); + PotBase* tmp = this->get_pot_type(comp, input); this->components.push_back(tmp); } diff --git a/source/source_estate/module_pot/potential_new.h b/source/source_estate/module_pot/potential_new.h index 77cc8e210b5..fb72d1e5c86 100644 --- a/source/source_estate/module_pot/potential_new.h +++ b/source/source_estate/module_pot/potential_new.h @@ -10,6 +10,8 @@ #include #include +struct Input_para; + namespace elecstate { class PotPcc; @@ -73,12 +75,16 @@ class Potential : public PotBase void init_pot(const Charge* const chg); // initialize potential components before SCF void pot_register(const std::vector& components_list); + void pot_register(const std::vector& components_list, const Input_para& input); // update potential from current charge void update_from_charge(const Charge* const chg, const UnitCell* const ucell); // interface for SCF-converged, etxc vtxc for Energy, vnew for force_scc void get_vnew(const Charge* chg, ModuleBase::matrix& vnew); - PotBase* get_pot_type(const std::string& pot_type); + PotBase* get_pot_type(const std::string& pot_type, const Input_para& input); + + void get_solvation_energy(double& electrostatic, double& non_electrostatic) const override; + const std::vector* solvent_electrostatic_potential() const override; /// PCC results belong to this electronic state's registered potential. double pcc_energy_rydberg() const; diff --git a/source/source_estate/module_pot/potential_types.cpp b/source/source_estate/module_pot/potential_types.cpp index 748c2b03e99..7feab0cc7c5 100644 --- a/source/source_estate/module_pot/potential_types.cpp +++ b/source/source_estate/module_pot/potential_types.cpp @@ -3,6 +3,7 @@ #include "gatefield.h" #include "pot_local.h" #include "pot_pcc.h" +#include "pot_sccs.h" #include "pot_sep.h" #include "pot_surchem.hpp" #include "pot_xc.h" @@ -21,7 +22,7 @@ namespace elecstate { -PotBase* Potential::get_pot_type(const std::string& pot_type) +PotBase* Potential::get_pot_type(const std::string& pot_type, const Input_para& input) { ModuleBase::TITLE("Potential", "get_pot_type"); if (pot_type == "local") @@ -34,7 +35,7 @@ PotBase* Potential::get_pot_type(const std::string& pot_type) } else if (pot_type == "xc") { - return new PotXC(PARAM.globalv.domag, PARAM.globalv.domag_z, PARAM.inp.gga_grad, this->rho_basis_, this->etxc_, this->vtxc_, &(this->vofk_eff)); + return new PotXC(PARAM.globalv.domag, PARAM.globalv.domag_z, input.gga_grad, this->rho_basis_, this->etxc_, this->vtxc_, &(this->vofk_eff)); } else if (pot_type == "pcc") { @@ -42,16 +43,25 @@ PotBase* Potential::get_pot_type(const std::string& pot_type) } else if (pot_type == "pcc_2d") { - const int open_axis = PARAM.inp.pcc_2d_axis; + const int open_axis = input.pcc_2d_axis; return new PotPcc(this->rho_basis_, PotPcc::Dimension::slab, open_axis); } + else if (pot_type == "sccs") + { + ModuleSccs::SccsConfig config; + ModuleSccs::PolarizationSolverParameters solver; + std::string error; + const bool valid = make_sccs_config_from_input(input, config, solver, error); + if (!valid) { ModuleBase::WARNING_QUIT("Potential::get_pot_type", error); } + return new PotSccs(this->rho_basis_, config, solver); + } else if (pot_type == "surchem") { return new PotSurChem(this->rho_basis_, this->structure_factors_, this->v_eff_fixed.data(), this->solvent_); } else if (pot_type == "efield") { - return new PotEfield(this->rho_basis_, this->ucell_, this->solvent_, PARAM.inp.dip_cor_flag); + return new PotEfield(this->rho_basis_, this->ucell_, this->solvent_, input.dip_cor_flag); } else if (pot_type == "gatefield") { diff --git a/source/source_estate/test/elecstate_energy_test.cpp b/source/source_estate/test/elecstate_energy_test.cpp index 86f1334d7c3..6b98a8bd1cf 100644 --- a/source/source_estate/test/elecstate_energy_test.cpp +++ b/source/source_estate/test/elecstate_energy_test.cpp @@ -12,6 +12,17 @@ int XC_Functional::func_type = 1; bool XC_Functional::ked_flag = false; namespace elecstate { +Potential::~Potential() {} +void Potential::cal_v_eff(const Charge*, const UnitCell*, ModuleBase::matrix&) {} +void Potential::cal_fixed_v(double*) {} +const std::vector* Potential::solvent_electrostatic_potential() const { return nullptr; } + +void Potential::get_solvation_energy(double& el, double& cav) const +{ + el = 0.8; + cav = 0.9; +} + double Potential::pcc_energy_rydberg() const { return 0.25; @@ -80,7 +91,7 @@ class ElecStateEnergyTest : public ::testing::Test /// explicitly, so the fixture owns them instead of staging them in the /// global parameter singleton. The values mirror the Input_para defaults, /// except sc_mag_switch, which the original fixture turned on. - bool imp_sol = false; + int imp_sol = 0; bool sc_mag_switch = true; int dft_plus_u = 0; std::string assume_isolated = "none"; @@ -288,3 +299,21 @@ TEST_F(ElecStateEnergyTest, CalBandgapUpDwBoundaryConditions) EXPECT_DOUBLE_EQ(elecstate->bandgap_up, 5.0); EXPECT_DOUBLE_EQ(elecstate->bandgap_dw, 5.0); } + +TEST_F(ElecStateEnergyTest, AddsSccsEnergyOnceAndResetsVacuum) +{ + elecstate::Potential potential; + elecstate->pot = &potential; + elecstate->f_en.deband = 0.1; + imp_sol = 2; + elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); + EXPECT_DOUBLE_EQ(elecstate->f_en.esol_el, 0.8); + EXPECT_DOUBLE_EQ(elecstate->f_en.esol_cav, 0.9); + EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 2.4); + imp_sol = 0; + elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); + EXPECT_DOUBLE_EQ(elecstate->f_en.esol_el, 0.0); + EXPECT_DOUBLE_EQ(elecstate->f_en.esol_cav, 0.0); + EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.7); + elecstate->pot = nullptr; +} diff --git a/source/source_estate/test/potential_new_test.cpp b/source/source_estate/test/potential_new_test.cpp index 6dc01b9f321..2938bc0e8b2 100644 --- a/source/source_estate/test/potential_new_test.cpp +++ b/source/source_estate/test/potential_new_test.cpp @@ -153,7 +153,7 @@ int MockPotComponent::destroyed = 0; int MockPotComponent::fixed_calls = 0; int MockPotComponent::dynamic_calls = 0; -PotBase* Potential::get_pot_type(const std::string& pot_type) +PotBase* Potential::get_pot_type(const std::string& pot_type, const Input_para&) { const int grid_size = this->get_rho_basis() == nullptr ? 0 : this->get_rho_basis()->nrxx; return new MockPotComponent(pot_type, grid_size); diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp new file mode 100644 index 00000000000..4ad74fc3c9f --- /dev/null +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -0,0 +1,91 @@ +#include "source_estate/module_pot/pot_sccs.h" +#include "source_io/module_parameter/input_parameter.h" +#include "source_hamilt/module_sccs/test/sccs_test.h" + +// Public storage fixtures: no INPUT initialization or privately owned atom maps. +UnitCell::UnitCell() {} +UnitCell::~UnitCell() {} +Magnetism::Magnetism() {} +Magnetism::~Magnetism() {} +SepPot::SepPot() {} +SepPot::~SepPot() {} +Sep_Cell::Sep_Cell() noexcept {} +Sep_Cell::~Sep_Cell() noexcept {} +Charge::Charge() {} +Charge::~Charge() {} + +using PotSccsTest = SccsTest::PwTest; + +TEST_F(PotSccsTest, IndependentInstancesAndSpinChannels) +{ + UnitCell cell; + Atom atom; + cell.lat0 = length; + cell.tpiba = tpiba; + cell.omega = basis.omega; + cell.ntype = 1; + cell.nat = 1; + cell.atoms = &atom; + atom.na = 1; + atom.ncpp.zv = 1.0; + atom.tau = {ModuleBase::Vector3(0.25, 0.25, 0.25)}; + const double density_value = 1.0 / basis.omega; + std::vector density(basis.nrxx, density_value); + double* channels[] = {density.data(), density.data()}; + Charge charge; + charge.nspin = 1; + charge.rho = channels; + Input_para input; + ModuleSccs::SccsConfig config; + ModuleSccs::PolarizationSolverParameters solver; + ASSERT_TRUE(elecstate::make_sccs_config_from_input(input, config, solver, error)); + config.cavity.epsilon_bulk = 5.0; + elecstate::PotSccs first(&basis, config, solver); + config.cavity.epsilon_bulk = 1.0; + elecstate::PotSccs vacuum(&basis, config, solver); + EXPECT_DOUBLE_EQ(first.get_energy(), 0.0); + ModuleBase::matrix potential(1, basis.nrxx); + first.cal_v_eff(&charge, &cell, potential); + const double first_energy = first.get_energy(); + EXPECT_LT(first_energy, 0.0); + double el = 0.0; + double cav = 0.0; + first.get_solvation_energy(el, cav); + EXPECT_DOUBLE_EQ(el, first_energy); + EXPECT_DOUBLE_EQ(cav, 0.0); + ModuleBase::matrix zero(1, basis.nrxx); + vacuum.cal_v_eff(&charge, &cell, zero); + EXPECT_NEAR(vacuum.get_energy(), 0.0, 1e-12); + EXPECT_DOUBLE_EQ(first.get_energy(), first_energy); + for (int ir = 0; ir < basis.nrxx; ++ir) { EXPECT_NEAR(zero(0, ir), 0.0, 1e-11); } + for (double& value : density) { value *= 0.5; } + charge.nspin = 2; + ModuleBase::matrix spin_potential(2, basis.nrxx); + first.cal_v_eff(&charge, &cell, spin_potential); + EXPECT_NEAR(first.get_energy(), first_energy, 1e-12); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + EXPECT_NEAR(spin_potential(0, ir), potential(0, ir), 1e-11); + EXPECT_DOUBLE_EQ(spin_potential(0, ir), spin_potential(1, ir)); + } +} + +TEST_F(PotSccsTest, PresetConfigurationIsExplicit) +{ + Input_para input; + input.sccs_preset = "water-neutral"; + input.sccs_epsilon = 2.0; + input.sccs_maxiter = 42; + input.sccs_surface_eta = 1e-6; + ModuleSccs::SccsConfig config; + ModuleSccs::PolarizationSolverParameters solver; + ASSERT_TRUE(elecstate::make_sccs_config_from_input(input, config, solver, error)); + EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 78.3); + EXPECT_DOUBLE_EQ(config.cavity.density_min, 1e-4); + EXPECT_LT(config.pressure, 0.0); + EXPECT_DOUBLE_EQ(config.surface_regularization, 1e-6); + EXPECT_EQ(solver.max_iterations, 42); + input.sccs_maxiter = 0; + EXPECT_FALSE(elecstate::make_sccs_config_from_input(input, config, solver, error)); + EXPECT_EQ(solver.max_iterations, 42); +} diff --git a/source/source_estate/write_elecstat_pot.cpp b/source/source_estate/write_elecstat_pot.cpp index f3076de2dbf..a1db57911cf 100644 --- a/source/source_estate/write_elecstat_pot.cpp +++ b/source/source_estate/write_elecstat_pot.cpp @@ -23,6 +23,7 @@ void write_elecstat_pot( const double* v_eff, const surchem& solvent, const std::vector* pcc_potential, + const std::vector* solvent_potential, const int precision) { ModuleBase::TITLE("ModuleIO", "write_elecstat_pot"); @@ -34,10 +35,15 @@ void write_elecstat_pot( ModuleBase::WARNING_QUIT("write_elecstat_pot", "PCC potential does not match the output grid"); } + if (solvent_potential != nullptr && solvent_potential->size() != static_cast(rho_basis->nrxx)) + { + ModuleBase::WARNING_QUIT("write_elecstat_pot", "Solvent potential does not match the output grid"); + } + const int nspin = PARAM.inp.nspin; const int efield = PARAM.inp.efield_flag; const int dip_corr = PARAM.inp.dip_cor_flag; - const bool imp_sol = PARAM.inp.imp_sol; + const int imp_sol = PARAM.inp.imp_sol; //========================================== // Hartree potential @@ -75,7 +81,11 @@ void write_elecstat_pot( { v_elecstat[ir] += (*pcc_potential)[ir]; } - if(imp_sol == true) + if (solvent_potential != nullptr) + { + v_elecstat[ir] += (*solvent_potential)[ir]; + } + if (imp_sol == 1) { v_elecstat[ir] += solvent.delta_phi[ir]; } diff --git a/source/source_estate/write_elecstat_pot.h b/source/source_estate/write_elecstat_pot.h index 2ad8452ed43..f21239935b4 100644 --- a/source/source_estate/write_elecstat_pot.h +++ b/source/source_estate/write_elecstat_pot.h @@ -34,6 +34,7 @@ void write_elecstat_pot( const double* v_eff_fixed, const surchem& solvent, const std::vector* pcc_potential, + const std::vector* solvent_potential, const int precision); } // namespace ModuleIO diff --git a/source/source_hamilt/module_sccs/test/CMakeLists.txt b/source/source_hamilt/module_sccs/test/CMakeLists.txt index 20b4b7b7b59..fe8c5cda2eb 100644 --- a/source/source_hamilt/module_sccs/test/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/test/CMakeLists.txt @@ -6,6 +6,10 @@ AddTest(TARGET MODULE_HAMILT_sccs_parameters AddTest(TARGET MODULE_HAMILT_sccs_poisson LIBS parameter planewave device base SOURCES test_sccs_pw_coulomb.cpp test_sccs_ionic_charge.cpp test_sccs_response.cpp test_sccs_functional.cpp + ../../../source_estate/test/test_pot_sccs.cpp + ../../../source_estate/module_pot/pot_sccs.cpp + ../../../source_cell/cell_tools.cpp ../../../source_cell/atom_spec.cpp + ../../../source_cell/atom_pseudo.cpp ../../../source_cell/pseudo.cpp ../sccs_functional.cpp ../sccs_parameters.cpp ../sccs_ionic_charge.cpp ../sccs_pw_coulomb.cpp ../sccs_response.cpp ../sccs_cavity.cpp diff --git a/source/source_io/CMakeLists.txt b/source/source_io/CMakeLists.txt index 9c5411a589f..45e3d0572fb 100644 --- a/source/source_io/CMakeLists.txt +++ b/source/source_io/CMakeLists.txt @@ -125,6 +125,7 @@ add_library( module_parameter/read_inp_dfpt.cpp module_parameter/read_inp_deepks.cpp module_parameter/read_inp_model.cpp + module_parameter/read_inp_sccs.cpp module_parameter/read_inp_postproc.cpp module_parameter/read_inp_exx_dftu.cpp module_parameter/read_inp_other.cpp diff --git a/source/source_io/module_ctrl/ctrl_output_fp.cpp b/source/source_io/module_ctrl/ctrl_output_fp.cpp index 09a26c15a74..1d033a50842 100644 --- a/source/source_io/module_ctrl/ctrl_output_fp.cpp +++ b/source/source_io/module_ctrl/ctrl_output_fp.cpp @@ -150,6 +150,7 @@ void ctrl_output_fp(UnitCell& ucell, fn += geom_block + ".cube"; const std::vector* pcc_potential = pelec->pot->pcc_electron_potential(); + const std::vector* solvent_potential = pelec->pot->solvent_electrostatic_potential(); ModuleIO::write_elecstat_pot( #ifdef __MPI pw_big->bz, @@ -163,6 +164,7 @@ void ctrl_output_fp(UnitCell& ucell, pelec->pot->get_fixed_v(), solvent, pcc_potential, + solvent_potential, inp.out_pot[1]); } diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index 16f8eab42c2..cf09cb16caa 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -570,12 +570,21 @@ struct Input_para double block_height = 0.1; ///< height of the block // implicit solvation model Menglin Sun added on 2022-04-04 - bool imp_sol = false; ///< true: implicit solvation correction; false: - ///< vacuum calculation(default) + int imp_sol = 0; ///< 0: vacuum, 1: legacy solvation, 2: SCCS double eb_k = 80; ///< the relative permittivity of the bulk solvent double tau = 1.0798e-05; ///< the effective surface tension parameter double sigma_k = 0.6; ///< the width of the diffuse cavity double nc_k = 0.00037; ///< the cut-off charge density + std::string sccs_preset = "custom"; + double sccs_epsilon = 78.3; + double sccs_rho_min = 1e-4; + double sccs_rho_max = 5e-3; + double sccs_gamma = 0.0; // dyn/cm + double sccs_pressure = 0.0; // GPa + int sccs_maxiter = 200; + double sccs_tol_rms = 1e-10; + double sccs_tol_max = 1e-8; + double sccs_surface_eta = 1e-8; // Bohr^-1 // ============== #Parameters (14.vdW Correction) =========================== // ========================================================== diff --git a/source/source_io/module_parameter/read_inp_model.cpp b/source/source_io/module_parameter/read_inp_model.cpp index aa1d232feb5..4a60a73ce91 100644 --- a/source/source_io/module_parameter/read_inp_model.cpp +++ b/source/source_io/module_parameter/read_inp_model.cpp @@ -181,18 +181,8 @@ void ReadInput::item_model() this->add_item(item); } - // imlicit_solvation - { - Input_Item item("imp_sol"); - item.annotation = "calculate implicit solvation correction or not"; - item.category = "Implicit solvation model"; - item.type = "Boolean"; - item.description = "Calculate implicit solvation correction"; - item.default_value = "False"; - item.unit = ""; - read_sync_bool(input.imp_sol); - this->add_item(item); - } + // Implicit solvation: preserve the existing model parameter ordering. + this->item_sccs(); { Input_Item item("eb_k"); item.annotation = "the relative permittivity of the bulk solvent"; @@ -201,7 +191,7 @@ void ReadInput::item_model() item.description = "The relative permittivity of the bulk solvent, 80 for water"; item.default_value = "80"; item.unit = ""; - item.set_availability("imp_sol==true"); + item.set_availability("imp_sol==1"); read_sync_double(input.eb_k); this->add_item(item); } diff --git a/source/source_io/module_parameter/read_inp_sccs.cpp b/source/source_io/module_parameter/read_inp_sccs.cpp new file mode 100644 index 00000000000..500c19e2ce0 --- /dev/null +++ b/source/source_io/module_parameter/read_inp_sccs.cpp @@ -0,0 +1,220 @@ +#include "read_inp_sccs.h" +#include "read_input.h" +#include "read_input_tool.h" +#include "source_base/tool_quit.h" + +#include +#include +#include + +namespace ModuleIO +{ +bool parse_solvation_model(const std::string& value, int& model, std::string& error) +{ + error.clear(); + std::string normalized = value; + for (char& c : normalized) + { + const unsigned char byte = static_cast(c); + const int lower = std::tolower(byte); + c = static_cast(lower); + } + const std::vector yes = {"true", "1", "t", "yes", "y", "on", ".true."}; + const std::vector no = {"false", "0", "f", "no", "n", "off", ".false."}; + if (normalized == "2") { model = 2; } + else if (std::find(yes.begin(), yes.end(), normalized) != yes.end()) { model = 1; } + else if (std::find(no.begin(), no.end(), normalized) != no.end()) { model = 0; } + else + { + error = "imp_sol must be 0, 1, 2 or a legacy Boolean value"; + return false; + } + return true; +} + +bool validate_sccs_input(const Input_para& input, std::string& error) +{ + error.clear(); + if (input.imp_sol < 0 || input.imp_sol > 2) + { + error = "imp_sol must be 0, 1 or 2"; + return false; + } + if (input.imp_sol != 2) { return true; } + if (input.assume_isolated != "none") { error = "SCCS currently requires assume_isolated none"; } + else if (input.device != "cpu" || input.esolver_type != "ksdft") { error = "SCCS requires CPU KS-DFT"; } + else if (input.basis_type != "pw" && input.basis_type != "lcao") { error = "SCCS requires basis_type pw or lcao"; } + else if (input.calculation != "scf" || input.cal_force || input.cal_stress) + { error = "SCCS currently supports SCF energies without forces or stress"; } + else if (input.nspin != 1 && input.nspin != 2) { error = "SCCS requires nspin 1 or 2"; } + else if (input.efield_flag || input.gate_flag || input.dfthalf_type != 0 + || input.deepks_scf || input.deepks_out_labels || input.deepks_bandgap + || input.deepks_v_delta || input.dm_to_rho) + { error = "SCCS does not support electric/gate fields, DFT-1/2, DeePKS or dm_to_rho"; } + else if (!input.vl_in_h || !input.vion_in_h || !input.vh_in_h) + { error = "SCCS requires the local ionic and Hartree potentials in the Hamiltonian"; } + if (!error.empty()) { return false; } + const std::vector presets = {"custom", "vacuum", "water-neutral", "water-cation", "water-anion"}; + if (std::find(presets.begin(), presets.end(), input.sccs_preset) == presets.end()) + { + error = "Unknown sccs_preset"; + return false; + } + if (input.sccs_maxiter <= 0 || !std::isfinite(input.sccs_epsilon) || input.sccs_epsilon < 1.0 + || !std::isfinite(input.sccs_rho_min) || !std::isfinite(input.sccs_rho_max) + || input.sccs_rho_min <= 0.0 || input.sccs_rho_max <= input.sccs_rho_min + || !std::isfinite(input.sccs_gamma) || !std::isfinite(input.sccs_pressure) + || !std::isfinite(input.sccs_tol_rms) || input.sccs_tol_rms <= 0.0 + || !std::isfinite(input.sccs_tol_max) || input.sccs_tol_max <= 0.0 + || !std::isfinite(input.sccs_surface_eta) || input.sccs_surface_eta <= 0.0) + { + error = "Invalid SCCS numerical parameters"; + return false; + } + return true; +} + +void ReadInput::item_sccs() +{ + { + Input_Item item("imp_sol"); + item.annotation = "implicit solvent model"; + item.category = "Implicit solvation model"; + item.type = "Integer"; + item.description = "Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without forces, stress, external fields or other correction models."; + item.default_value = "0"; + item.read_value = [](const Input_Item& item, Parameter& para) { + std::string error; + const bool valid = parse_solvation_model(item.str_values[0], para.input.imp_sol, error); + if (!valid) { ModuleBase::WARNING_QUIT("ReadInput", error); } + }; + sync_int(input.imp_sol); + item.check_value = [](const Input_Item&, const Parameter& para) { + std::string error; + const bool valid = validate_sccs_input(para.input, error); + if (!valid) { ModuleBase::WARNING_QUIT("ReadInput", error); } + }; + this->add_item(item); + } + { + Input_Item item("sccs_preset"); + item.annotation = "SCCS parameter preset"; + item.category = "Implicit solvation model"; + item.type = "String"; + item.description = "SCCS parameter preset. Allowed values: custom, vacuum, water-neutral, water-cation, water-anion. Non-custom presets override sccs_epsilon, sccs_rho_min, sccs_rho_max, sccs_gamma and sccs_pressure; solver controls and surface regularization remain user-controlled."; + item.default_value = "custom"; + item.unit = ""; + item.set_availability("imp_sol==2"); + read_sync_string(input.sccs_preset); + this->add_item(item); + } + { + Input_Item item("sccs_epsilon"); + item.annotation = "Bulk dielectric constant >= 1 for the custom preset"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Bulk dielectric constant >= 1 for the custom preset. Water presets use 78.3 and vacuum uses 1."; + item.default_value = "78.3"; + item.unit = ""; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_epsilon); + this->add_item(item); + } + { + Input_Item item("sccs_rho_min"); + item.annotation = "Lower electronic-density cavity threshold for the custom preset"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Lower electronic-density cavity threshold for the custom preset. Water-neutral/vacuum: 1e-4; water-cation: 2e-4; water-anion: 2.4e-3."; + item.default_value = "1e-4"; + item.unit = "e/bohr^3"; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_rho_min); + this->add_item(item); + } + { + Input_Item item("sccs_rho_max"); + item.annotation = "Upper electronic-density cavity threshold for the custom preset"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Upper electronic-density cavity threshold for the custom preset. Water-neutral/vacuum: 5e-3; water-cation: 3.5e-3; water-anion: 1.55e-2."; + item.default_value = "5e-3"; + item.unit = "e/bohr^3"; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_rho_max); + this->add_item(item); + } + { + Input_Item item("sccs_gamma"); + item.annotation = "Surface coefficient for the custom preset"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Surface coefficient for the custom preset. Water-neutral: 47.9; water-cation: 5; water-anion/vacuum: 0."; + item.default_value = "0.0"; + item.unit = "dyn/cm"; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_gamma); + this->add_item(item); + } + { + Input_Item item("sccs_pressure"); + item.annotation = "Volume coefficient for the custom preset"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Volume coefficient for the custom preset. Water-neutral: -0.36; water-cation: 0.125; water-anion: 0.45; vacuum: 0."; + item.default_value = "0.0"; + item.unit = "GPa"; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_pressure); + this->add_item(item); + } + { + Input_Item item("sccs_maxiter"); + item.annotation = "Positive maximum number of SCCS sqrt-CG iterations"; + item.category = "Implicit solvation model"; + item.type = "Integer"; + item.description = "Positive maximum number of SCCS sqrt-CG iterations. Failure to satisfy both residual tolerances terminates the calculation."; + item.default_value = "200"; + item.unit = ""; + item.set_availability("imp_sol==2"); + read_sync_int(input.sccs_maxiter); + this->add_item(item); + } + { + Input_Item item("sccs_tol_rms"); + item.annotation = "Positive RMS charge-residual tolerance for the SCCS sqrt-CG solver"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Positive RMS charge-residual tolerance for the SCCS sqrt-CG solver."; + item.default_value = "1e-10"; + item.unit = "e/bohr^3"; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_tol_rms); + this->add_item(item); + } + { + Input_Item item("sccs_tol_max"); + item.annotation = "Positive maximum charge-residual tolerance for the SCCS sqrt-CG solver"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Positive maximum charge-residual tolerance for the SCCS sqrt-CG solver."; + item.default_value = "1e-8"; + item.unit = "e/bohr^3"; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_tol_max); + this->add_item(item); + } + { + Input_Item item("sccs_surface_eta"); + item.annotation = "Positive regularization of the SCCS surface gradient norm"; + item.category = "Implicit solvation model"; + item.type = "Real"; + item.description = "Positive regularization of the SCCS surface gradient norm."; + item.default_value = "1e-8"; + item.unit = "bohr^-1"; + item.set_availability("imp_sol==2"); + read_sync_double(input.sccs_surface_eta); + this->add_item(item); + } +} +} // namespace ModuleIO diff --git a/source/source_io/module_parameter/read_inp_sccs.h b/source/source_io/module_parameter/read_inp_sccs.h new file mode 100644 index 00000000000..8736ed5768e --- /dev/null +++ b/source/source_io/module_parameter/read_inp_sccs.h @@ -0,0 +1,10 @@ +#ifndef READ_INP_SCCS_H +#define READ_INP_SCCS_H +#include +struct Input_para; +namespace ModuleIO +{ +bool parse_solvation_model(const std::string& value, int& model, std::string& error); +bool validate_sccs_input(const Input_para& input, std::string& error); +} +#endif diff --git a/source/source_io/module_parameter/read_input.h b/source/source_io/module_parameter/read_input.h index 37484a21247..02d6a95d71c 100644 --- a/source/source_io/module_parameter/read_input.h +++ b/source/source_io/module_parameter/read_input.h @@ -139,6 +139,7 @@ class ReadInput void item_postprocess(); // items for some models void item_model(); + void item_sccs(); // items for exx void item_exx(); // items for dft+u diff --git a/source/source_io/test_serial/CMakeLists.txt b/source/source_io/test_serial/CMakeLists.txt index 3238fb743d4..0201fb14e01 100644 --- a/source/source_io/test_serial/CMakeLists.txt +++ b/source/source_io/test_serial/CMakeLists.txt @@ -17,6 +17,7 @@ add_library( ../module_parameter/read_inp_dfpt.cpp ../module_parameter/read_inp_deepks.cpp ../module_parameter/read_inp_model.cpp + ../module_parameter/read_inp_sccs.cpp ../module_parameter/read_inp_postproc.cpp ../module_parameter/read_inp_exx_dftu.cpp ../module_parameter/read_inp_other.cpp @@ -40,7 +41,7 @@ AddTest( AddTest( TARGET MODULE_IO_read_item_serial LIBS parameter base device io_input_serial - SOURCES read_input_item_test.cpp + SOURCES read_input_item_test.cpp test_read_inp_sccs.cpp ) AddTest( diff --git a/source/source_io/test_serial/test_read_inp_sccs.cpp b/source/source_io/test_serial/test_read_inp_sccs.cpp new file mode 100644 index 00000000000..ae787a82180 --- /dev/null +++ b/source/source_io/test_serial/test_read_inp_sccs.cpp @@ -0,0 +1,53 @@ +#include "source_io/module_parameter/read_inp_sccs.h" +#include "source_io/module_parameter/input_parameter.h" +#include + +TEST(ReadInpSccs, LegacyBooleansAndModelSelection) +{ + std::string error; + const std::string yes[] = {"true", "TRUE", "yes", "On", ".true.", "1", "t", "y"}; + const std::string no[] = {"false", "FALSE", "no", "Off", ".false.", "0", "f", "n"}; + int model = 0; + for (const std::string& value : yes) + { + ASSERT_TRUE(ModuleIO::parse_solvation_model(value, model, error)); + EXPECT_EQ(model, 1); + } + for (const std::string& value : no) + { + ASSERT_TRUE(ModuleIO::parse_solvation_model(value, model, error)); + EXPECT_EQ(model, 0); + } + ASSERT_TRUE(ModuleIO::parse_solvation_model("2", model, error)); + EXPECT_EQ(model, 2); + EXPECT_FALSE(ModuleIO::parse_solvation_model("3", model, error)); + EXPECT_EQ(model, 2); + EXPECT_FALSE(ModuleIO::parse_solvation_model("2.0", model, error)); +} + +TEST(ReadInpSccs, SupportedScopeAndNumericalValidation) +{ + Input_para input; + input.imp_sol = 2; + input.device = "cpu"; + input.basis_type = "pw"; + std::string error; + ASSERT_TRUE(ModuleIO::validate_sccs_input(input, error)) << error; + input.cal_force = true; + EXPECT_FALSE(ModuleIO::validate_sccs_input(input, error)); + input.cal_force = false; + input.assume_isolated = "pcc_0d"; + EXPECT_FALSE(ModuleIO::validate_sccs_input(input, error)); + input.assume_isolated = "none"; + input.sccs_rho_min = input.sccs_rho_max; + EXPECT_FALSE(ModuleIO::validate_sccs_input(input, error)); + input.sccs_rho_min = 1e-4; + input.sccs_preset = "unknown"; + EXPECT_FALSE(ModuleIO::validate_sccs_input(input, error)); + input.sccs_preset = "vacuum"; + input.basis_type = "lcao"; + EXPECT_TRUE(ModuleIO::validate_sccs_input(input, error)); + input.imp_sol = 1; + input.sccs_preset = "unused"; + EXPECT_TRUE(ModuleIO::validate_sccs_input(input, error)); +} diff --git a/source/source_lcao/hamilt_lcao.cpp b/source/source_lcao/hamilt_lcao.cpp index 6e07150bda1..22cfa837430 100644 --- a/source/source_lcao/hamilt_lcao.cpp +++ b/source/source_lcao/hamilt_lcao.cpp @@ -87,7 +87,8 @@ HamiltLCAO::HamiltLCAO(const UnitCell& ucell, if (inp.vion_in_h) { pot_register_in.push_back("local"); } if (inp.vh_in_h) { pot_register_in.push_back("hartree"); } pot_register_in.push_back("xc"); - if (inp.imp_sol) { pot_register_in.push_back("surchem"); } + if (inp.imp_sol == 1) { pot_register_in.push_back("surchem"); } + if (inp.imp_sol == 2) { pot_register_in.push_back("sccs"); } if (inp.assume_isolated == "pcc_0d") { pot_register_in.push_back("pcc"); } if (inp.assume_isolated == "pcc_2d") { diff --git a/source/source_lcao/hamilt_lcao_factory.cpp b/source/source_lcao/hamilt_lcao_factory.cpp index 05a7b34b42b..6280c7c3402 100644 --- a/source/source_lcao/hamilt_lcao_factory.cpp +++ b/source/source_lcao/hamilt_lcao_factory.cpp @@ -169,7 +169,7 @@ LcaoOpsBundle build_gamma_ops(const UnitCell& ucell, if (pot_register_in.size() > 0) { // register Potential by gathered operator - pot_in->pot_register(pot_register_in); + pot_in->pot_register(pot_register_in, inp); // effective potential term Operator* veff = new Veff>(hsk, kv->kvec_d, pot_in, @@ -230,7 +230,7 @@ LcaoOpsBundle build_multik_ops(const UnitCell& ucell, if (pot_register_in.size() > 0) { // register Potential by gathered operator - pot_in->pot_register(pot_register_in); + pot_in->pot_register(pot_register_in, inp); // Veff term ops = new Veff>(hsk, kv->kvec_d, pot_in, diff --git a/source/source_pw/module_pwdft/hamilt_pw.cpp b/source/source_pw/module_pwdft/hamilt_pw.cpp index 13977cd9f6e..d3d284643f5 100644 --- a/source/source_pw/module_pwdft/hamilt_pw.cpp +++ b/source/source_pw/module_pwdft/hamilt_pw.cpp @@ -83,10 +83,11 @@ HamiltPW::HamiltPW(elecstate::Potential* pot_in, } // no variable can choose xc, maybe it is necessary pot_register_in.push_back("xc"); - if (input.imp_sol) + if (input.imp_sol == 1) { pot_register_in.push_back("surchem"); } + if (input.imp_sol == 2) { pot_register_in.push_back("sccs"); } if (input.assume_isolated == "pcc_0d") { pot_register_in.push_back("pcc"); @@ -122,7 +123,7 @@ HamiltPW::HamiltPW(elecstate::Potential* pot_in, if (pot_register_in.size() > 0) { // register Potential by gathered operator - pot_in->pot_register(pot_register_in); + pot_in->pot_register(pot_register_in, input); Operator* veff = new Veff>(isk, pot_in->get_veff_smooth_data(), pot_in->get_veff_smooth().nr, diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index 324309c3fb2..786d12b6f3c 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -140,3 +140,4 @@ scf_deltaspin2 scf_deltaspin4 221_PW_PCC0D_H3O 222_PW_PCC2D_H3O +scf_sccs_h2 diff --git a/tests/01_PW/scf_sccs_h2/INPUT b/tests/01_PW/scf_sccs_h2/INPUT new file mode 100644 index 00000000000..643f65618f5 --- /dev/null +++ b/tests/01_PW/scf_sccs_h2/INPUT @@ -0,0 +1,22 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type pw +ks_solver cg +pseudo_dir ../../PP_ORB +orbital_dir ../../PP_ORB +ntype 1 +nbands 2 +ecutwfc 20 +scf_thr 1e-9 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 0 +init_wfc atomic +imp_sol 2 +sccs_preset water-neutral +assume_isolated none +nspin 1 diff --git a/tests/01_PW/scf_sccs_h2/KPT b/tests/01_PW/scf_sccs_h2/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/01_PW/scf_sccs_h2/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/01_PW/scf_sccs_h2/README.md b/tests/01_PW/scf_sccs_h2/README.md new file mode 100644 index 00000000000..ed8c4a153db --- /dev/null +++ b/tests/01_PW/scf_sccs_h2/README.md @@ -0,0 +1,5 @@ +Periodic SCCS SCF for neutral H2 with the original water-neutral preset. +Checks total energy and electrostatic/non-electrostatic solvation components. +Uses ecutwfc=20 and the default density cutoff for short regression coverage; +this case does not establish physical cutoff convergence. Reference values +come from the original SCCS implementation at identical inputs and two MPI ranks. diff --git a/tests/01_PW/scf_sccs_h2/STRU b/tests/01_PW/scf_sccs_h2/STRU new file mode 100644 index 00000000000..d0697276a56 --- /dev/null +++ b/tests/01_PW/scf_sccs_h2/STRU @@ -0,0 +1,20 @@ +ATOMIC_SPECIES +H 1.008 H_ONCV_PBE-1.0.upf + +LATTICE_CONSTANT +1 + +LATTICE_VECTORS +10 0 0 +0 10 0 +0 0 10 + +ATOMIC_POSITIONS +Cartesian # Cartesian(Unit is LATTICE_CONSTANT) + + +H +0.0 +2 +0.00 0.00 -0.70 0 0 0 +0.00 0.01 0.70 0 0 0 diff --git a/tests/01_PW/scf_sccs_h2/result.ref b/tests/01_PW/scf_sccs_h2/result.ref new file mode 100644 index 00000000000..88e0e841122 --- /dev/null +++ b/tests/01_PW/scf_sccs_h2/result.ref @@ -0,0 +1,5 @@ +etotref -31.06632582920303 +etotperatomref -15.5331629146 +esolelref -0.0200563129 +esolcavref 0.0667246746 +totaltimeref 0.22 diff --git a/tests/02_NAO_Gamma/CASES_CPU.txt b/tests/02_NAO_Gamma/CASES_CPU.txt index 9ba3fa93de9..e6ee5f51a94 100644 --- a/tests/02_NAO_Gamma/CASES_CPU.txt +++ b/tests/02_NAO_Gamma/CASES_CPU.txt @@ -40,3 +40,4 @@ scf_upf100 test.sum scf_pcc0d_water scf_pcc2d_water +scf_sccs_h2 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/INPUT b/tests/02_NAO_Gamma/scf_sccs_h2/INPUT new file mode 100644 index 00000000000..0e0b9f824d9 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_h2/INPUT @@ -0,0 +1,23 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type lcao +ks_solver genelpa +pseudo_dir ../../PP_ORB +orbital_dir ../../PP_ORB +ntype 1 +nbands 2 +ecutwfc 20 +scf_thr 1e-7 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 0 +init_wfc atomic +imp_sol 2 +sccs_preset water-neutral +assume_isolated none +nspin 2 +gamma_only 1 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/KPT b/tests/02_NAO_Gamma/scf_sccs_h2/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_h2/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/README.md b/tests/02_NAO_Gamma/scf_sccs_h2/README.md new file mode 100644 index 00000000000..ed8c4a153db --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_h2/README.md @@ -0,0 +1,5 @@ +Periodic SCCS SCF for neutral H2 with the original water-neutral preset. +Checks total energy and electrostatic/non-electrostatic solvation components. +Uses ecutwfc=20 and the default density cutoff for short regression coverage; +this case does not establish physical cutoff convergence. Reference values +come from the original SCCS implementation at identical inputs and two MPI ranks. diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/STRU b/tests/02_NAO_Gamma/scf_sccs_h2/STRU new file mode 100644 index 00000000000..bf3cff73960 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_h2/STRU @@ -0,0 +1,23 @@ +ATOMIC_SPECIES +H 1.008 H_ONCV_PBE-1.0.upf + +NUMERICAL_ORBITAL +H_gga_8au_100Ry_2s1p.orb + +LATTICE_CONSTANT +1 + +LATTICE_VECTORS +10 0 0 +0 10 0 +0 0 10 + +ATOMIC_POSITIONS +Cartesian # Cartesian(Unit is LATTICE_CONSTANT) + + +H +0.0 +2 +0.00 0.00 -0.70 0 0 0 +0.00 0.01 0.70 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/result.ref b/tests/02_NAO_Gamma/scf_sccs_h2/result.ref new file mode 100644 index 00000000000..72d6f6ffa6a --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_h2/result.ref @@ -0,0 +1,5 @@ +etotref -31.67490984733957 +etotperatomref -15.8374549237 +esolelref -0.0091165409 +esolcavref 0.0640459014 +totaltimeref 0.71 diff --git a/tests/integrate/tools/catch_properties.sh b/tests/integrate/tools/catch_properties.sh index 68fadf7a147..7a3da18ad3c 100755 --- a/tests/integrate/tools/catch_properties.sh +++ b/tests/integrate/tools/catch_properties.sh @@ -797,7 +797,7 @@ fi #-------------------------------------------- # implicit solvation model #-------------------------------------------- -if ! test -z "$imp_sol" && [ $imp_sol == 1 ]; then +if ! test -z "$imp_sol" && { [ "$imp_sol" = 1 ] || [ "$imp_sol" = 2 ]; }; then esol_el=`grep E_sol_el $running_path | awk '{print $3}'` esol_cav=`grep E_sol_cav $running_path | awk '{print $3}'` echo "esolelref $esol_el" >>$1 From cd61d430bb577a4bb3de5b03ac1acdf3c3092d0d Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 18:28:41 -0600 Subject: [PATCH 16/41] test(pcc): tighten SCF convergence for stable MPI regressions --- tests/01_PW/221_PW_PCC0D_H3O/INPUT | 2 +- tests/01_PW/221_PW_PCC0D_H3O/README.md | 8 +++----- tests/01_PW/221_PW_PCC0D_H3O/result.ref | 8 ++++---- tests/01_PW/222_PW_PCC2D_H3O/INPUT | 2 +- tests/01_PW/222_PW_PCC2D_H3O/README.md | 12 ++++++------ tests/01_PW/222_PW_PCC2D_H3O/result.ref | 8 ++++---- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/tests/01_PW/221_PW_PCC0D_H3O/INPUT b/tests/01_PW/221_PW_PCC0D_H3O/INPUT index 00d83ad0364..25328671483 100644 --- a/tests/01_PW/221_PW_PCC0D_H3O/INPUT +++ b/tests/01_PW/221_PW_PCC0D_H3O/INPUT @@ -7,7 +7,7 @@ pseudo_dir ../../PP_ORB ntype 2 nbands 8 ecutwfc 20 -scf_thr 1e-9 +scf_thr 1e-12 scf_nmax 200 mixing_type pulay mixing_beta 0.2 diff --git a/tests/01_PW/221_PW_PCC0D_H3O/README.md b/tests/01_PW/221_PW_PCC0D_H3O/README.md index 218f62738eb..2be075dd83d 100644 --- a/tests/01_PW/221_PW_PCC0D_H3O/README.md +++ b/tests/01_PW/221_PW_PCC0D_H3O/README.md @@ -1,7 +1,5 @@ -Vacuum PCC 0D regression with `assume_isolated pcc_0d`. +Vacuum PCC 0D regression for charged H3O with `assume_isolated pcc_0d`. The integration harness compares total energy, ionic force magnitudes and `E_pcc` -(in eV). References were generated with two MPI ranks, one OpenMP thread and -the repository pseudopotentials/orbitals. Numerical kernels have independent -analytic and finite-difference unit tests; PW neutral/charged results were also -compared against the original full PCC implementation. +(in eV). References use the repository pseudopotentials, `scf_thr 1e-12`, +two MPI ranks and one OpenMP thread per rank. diff --git a/tests/01_PW/221_PW_PCC0D_H3O/result.ref b/tests/01_PW/221_PW_PCC0D_H3O/result.ref index c2b35c6f8c4..2b81bdee45c 100644 --- a/tests/01_PW/221_PW_PCC0D_H3O/result.ref +++ b/tests/01_PW/221_PW_PCC0D_H3O/result.ref @@ -1,5 +1,5 @@ -etotref -448.4562759200063624 -etotperatomref -112.1140689800 -totalforceref 20.653424 -epccref 2.0044958312 +etotref -448.4562759354126911 +etotperatomref -112.1140689839 +totalforceref 20.652937 +epccref 2.0044964276 totaltimeref 1.80 diff --git a/tests/01_PW/222_PW_PCC2D_H3O/INPUT b/tests/01_PW/222_PW_PCC2D_H3O/INPUT index b93c000c7a4..d2fbefd04c6 100644 --- a/tests/01_PW/222_PW_PCC2D_H3O/INPUT +++ b/tests/01_PW/222_PW_PCC2D_H3O/INPUT @@ -7,7 +7,7 @@ pseudo_dir ../../PP_ORB ntype 2 nbands 8 ecutwfc 20 -scf_thr 1e-9 +scf_thr 1e-12 scf_nmax 200 mixing_type pulay mixing_beta 0.2 diff --git a/tests/01_PW/222_PW_PCC2D_H3O/README.md b/tests/01_PW/222_PW_PCC2D_H3O/README.md index 7924bf4081c..635d18e9523 100644 --- a/tests/01_PW/222_PW_PCC2D_H3O/README.md +++ b/tests/01_PW/222_PW_PCC2D_H3O/README.md @@ -1,6 +1,6 @@ -Standalone vacuum PCC 2D regression. Compare total energy, ionic forces -and E_pcc against a two-rank reference with one OpenMP thread. The PW case -checks a charged periodic layer with open axis 2; the LCAO case checks a -neutral water layer with open axis 1. Analytic sheet/dipole, normal-force and -electronic-potential derivative tests provide independent kernel references. -The saved original PCC executable was also used for numerical comparison. +Vacuum PCC 2D regression for a charged H3O periodic layer with +`assume_isolated pcc_2d` and open axis 2 (`pcc_2d_axis 2`). + +The integration harness compares total energy, ionic force magnitudes and `E_pcc` +(in eV). References use the repository pseudopotentials, `scf_thr 1e-12`, +two MPI ranks and one OpenMP thread per rank. diff --git a/tests/01_PW/222_PW_PCC2D_H3O/result.ref b/tests/01_PW/222_PW_PCC2D_H3O/result.ref index 4be028f8eb1..448dacbcd64 100644 --- a/tests/01_PW/222_PW_PCC2D_H3O/result.ref +++ b/tests/01_PW/222_PW_PCC2D_H3O/result.ref @@ -1,5 +1,5 @@ -etotref -451.0967403180009114 -etotperatomref -112.7741850795 -totalforceref 20.659971 -epccref -0.6360808366 +etotref -451.0967403187477203 +etotperatomref -112.7741850797 +totalforceref 20.659011 +epccref -0.6360805933 totaltimeref 1.91 From 5ab8853eab62fae54d21439a34e503ea21df9e75 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 20:42:51 -0600 Subject: [PATCH 17/41] feat(sccs): add Gaussian ionic reaction-force kernel Compute the explicit ionic derivative at fixed electron density in Ha/Bohr using the existing PW FFT and guarded reductions. Verify analytic Fourier forces, gauge/translation invariance, six central energy differences in a nonuniform cavity, vacuum zero force, and distributed validation with output preservation. Build sccs and MODULE_HAMILT_sccs_poisson; 16 cases pass on both one and two ranks. INPUT behavior is unchanged: force/stress remain rejected until full SCF-force validation and integration. --- source/Makefile.Objects | 1 + .../source_hamilt/module_sccs/CMakeLists.txt | 1 + .../module_sccs/sccs_ionic_force.cpp | 77 ++++++++++++ .../module_sccs/sccs_ionic_force.h | 32 +++++ .../module_sccs/test/CMakeLists.txt | 4 +- .../test/test_sccs_ionic_force.cpp | 116 ++++++++++++++++++ 6 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 source/source_hamilt/module_sccs/sccs_ionic_force.cpp create mode 100644 source/source_hamilt/module_sccs/sccs_ionic_force.h create mode 100644 source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 48bad78ceea..c77fc98e281 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -617,6 +617,7 @@ OBJS_SCCS=sccs_cavity.o\ sccs_functional.o\ sccs_parameters.o\ sccs_ionic_charge.o\ + sccs_ionic_force.o\ sccs_pw_coulomb.o\ sccs_response.o diff --git a/source/source_hamilt/module_sccs/CMakeLists.txt b/source/source_hamilt/module_sccs/CMakeLists.txt index cd6c73238f3..f76b46ec716 100644 --- a/source/source_hamilt/module_sccs/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(sccs OBJECT sccs_parameters.cpp sccs_functional.cpp sccs_ionic_charge.cpp + sccs_ionic_force.cpp sccs_pw_coulomb.cpp sccs_response.cpp) diff --git a/source/source_hamilt/module_sccs/sccs_ionic_force.cpp b/source/source_hamilt/module_sccs/sccs_ionic_force.cpp new file mode 100644 index 00000000000..44bc25712c1 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_ionic_force.cpp @@ -0,0 +1,77 @@ +#include "sccs_ionic_force.h" +#include "sccs_pw_coulomb.h" + +#include "source_base/constants.h" +#include "source_base/parallel_reduce.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_cell/cell_tools.h" + +#include +#include + +namespace ModuleSccs +{ +bool gaussian_ionic_force(const std::vector& atoms, + const std::vector& reaction_potential, + const ModulePW::PW_Basis& basis, + double tpiba, + double spread, + std::vector>& forces, + std::string& error) +{ + if (!validate_pw_grid(basis, tpiba, error) + || !validate_grid_values(reaction_potential, basis, error)) + { + return false; + } + double invalid = 0.0; + if (!std::isfinite(spread) || spread <= 0.0) { invalid = 1.0; } + for (const unitcell::AtomData& atom : atoms) + { + if (!std::isfinite(atom.valence_charge) || atom.valence_charge < 0.0 + || !std::isfinite(atom.position.x) || !std::isfinite(atom.position.y) + || !std::isfinite(atom.position.z)) + { + invalid = 1.0; + } + } + Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); + if (invalid != 0.0) + { + error = "SCCS Gaussian forces require finite atoms and a positive finite spread"; + return false; + } + std::vector> potential_g(basis.npw); + basis.real2recip(reaction_potential.data(), potential_g.data()); + std::vector> candidate(atoms.size()); + const double tpiba2 = tpiba * tpiba; + // F = -integral(v_reaction d rho_ion/d R), using ABACUS's normalized FFT. + // The cell volume cancels the Z/omega in the Gaussian charge coefficients. + for (std::size_t ia = 0; ia < atoms.size(); ++ia) + { + const unitcell::AtomData& atom = atoms[ia]; + for (int ig = 0; ig < basis.npw; ++ig) + { + const double exponent = -0.25 * spread * spread * tpiba2 * basis.gg[ig]; + const double gaussian = std::exp(exponent); + const double phase = tpiba * (basis.gcar[ig] * atom.position); + const std::complex phase_argument = ModuleBase::NEG_IMAG_UNIT * phase; + const std::complex phase_factor = std::exp(phase_argument); + const std::complex weighted = std::conj(potential_g[ig]) * phase_factor; + const double factor = -atom.valence_charge * tpiba * gaussian * weighted.imag(); + candidate[ia] += basis.gcar[ig] * factor; + } + Parallel_Reduce::reduce_pool(candidate[ia].x); + Parallel_Reduce::reduce_pool(candidate[ia].y); + Parallel_Reduce::reduce_pool(candidate[ia].z); + if (!std::isfinite(candidate[ia].x) || !std::isfinite(candidate[ia].y) + || !std::isfinite(candidate[ia].z)) + { + error = "SCCS Gaussian ionic force is not finite"; + return false; + } + } + forces.swap(candidate); + return true; +} +} // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_ionic_force.h b/source/source_hamilt/module_sccs/sccs_ionic_force.h new file mode 100644 index 00000000000..400e0d2eda5 --- /dev/null +++ b/source/source_hamilt/module_sccs/sccs_ionic_force.h @@ -0,0 +1,32 @@ +#ifndef SCCS_IONIC_FORCE_H +#define SCCS_IONIC_FORCE_H + +#include +#include + +namespace ModuleBase +{ +template class Vector3; +} +namespace ModulePW +{ +class PW_Basis; +} +namespace unitcell +{ +struct AtomData; +} +namespace ModuleSccs +{ +// Explicit Gaussian-ion derivative of the reaction energy at fixed electron +// density. reaction_potential is in Ha; force is in Ha/Bohr, atom-major. +// Collective over the PW pool; failure leaves forces unchanged. +bool gaussian_ionic_force(const std::vector& atoms, + const std::vector& reaction_potential, + const ModulePW::PW_Basis& basis, + double tpiba, + double spread, + std::vector>& forces, + std::string& error); +} // namespace ModuleSccs +#endif diff --git a/source/source_hamilt/module_sccs/test/CMakeLists.txt b/source/source_hamilt/module_sccs/test/CMakeLists.txt index fe8c5cda2eb..1bd0619b719 100644 --- a/source/source_hamilt/module_sccs/test/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/test/CMakeLists.txt @@ -5,14 +5,14 @@ AddTest(TARGET MODULE_HAMILT_sccs_parameters AddTest(TARGET MODULE_HAMILT_sccs_poisson LIBS parameter planewave device base - SOURCES test_sccs_pw_coulomb.cpp test_sccs_ionic_charge.cpp test_sccs_response.cpp test_sccs_functional.cpp + SOURCES test_sccs_pw_coulomb.cpp test_sccs_ionic_charge.cpp test_sccs_ionic_force.cpp test_sccs_response.cpp test_sccs_functional.cpp ../../../source_estate/test/test_pot_sccs.cpp ../../../source_estate/module_pot/pot_sccs.cpp ../../../source_cell/cell_tools.cpp ../../../source_cell/atom_spec.cpp ../../../source_cell/atom_pseudo.cpp ../../../source_cell/pseudo.cpp ../sccs_functional.cpp ../sccs_parameters.cpp - ../sccs_ionic_charge.cpp ../sccs_pw_coulomb.cpp ../sccs_response.cpp ../sccs_cavity.cpp + ../sccs_ionic_charge.cpp ../sccs_ionic_force.cpp ../sccs_pw_coulomb.cpp ../sccs_response.cpp ../sccs_cavity.cpp ../../module_xc/xc_grad_utils.cpp) set_tests_properties(MODULE_HAMILT_sccs_poisson PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1" TIMEOUT 60) diff --git a/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp b/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp new file mode 100644 index 00000000000..1e03925511d --- /dev/null +++ b/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp @@ -0,0 +1,116 @@ +#include "sccs_test.h" +#include "../sccs_ionic_force.h" +#include "../sccs_ionic_charge.h" +#include "../sccs_response.h" +#include "../sccs_functional.h" + +#include "source_cell/cell_tools.h" +#include + +using SccsIonicForceTest = SccsTest::PwTest; + +TEST_F(SccsIonicForceTest, AnalyticFourierForceAndGaugeInvariance) +{ + std::vector atoms(1); + atoms[0].valence_charge = 2.0; + atoms[0].position.x = length / 4.0; + std::vector potential = cosine_mode(0); + const double spread = ModuleSccs::gaussian_ion_spread; + std::vector> forces; + ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces, error)) << error; + const double exponent = -0.25 * spread * spread * tpiba * tpiba; + const double expected = 2.0 * tpiba * std::exp(exponent); + EXPECT_NEAR(forces[0].x, expected, 1e-12); + EXPECT_NEAR(forces[0].y, 0.0, 1e-12); + EXPECT_NEAR(forces[0].z, 0.0, 1e-12); + atoms[0].position.x += length; + for (double& value : potential) { value += 4.0; } + ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces, error)) << error; + EXPECT_NEAR(forces[0].x, expected, 1e-12); + potential.assign(basis.nrxx, 3.0); + ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces, error)) << error; + EXPECT_NEAR(forces[0].x, 0.0, 1e-12); + EXPECT_NEAR(forces[0].y, 0.0, 1e-12); + EXPECT_NEAR(forces[0].z, 0.0, 1e-12); +} + +TEST_F(SccsIonicForceTest, ReactionEnergyFiniteDifferenceAtFixedNonuniformCavity) +{ + std::vector atoms(2); + atoms[0].valence_charge = 1.0; + atoms[0].position = ModuleBase::Vector3(2.1, 3.2, 4.3); + atoms[1].valence_charge = 1.0; + atoms[1].position = ModuleBase::Vector3(6.4, 5.1, 2.7); + std::vector density = cosine_mode(0); + for (double& value : density) { value = (2.0 / basis.omega) * (1.0 + 0.1 * value); } + ModuleSccs::SccsConfig config; + config.cavity.density_min = 1e-3; + config.cavity.density_max = 3e-3; + config.cavity.epsilon_bulk = 5.0; + config.surface_regularization = 1e-8; + ModuleSccs::PolarizationSolverParameters solver; + solver.tolerance_rms = 1e-13; + solver.tolerance_max = 1e-12; + const double spread = ModuleSccs::gaussian_ion_spread; + const std::vector cold_start; + auto evaluate = [&](const std::vector& displaced, + ModuleSccs::FunctionalResult& functional) -> bool { + std::vector charge; + if (!ModuleSccs::gaussian_ionic_density(displaced, basis, tpiba, spread, charge, error)) { return false; } + for (int ir = 0; ir < basis.nrxx; ++ir) { charge[ir] -= density[ir]; } + ModuleSccs::SccsResponse response; + if (!ModuleSccs::solve_sccs_response(density, charge, config.cavity, solver, cold_start, + basis, tpiba, response, error)) { return false; } + return ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, functional, error); + }; + ModuleSccs::FunctionalResult baseline; + ASSERT_TRUE(evaluate(atoms, baseline)) << error; + std::vector> forces; + ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, baseline.reaction_potential, basis, + tpiba, spread, forces, error)) << error; + const double step = 1e-4; + for (std::size_t ia = 0; ia < atoms.size(); ++ia) + { + for (int axis = 0; axis < 3; ++axis) + { + std::vector plus = atoms; + std::vector minus = atoms; + plus[ia].position[axis] += step; + minus[ia].position[axis] -= step; + ModuleSccs::FunctionalResult positive; + ModuleSccs::FunctionalResult negative; + ASSERT_TRUE(evaluate(plus, positive)) << error; + ASSERT_TRUE(evaluate(minus, negative)) << error; + const double finite_difference = -(positive.reaction_energy - negative.reaction_energy) / (2.0 * step); + EXPECT_NEAR(forces[ia][axis], finite_difference, 1e-8); + } + } + config.cavity.epsilon_bulk = 1.0; + ASSERT_TRUE(evaluate(atoms, baseline)) << error; + ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, baseline.reaction_potential, basis, + tpiba, spread, forces, error)) << error; + for (const ModuleBase::Vector3& force : forces) + { + EXPECT_NEAR(force.x, 0.0, 1e-12); + EXPECT_NEAR(force.y, 0.0, 1e-12); + EXPECT_NEAR(force.z, 0.0, 1e-12); + } +} + +TEST_F(SccsIonicForceTest, DistributedInvalidInputPreservesOutput) +{ + std::vector atoms(1); + atoms[0].valence_charge = 1.0; + std::vector potential(basis.nrxx, 0.0); + if (basis.poolrank == 0) { atoms[0].position.x = std::numeric_limits::quiet_NaN(); } + const ModuleBase::Vector3 sentinel(7.0, 8.0, 9.0); + std::vector> forces(1, sentinel); + EXPECT_FALSE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, 0.5, forces, error)); + EXPECT_DOUBLE_EQ(forces[0].x, 7.0); + atoms[0].position.x = 0.0; + EXPECT_FALSE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, 0.0, forces, error)); + EXPECT_DOUBLE_EQ(forces[0].y, 8.0); + if (basis.poolrank == 0 && basis.nrxx > 0) { potential[0] = std::numeric_limits::infinity(); } + EXPECT_FALSE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, 0.5, forces, error)); + EXPECT_DOUBLE_EQ(forces[0].z, 9.0); +} From 8eeca990eb60c7ec40c454a113e047b80c0ec350 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 21:22:05 -0600 Subject: [PATCH 18/41] feat(sccs): integrate neutral periodic SCF forces Dispatch solvation forces through PotBase, recover the reaction field in Ha and convert Gaussian ionic forces to Ry/Bohr once. Keep legacy solvent dispatch separate in PW/LCAO. Enable SCF cal_force for imp_sol=2 while stress, relaxation and PCC remain rejected; regenerate INPUT docs. Build abacus_basic_para and focused targets; eight CTest targets and both short integration cases pass. H2 full-SCF force finite differences at 100 Ry differ by 8.21e-4 eV/Angstrom in PW and 9.02e-6 in LCAO; PW at 200 Ry differs by 5.57e-4. New/old force differences are at most 3.4e-9 eV/Angstrom. Vacuum and legacy solvent force/energy results match exactly. Preserve the inherited finite-cutoff cavity-derivative discrepancy without loosening references. --- docs/advanced/input_files/input-main.md | 2 +- docs/parameters.yaml | 2 +- source/source_estate/module_pot/pot_base.h | 2 + source/source_estate/module_pot/pot_sccs.cpp | 33 +++++++++ source/source_estate/module_pot/pot_sccs.h | 1 + .../module_pot/potential_new.cpp | 8 +++ .../source_estate/module_pot/potential_new.h | 1 + .../test/elecstate_base_test.cpp | 4 ++ .../test/elecstate_energy_test.cpp | 1 + .../source_estate/test/elecstate_pw_test.cpp | 4 ++ .../source_estate/test/potential_new_test.cpp | 32 +++++++++ source/source_estate/test/test_pot_sccs.cpp | 68 +++++++++++++++++++ .../module_parameter/read_inp_sccs.cpp | 6 +- .../test_serial/test_read_inp_sccs.cpp | 3 + source/source_lcao/force_stress_lcao.cpp | 2 + source/source_lcao/force_stress_terms.cpp | 2 +- source/source_pw/module_pwdft/force_pw.cpp | 16 +++-- .../module_stodft/test/test_sto_hamilt_pw.cpp | 3 + tests/01_PW/scf_sccs_h2/INPUT | 2 +- tests/01_PW/scf_sccs_h2/README.md | 2 +- tests/01_PW/scf_sccs_h2/result.ref | 5 +- tests/02_NAO_Gamma/scf_sccs_h2/INPUT | 2 +- tests/02_NAO_Gamma/scf_sccs_h2/README.md | 2 +- tests/02_NAO_Gamma/scf_sccs_h2/result.ref | 3 +- 24 files changed, 189 insertions(+), 17 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 121e8f88580..fc59045a7d3 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -4917,7 +4917,7 @@ ### imp_sol - **Type**: Integer -- **Description**: Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without forces, stress, external fields or other correction models. +- **Description**: Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. - **Default**: 0 ### sccs_preset diff --git a/docs/parameters.yaml b/docs/parameters.yaml index fad4670ae1f..8768e4c1873 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -4334,7 +4334,7 @@ parameters: category: Implicit solvation model type: Integer description: | - Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without forces, stress, external fields or other correction models. + Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. default_value: "0" unit: "" availability: "" diff --git a/source/source_estate/module_pot/pot_base.h b/source/source_estate/module_pot/pot_base.h index f87b442df93..658962f276e 100644 --- a/source/source_estate/module_pot/pot_base.h +++ b/source/source_estate/module_pot/pot_base.h @@ -36,6 +36,8 @@ class PotBase electrostatic = 0.0; non_electrostatic = 0.0; } + // Explicit ionic solvation force in Ry/Bohr, added to existing contributions. + virtual void add_solvation_force(const UnitCell&, ModuleBase::matrix&) const {} // Electronic electrostatic correction only; excludes cavity/surface derivatives. virtual const std::vector* solvent_electrostatic_potential() const { return nullptr; } diff --git a/source/source_estate/module_pot/pot_sccs.cpp b/source/source_estate/module_pot/pot_sccs.cpp index 104c6df0439..43b09d947b6 100644 --- a/source/source_estate/module_pot/pot_sccs.cpp +++ b/source/source_estate/module_pot/pot_sccs.cpp @@ -6,6 +6,7 @@ #include "source_cell/cell_tools.h" #include "source_hamilt/module_sccs/sccs_functional.h" #include "source_hamilt/module_sccs/sccs_ionic_charge.h" +#include "source_hamilt/module_sccs/sccs_ionic_force.h" #include "source_io/module_parameter/input_parameter.h" #include @@ -139,6 +140,38 @@ void PotSccs::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase:: ModuleBase::timer::end("PotSccs", "cal_v_eff"); } +void PotSccs::add_solvation_force(const UnitCell& cell, ModuleBase::matrix& force) const +{ + ModuleBase::timer::start("PotSccs", "add_solvation_force"); + const bool shape_valid = force.nr == cell.nat && force.nc == 3 + && this->rho_basis_ != nullptr && cell.atoms != nullptr && cell.ntype > 0; + require_valid_on_pool(shape_valid, "SCCS force requires initialized cell and atom-major storage"); + const ModulePW::PW_Basis& basis = *this->rho_basis_; + const bool result_valid = electrostatic_potential_.size() == static_cast(basis.nrxx); + require_valid_on_pool(result_valid, "SCCS force requires a completed response on the current grid"); + const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); + const bool count_valid = atoms.size() == static_cast(cell.nat); + require_valid_on_pool(count_valid, "SCCS force atom count does not match UnitCell"); + std::vector reaction(basis.nrxx); + for (int ir = 0; ir < basis.nrxx; ++ir) + { + reaction[ir] = -0.5 * electrostatic_potential_[ir]; + } + std::vector> ionic_force; + std::string error; + const bool valid = ModuleSccs::gaussian_ionic_force(atoms, reaction, basis, cell.tpiba, + ModuleSccs::gaussian_ion_spread, ionic_force, error); + require_valid_on_pool(valid, error); + for (int ia = 0; ia < cell.nat; ++ia) + { + for (int axis = 0; axis < 3; ++axis) + { + force(ia, axis) += 2.0 * ionic_force[ia][axis]; + } + } + ModuleBase::timer::end("PotSccs", "add_solvation_force"); +} + double PotSccs::get_energy() const { return electrostatic_rydberg_ + non_electrostatic_rydberg_; diff --git a/source/source_estate/module_pot/pot_sccs.h b/source/source_estate/module_pot/pot_sccs.h index 327445b8a08..be723a359ec 100644 --- a/source/source_estate/module_pot/pot_sccs.h +++ b/source/source_estate/module_pot/pot_sccs.h @@ -21,6 +21,7 @@ class PotSccs : public PotBase const ModuleSccs::PolarizationSolverParameters& solver); void cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) override; double get_energy() const override; + void add_solvation_force(const UnitCell& cell, ModuleBase::matrix& force) const override; void get_solvation_energy(double& electrostatic, double& non_electrostatic) const override; const std::vector* solvent_electrostatic_potential() const override; diff --git a/source/source_estate/module_pot/potential_new.cpp b/source/source_estate/module_pot/potential_new.cpp index 0949100824e..5ddce50283c 100644 --- a/source/source_estate/module_pot/potential_new.cpp +++ b/source/source_estate/module_pot/potential_new.cpp @@ -15,6 +15,14 @@ namespace elecstate { +void Potential::add_solvation_force(const UnitCell& cell, ModuleBase::matrix& force) const +{ + for (const PotBase* component : this->components) + { + component->add_solvation_force(cell, force); + } +} + void Potential::get_solvation_energy(double& electrostatic, double& non_electrostatic) const { electrostatic = 0.0; diff --git a/source/source_estate/module_pot/potential_new.h b/source/source_estate/module_pot/potential_new.h index fb72d1e5c86..d017035b6e5 100644 --- a/source/source_estate/module_pot/potential_new.h +++ b/source/source_estate/module_pot/potential_new.h @@ -84,6 +84,7 @@ class Potential : public PotBase PotBase* get_pot_type(const std::string& pot_type, const Input_para& input); void get_solvation_energy(double& electrostatic, double& non_electrostatic) const override; + void add_solvation_force(const UnitCell& cell, ModuleBase::matrix& force) const override; const std::vector* solvent_electrostatic_potential() const override; /// PCC results belong to this electronic state's registered potential. diff --git a/source/source_estate/test/elecstate_base_test.cpp b/source/source_estate/test/elecstate_base_test.cpp index 23a24dad342..e23ee4d2d6c 100644 --- a/source/source_estate/test/elecstate_base_test.cpp +++ b/source/source_estate/test/elecstate_base_test.cpp @@ -36,6 +36,10 @@ int main(int argc, char** argv) // Mock functions for testing elecstate.cpp namespace elecstate { +void Potential::add_solvation_force(const UnitCell&, ModuleBase::matrix&) const {} +void Potential::get_solvation_energy(double& el, double& cav) const { el = 0.0; cav = 0.0; } +const std::vector* Potential::solvent_electrostatic_potential() const { return nullptr; } + void Potential::init_pot(Charge const*) { } diff --git a/source/source_estate/test/elecstate_energy_test.cpp b/source/source_estate/test/elecstate_energy_test.cpp index 6b98a8bd1cf..67b2bd88908 100644 --- a/source/source_estate/test/elecstate_energy_test.cpp +++ b/source/source_estate/test/elecstate_energy_test.cpp @@ -13,6 +13,7 @@ bool XC_Functional::ked_flag = false; namespace elecstate { Potential::~Potential() {} +void Potential::add_solvation_force(const UnitCell&, ModuleBase::matrix&) const {} void Potential::cal_v_eff(const Charge*, const UnitCell*, ModuleBase::matrix&) {} void Potential::cal_fixed_v(double*) {} const std::vector* Potential::solvent_electrostatic_potential() const { return nullptr; } diff --git a/source/source_estate/test/elecstate_pw_test.cpp b/source/source_estate/test/elecstate_pw_test.cpp index 51cea17a6a1..2f6918ed120 100644 --- a/source/source_estate/test/elecstate_pw_test.cpp +++ b/source/source_estate/test/elecstate_pw_test.cpp @@ -25,6 +25,10 @@ class TestParameters int XC_Functional::func_type = 1; namespace elecstate { +void Potential::add_solvation_force(const UnitCell&, ModuleBase::matrix&) const {} +void Potential::get_solvation_energy(double& el, double& cav) const { el = 0.0; cav = 0.0; } +const std::vector* Potential::solvent_electrostatic_potential() const { return nullptr; } + void Potential::init_pot(Charge const*) { } diff --git a/source/source_estate/test/potential_new_test.cpp b/source/source_estate/test/potential_new_test.cpp index 2938bc0e8b2..026b2222d70 100644 --- a/source/source_estate/test/potential_new_test.cpp +++ b/source/source_estate/test/potential_new_test.cpp @@ -130,6 +130,15 @@ class MockPotComponent : public PotBase } } + void add_solvation_force(const UnitCell&, ModuleBase::matrix& force) const override + { + if (type_ != "solvation") { return; } + for (int ia = 0; ia < force.nr; ++ia) + { + for (int axis = 0; axis < force.nc; ++axis) { force(ia, axis) += 1.25; } + } + } + static void reset() { created = 0; @@ -411,3 +420,26 @@ TEST_F(PotentialNewTest, DifferentBasisObjectsRequireMatchingGammaOnly) EXPECT_EXIT(potential->update_from_charge(&charge, ucell.get()), ::testing::ExitedWithCode(1), ""); } + +TEST_F(PotentialNewTest, SolvationForcesUseRegisteredComponentsOnce) +{ + setup_smooth_basis(); + create_potential(smooth_basis.get(), smooth_basis.get()); + potential->pot_register({"fixed", "solvation"}); + ModuleBase::matrix force(2, 3); + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) { force(ia, axis) = 7.0; } + } + potential->add_solvation_force(*ucell, force); + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) { EXPECT_DOUBLE_EQ(force(ia, axis), 8.25); } + } + potential->pot_register({"fixed"}); + potential->add_solvation_force(*ucell, force); + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) { EXPECT_DOUBLE_EQ(force(ia, axis), 8.25); } + } +} diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp index 4ad74fc3c9f..32069c591bc 100644 --- a/source/source_estate/test/test_pot_sccs.cpp +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -89,3 +89,71 @@ TEST_F(PotSccsTest, PresetConfigurationIsExplicit) EXPECT_FALSE(elecstate::make_sccs_config_from_input(input, config, solver, error)); EXPECT_EQ(solver.max_iterations, 42); } + +TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) +{ + UnitCell cell; + Atom atom; + cell.lat0 = length; + cell.tpiba = tpiba; + cell.omega = basis.omega; + cell.ntype = 1; + cell.nat = 2; + cell.atoms = &atom; + atom.na = 2; + atom.ncpp.zv = 1.0; + atom.tau = {ModuleBase::Vector3(0.21, 0.32, 0.43), + ModuleBase::Vector3(0.64, 0.51, 0.27)}; + const double density_value = 2.0 / basis.omega; + std::vector density(basis.nrxx, density_value); + double* channels[] = {density.data()}; + Charge charge; + charge.nspin = 1; + charge.rho = channels; + Input_para input; + input.sccs_epsilon = 5.0; + input.sccs_tol_rms = 1e-13; + input.sccs_tol_max = 1e-12; + ModuleSccs::SccsConfig config; + ModuleSccs::PolarizationSolverParameters solver; + ASSERT_TRUE(elecstate::make_sccs_config_from_input(input, config, solver, error)); + elecstate::PotSccs component(&basis, config, solver); + ModuleBase::matrix potential(1, basis.nrxx); + component.cal_v_eff(&charge, &cell, potential); + ModuleBase::matrix force(2, 3); + component.add_solvation_force(cell, force); + const double step = 1e-4; + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) + { + const double original = atom.tau[ia][axis]; + atom.tau[ia][axis] = original + step / length; + potential.zero_out(); + component.cal_v_eff(&charge, &cell, potential); + const double positive = component.get_energy(); + atom.tau[ia][axis] = original - step / length; + potential.zero_out(); + component.cal_v_eff(&charge, &cell, potential); + const double negative = component.get_energy(); + atom.tau[ia][axis] = original; + const double finite_difference = -(positive - negative) / (2.0 * step); + EXPECT_NEAR(force(ia, axis), finite_difference, 1e-8); + } + } + potential.zero_out(); + component.cal_v_eff(&charge, &cell, potential); + ModuleBase::matrix accumulated(2, 3); + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) { accumulated(ia, axis) = 7.0; } + } + component.add_solvation_force(cell, accumulated); + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) + { + EXPECT_NEAR(accumulated(ia, axis), 7.0 + force(ia, axis), 1e-10); + } + } +} diff --git a/source/source_io/module_parameter/read_inp_sccs.cpp b/source/source_io/module_parameter/read_inp_sccs.cpp index 500c19e2ce0..badc3b000e8 100644 --- a/source/source_io/module_parameter/read_inp_sccs.cpp +++ b/source/source_io/module_parameter/read_inp_sccs.cpp @@ -44,8 +44,8 @@ bool validate_sccs_input(const Input_para& input, std::string& error) if (input.assume_isolated != "none") { error = "SCCS currently requires assume_isolated none"; } else if (input.device != "cpu" || input.esolver_type != "ksdft") { error = "SCCS requires CPU KS-DFT"; } else if (input.basis_type != "pw" && input.basis_type != "lcao") { error = "SCCS requires basis_type pw or lcao"; } - else if (input.calculation != "scf" || input.cal_force || input.cal_stress) - { error = "SCCS currently supports SCF energies without forces or stress"; } + else if (input.calculation != "scf" || input.cal_stress) + { error = "SCCS currently supports SCF energies and forces without stress"; } else if (input.nspin != 1 && input.nspin != 2) { error = "SCCS requires nspin 1 or 2"; } else if (input.efield_flag || input.gate_flag || input.dfthalf_type != 0 || input.deepks_scf || input.deepks_out_labels || input.deepks_bandgap @@ -81,7 +81,7 @@ void ReadInput::item_sccs() item.annotation = "implicit solvent model"; item.category = "Implicit solvation model"; item.type = "Integer"; - item.description = "Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without forces, stress, external fields or other correction models."; + item.description = "Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models."; item.default_value = "0"; item.read_value = [](const Input_Item& item, Parameter& para) { std::string error; diff --git a/source/source_io/test_serial/test_read_inp_sccs.cpp b/source/source_io/test_serial/test_read_inp_sccs.cpp index ae787a82180..94cb6bad718 100644 --- a/source/source_io/test_serial/test_read_inp_sccs.cpp +++ b/source/source_io/test_serial/test_read_inp_sccs.cpp @@ -34,7 +34,10 @@ TEST(ReadInpSccs, SupportedScopeAndNumericalValidation) std::string error; ASSERT_TRUE(ModuleIO::validate_sccs_input(input, error)) << error; input.cal_force = true; + EXPECT_TRUE(ModuleIO::validate_sccs_input(input, error)); + input.cal_stress = true; EXPECT_FALSE(ModuleIO::validate_sccs_input(input, error)); + input.cal_stress = false; input.cal_force = false; input.assume_isolated = "pcc_0d"; EXPECT_FALSE(ModuleIO::validate_sccs_input(input, error)); diff --git a/source/source_lcao/force_stress_lcao.cpp b/source/source_lcao/force_stress_lcao.cpp index a65ba5b204e..9f80519426c 100644 --- a/source/source_lcao/force_stress_lcao.cpp +++ b/source/source_lcao/force_stress_lcao.cpp @@ -190,6 +190,8 @@ void Force_Stress_LCAO::getForceStress(UnitCell& ucell, { parts.fpcc.create(ucell.nat, 3); pelec->pot->add_pcc_force(ucell, parts.fpcc); + if (parts.fsol.nr == 0) { parts.fsol.create(ucell.nat, 3); } + pelec->pot->add_solvation_force(ucell, parts.fsol); } // DFT+U force/stress diff --git a/source/source_lcao/force_stress_terms.cpp b/source/source_lcao/force_stress_terms.cpp index 0ca1cc1db03..671d5e13628 100644 --- a/source/source_lcao/force_stress_terms.cpp +++ b/source/source_lcao/force_stress_terms.cpp @@ -92,7 +92,7 @@ void cal_external_field_forces(UnitCell& ucell, } //! atomic forces from implicit solvation model - if (PARAM.inp.imp_sol) + if (PARAM.inp.imp_sol == 1) { parts.fsol.create(ucell.nat, 3); solvent.cal_force_sol(ucell, rhopw, locpp.vloc, PARAM.inp.nspin, parts.fsol); diff --git a/source/source_pw/module_pwdft/force_pw.cpp b/source/source_pw/module_pwdft/force_pw.cpp index 9381316b46f..e5c93e312cf 100644 --- a/source/source_pw/module_pwdft/force_pw.cpp +++ b/source/source_pw/module_pwdft/force_pw.cpp @@ -137,13 +137,21 @@ void Forces::cal_force(const int nspin, } } + const int solvation_model = PARAM.inp.imp_sol; ModuleBase::matrix forcesol; ModuleBase::matrix forcepcc(this->nat, 3); elec.pot->add_pcc_force(ucell, forcepcc); - if (PARAM.inp.imp_sol) + if (solvation_model != 0) { forcesol.create(this->nat, 3); - solvent.cal_force_sol(ucell, rho_basis, locpp->vloc, nspin, forcesol); + if (solvation_model == 1) + { + solvent.cal_force_sol(ucell, rho_basis, locpp->vloc, nspin, forcesol); + } + else + { + elec.pot->add_solvation_force(ucell, forcesol); + } if (PARAM.inp.test_force) { ModuleIO::print_force(GlobalV::ofs_running, ucell, "IMP_SOL FORCE (Ry/Bohr)", forcesol); @@ -177,7 +185,7 @@ void Forces::cal_force(const int nspin, force(iat, ipol) = force(iat, ipol) + force_gate(iat, ipol); } - if (PARAM.inp.imp_sol) + if (solvation_model != 0) { force(iat, ipol) = force(iat, ipol) + forcesol(iat, ipol); } @@ -261,7 +269,7 @@ void Forces::cal_force(const int nspin, force_gate, false); } - if (PARAM.inp.imp_sol) + if (solvation_model != 0) { ModuleIO::print_force(GlobalV::ofs_running, ucell, diff --git a/source/source_pw/module_stodft/test/test_sto_hamilt_pw.cpp b/source/source_pw/module_stodft/test/test_sto_hamilt_pw.cpp index 7023d90719b..78dfc1a44aa 100644 --- a/source/source_pw/module_stodft/test/test_sto_hamilt_pw.cpp +++ b/source/source_pw/module_stodft/test/test_sto_hamilt_pw.cpp @@ -6,6 +6,9 @@ #include elecstate::Potential::~Potential(){} +void elecstate::Potential::add_solvation_force(const UnitCell&, ModuleBase::matrix&) const {} +void elecstate::Potential::get_solvation_energy(double& el, double& cav) const { el = 0.0; cav = 0.0; } +const std::vector* elecstate::Potential::solvent_electrostatic_potential() const { return nullptr; } void elecstate::Potential::cal_v_eff(Charge const*, UnitCell const*, ModuleBase::matrix&){} void elecstate::Potential::cal_fixed_v(double*){} diff --git a/tests/01_PW/scf_sccs_h2/INPUT b/tests/01_PW/scf_sccs_h2/INPUT index 643f65618f5..9ced12b8b91 100644 --- a/tests/01_PW/scf_sccs_h2/INPUT +++ b/tests/01_PW/scf_sccs_h2/INPUT @@ -14,7 +14,7 @@ mixing_type pulay mixing_beta 0.2 smearing_method fixed symmetry 0 -cal_force 0 +cal_force 1 init_wfc atomic imp_sol 2 sccs_preset water-neutral diff --git a/tests/01_PW/scf_sccs_h2/README.md b/tests/01_PW/scf_sccs_h2/README.md index ed8c4a153db..198d6aa65cb 100644 --- a/tests/01_PW/scf_sccs_h2/README.md +++ b/tests/01_PW/scf_sccs_h2/README.md @@ -1,5 +1,5 @@ Periodic SCCS SCF for neutral H2 with the original water-neutral preset. -Checks total energy and electrostatic/non-electrostatic solvation components. +Checks total energy, atomic forces and electrostatic/non-electrostatic solvation components. Uses ecutwfc=20 and the default density cutoff for short regression coverage; this case does not establish physical cutoff convergence. Reference values come from the original SCCS implementation at identical inputs and two MPI ranks. diff --git a/tests/01_PW/scf_sccs_h2/result.ref b/tests/01_PW/scf_sccs_h2/result.ref index 88e0e841122..21ddf000f55 100644 --- a/tests/01_PW/scf_sccs_h2/result.ref +++ b/tests/01_PW/scf_sccs_h2/result.ref @@ -1,5 +1,6 @@ -etotref -31.06632582920303 +etotref -31.0663258292030271 etotperatomref -15.5331629146 +totalforceref 2.004916 esolelref -0.0200563129 esolcavref 0.0667246746 -totaltimeref 0.22 +totaltimeref 0.21 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/INPUT b/tests/02_NAO_Gamma/scf_sccs_h2/INPUT index 0e0b9f824d9..3bba4ac7c1f 100644 --- a/tests/02_NAO_Gamma/scf_sccs_h2/INPUT +++ b/tests/02_NAO_Gamma/scf_sccs_h2/INPUT @@ -14,7 +14,7 @@ mixing_type pulay mixing_beta 0.2 smearing_method fixed symmetry 0 -cal_force 0 +cal_force 1 init_wfc atomic imp_sol 2 sccs_preset water-neutral diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/README.md b/tests/02_NAO_Gamma/scf_sccs_h2/README.md index ed8c4a153db..198d6aa65cb 100644 --- a/tests/02_NAO_Gamma/scf_sccs_h2/README.md +++ b/tests/02_NAO_Gamma/scf_sccs_h2/README.md @@ -1,5 +1,5 @@ Periodic SCCS SCF for neutral H2 with the original water-neutral preset. -Checks total energy and electrostatic/non-electrostatic solvation components. +Checks total energy, atomic forces and electrostatic/non-electrostatic solvation components. Uses ecutwfc=20 and the default density cutoff for short regression coverage; this case does not establish physical cutoff convergence. Reference values come from the original SCCS implementation at identical inputs and two MPI ranks. diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/result.ref b/tests/02_NAO_Gamma/scf_sccs_h2/result.ref index 72d6f6ffa6a..7ff831faf0a 100644 --- a/tests/02_NAO_Gamma/scf_sccs_h2/result.ref +++ b/tests/02_NAO_Gamma/scf_sccs_h2/result.ref @@ -1,5 +1,6 @@ etotref -31.67490984733957 etotperatomref -15.8374549237 +totalforceref 1.216748 esolelref -0.0091165409 esolcavref 0.0640459014 -totaltimeref 0.71 +totaltimeref 0.67 From 1b5763eb9dbb4e9e2595e25f57b70f523fad6d45 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Sun, 4 Oct 2026 22:04:01 -0600 Subject: [PATCH 19/41] refactor(sccs): minimize public header dependencies Move polarization solver parameters into the configuration header and use forward declarations for referenced SCCS types. Include complete types directly in implementations and tests. Validation: abacus_basic_para and MODULE_HAMILT_sccs_poisson rebuilt; SCCS serial and MPI CTest entries passed (2/2); four affected headers passed standalone C++11 syntax checks; staged governance and diff whitespace checks passed with review warnings only. Header warnings cover standard string/vector declarations needed by the APIs and owned containers. No INPUT or numerical behavior changes; user documentation updates are not required. --- source/source_estate/module_pot/pot_sccs.cpp | 1 + source/source_estate/module_pot/pot_sccs.h | 1 - source/source_hamilt/module_sccs/sccs_functional.cpp | 2 ++ source/source_hamilt/module_sccs/sccs_functional.h | 12 ++++++++++-- source/source_hamilt/module_sccs/sccs_parameters.h | 9 +++++++++ source/source_hamilt/module_sccs/sccs_response.cpp | 1 + source/source_hamilt/module_sccs/sccs_response.h | 8 ++------ .../module_sccs/test/test_sccs_functional.cpp | 2 ++ .../module_sccs/test/test_sccs_ionic_force.cpp | 1 + .../module_sccs/test/test_sccs_response.cpp | 1 + 10 files changed, 29 insertions(+), 9 deletions(-) diff --git a/source/source_estate/module_pot/pot_sccs.cpp b/source/source_estate/module_pot/pot_sccs.cpp index 43b09d947b6..23f8d34b324 100644 --- a/source/source_estate/module_pot/pot_sccs.cpp +++ b/source/source_estate/module_pot/pot_sccs.cpp @@ -7,6 +7,7 @@ #include "source_hamilt/module_sccs/sccs_functional.h" #include "source_hamilt/module_sccs/sccs_ionic_charge.h" #include "source_hamilt/module_sccs/sccs_ionic_force.h" +#include "source_hamilt/module_sccs/sccs_response.h" #include "source_io/module_parameter/input_parameter.h" #include diff --git a/source/source_estate/module_pot/pot_sccs.h b/source/source_estate/module_pot/pot_sccs.h index be723a359ec..92bf8cab8ca 100644 --- a/source/source_estate/module_pot/pot_sccs.h +++ b/source/source_estate/module_pot/pot_sccs.h @@ -3,7 +3,6 @@ #include "pot_base.h" #include "source_hamilt/module_sccs/sccs_parameters.h" -#include "source_hamilt/module_sccs/sccs_response.h" struct Input_para; namespace elecstate diff --git a/source/source_hamilt/module_sccs/sccs_functional.cpp b/source/source_hamilt/module_sccs/sccs_functional.cpp index eceab111234..2b2b4d52cc4 100644 --- a/source/source_hamilt/module_sccs/sccs_functional.cpp +++ b/source/source_hamilt/module_sccs/sccs_functional.cpp @@ -1,4 +1,6 @@ #include "sccs_functional.h" +#include "sccs_parameters.h" +#include "sccs_response.h" #include "sccs_pw_coulomb.h" #include "source_base/parallel_reduce.h" diff --git a/source/source_hamilt/module_sccs/sccs_functional.h b/source/source_hamilt/module_sccs/sccs_functional.h index 7a2f5b70046..35ca17cdde7 100644 --- a/source/source_hamilt/module_sccs/sccs_functional.h +++ b/source/source_hamilt/module_sccs/sccs_functional.h @@ -1,11 +1,19 @@ #ifndef SCCS_FUNCTIONAL_H #define SCCS_FUNCTIONAL_H -#include "sccs_parameters.h" -#include "sccs_response.h" +#include +#include + +namespace ModulePW +{ +class PW_Basis; +} namespace ModuleSccs { +struct SccsConfig; +struct SccsResponse; + struct FunctionalResult { double reaction_energy = 0.0; // Hartree diff --git a/source/source_hamilt/module_sccs/sccs_parameters.h b/source/source_hamilt/module_sccs/sccs_parameters.h index e45ad4093c5..6f16df44efe 100644 --- a/source/source_hamilt/module_sccs/sccs_parameters.h +++ b/source/source_hamilt/module_sccs/sccs_parameters.h @@ -3,6 +3,8 @@ #include "sccs_cavity.h" +#include + namespace ModuleSccs { enum class Preset { Custom, Vacuum, WaterNeutral, WaterCation, WaterAnion }; @@ -15,6 +17,13 @@ struct SccsConfig double surface_regularization = 0.0; }; +struct PolarizationSolverParameters +{ + int max_iterations = 200; + double tolerance_rms = 1e-10; + double tolerance_max = 1e-8; +}; + // Failure leaves the output unchanged. Custom configurations are supplied by callers. bool parse_preset(const std::string& name, Preset& preset, std::string& error); bool make_sccs_config(Preset preset, SccsConfig& config, std::string& error); diff --git a/source/source_hamilt/module_sccs/sccs_response.cpp b/source/source_hamilt/module_sccs/sccs_response.cpp index 039ca1643aa..dd79dada12d 100644 --- a/source/source_hamilt/module_sccs/sccs_response.cpp +++ b/source/source_hamilt/module_sccs/sccs_response.cpp @@ -1,4 +1,5 @@ #include "sccs_response.h" +#include "sccs_parameters.h" #include "sccs_pw_coulomb.h" #include "source_base/constants.h" diff --git a/source/source_hamilt/module_sccs/sccs_response.h b/source/source_hamilt/module_sccs/sccs_response.h index a8060a9d3a1..0befd59f7ff 100644 --- a/source/source_hamilt/module_sccs/sccs_response.h +++ b/source/source_hamilt/module_sccs/sccs_response.h @@ -4,6 +4,7 @@ #include "sccs_cavity.h" #include "source_base/vector3.h" +#include #include namespace ModulePW @@ -13,12 +14,7 @@ class PW_Basis; namespace ModuleSccs { -struct PolarizationSolverParameters -{ - int max_iterations = 200; - double tolerance_rms = 1e-10; - double tolerance_max = 1e-8; -}; +struct PolarizationSolverParameters; struct PolarizationResult { diff --git a/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp b/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp index 21487b422e6..2e92399542c 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp @@ -1,5 +1,7 @@ #include "sccs_test.h" #include "../sccs_functional.h" +#include "../sccs_parameters.h" +#include "../sccs_response.h" #include "source_base/parallel_reduce.h" using SccsFunctionalTest = SccsTest::PwTest; diff --git a/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp b/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp index 1e03925511d..846c9502571 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp @@ -2,6 +2,7 @@ #include "../sccs_ionic_force.h" #include "../sccs_ionic_charge.h" #include "../sccs_response.h" +#include "../sccs_parameters.h" #include "../sccs_functional.h" #include "source_cell/cell_tools.h" diff --git a/source/source_hamilt/module_sccs/test/test_sccs_response.cpp b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp index 0c36591345d..da061b95b35 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_response.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp @@ -1,5 +1,6 @@ #include "sccs_test.h" #include "../sccs_response.h" +#include "../sccs_parameters.h" #include "source_base/parallel_reduce.h" From 98629b2787d169d191ce6954845f075ef9170718 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Mon, 5 Oct 2026 20:43:36 -0600 Subject: [PATCH 20/41] test(sccs): remove added H2 integration examples --- tests/01_PW/CASES_CPU.txt | 1 - tests/01_PW/scf_sccs_h2/INPUT | 22 ---------------------- tests/01_PW/scf_sccs_h2/KPT | 4 ---- tests/01_PW/scf_sccs_h2/README.md | 5 ----- tests/01_PW/scf_sccs_h2/STRU | 20 -------------------- tests/01_PW/scf_sccs_h2/result.ref | 6 ------ tests/02_NAO_Gamma/CASES_CPU.txt | 1 - tests/02_NAO_Gamma/scf_sccs_h2/INPUT | 23 ----------------------- tests/02_NAO_Gamma/scf_sccs_h2/KPT | 4 ---- tests/02_NAO_Gamma/scf_sccs_h2/README.md | 5 ----- tests/02_NAO_Gamma/scf_sccs_h2/STRU | 23 ----------------------- tests/02_NAO_Gamma/scf_sccs_h2/result.ref | 6 ------ 12 files changed, 120 deletions(-) delete mode 100644 tests/01_PW/scf_sccs_h2/INPUT delete mode 100644 tests/01_PW/scf_sccs_h2/KPT delete mode 100644 tests/01_PW/scf_sccs_h2/README.md delete mode 100644 tests/01_PW/scf_sccs_h2/STRU delete mode 100644 tests/01_PW/scf_sccs_h2/result.ref delete mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/INPUT delete mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/KPT delete mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/README.md delete mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/STRU delete mode 100644 tests/02_NAO_Gamma/scf_sccs_h2/result.ref diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index 786d12b6f3c..324309c3fb2 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -140,4 +140,3 @@ scf_deltaspin2 scf_deltaspin4 221_PW_PCC0D_H3O 222_PW_PCC2D_H3O -scf_sccs_h2 diff --git a/tests/01_PW/scf_sccs_h2/INPUT b/tests/01_PW/scf_sccs_h2/INPUT deleted file mode 100644 index 9ced12b8b91..00000000000 --- a/tests/01_PW/scf_sccs_h2/INPUT +++ /dev/null @@ -1,22 +0,0 @@ -INPUT_PARAMETERS -suffix autotest -calculation scf -basis_type pw -ks_solver cg -pseudo_dir ../../PP_ORB -orbital_dir ../../PP_ORB -ntype 1 -nbands 2 -ecutwfc 20 -scf_thr 1e-9 -scf_nmax 200 -mixing_type pulay -mixing_beta 0.2 -smearing_method fixed -symmetry 0 -cal_force 1 -init_wfc atomic -imp_sol 2 -sccs_preset water-neutral -assume_isolated none -nspin 1 diff --git a/tests/01_PW/scf_sccs_h2/KPT b/tests/01_PW/scf_sccs_h2/KPT deleted file mode 100644 index c289c0158aa..00000000000 --- a/tests/01_PW/scf_sccs_h2/KPT +++ /dev/null @@ -1,4 +0,0 @@ -K_POINTS -0 -Gamma -1 1 1 0 0 0 diff --git a/tests/01_PW/scf_sccs_h2/README.md b/tests/01_PW/scf_sccs_h2/README.md deleted file mode 100644 index 198d6aa65cb..00000000000 --- a/tests/01_PW/scf_sccs_h2/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Periodic SCCS SCF for neutral H2 with the original water-neutral preset. -Checks total energy, atomic forces and electrostatic/non-electrostatic solvation components. -Uses ecutwfc=20 and the default density cutoff for short regression coverage; -this case does not establish physical cutoff convergence. Reference values -come from the original SCCS implementation at identical inputs and two MPI ranks. diff --git a/tests/01_PW/scf_sccs_h2/STRU b/tests/01_PW/scf_sccs_h2/STRU deleted file mode 100644 index d0697276a56..00000000000 --- a/tests/01_PW/scf_sccs_h2/STRU +++ /dev/null @@ -1,20 +0,0 @@ -ATOMIC_SPECIES -H 1.008 H_ONCV_PBE-1.0.upf - -LATTICE_CONSTANT -1 - -LATTICE_VECTORS -10 0 0 -0 10 0 -0 0 10 - -ATOMIC_POSITIONS -Cartesian # Cartesian(Unit is LATTICE_CONSTANT) - - -H -0.0 -2 -0.00 0.00 -0.70 0 0 0 -0.00 0.01 0.70 0 0 0 diff --git a/tests/01_PW/scf_sccs_h2/result.ref b/tests/01_PW/scf_sccs_h2/result.ref deleted file mode 100644 index 21ddf000f55..00000000000 --- a/tests/01_PW/scf_sccs_h2/result.ref +++ /dev/null @@ -1,6 +0,0 @@ -etotref -31.0663258292030271 -etotperatomref -15.5331629146 -totalforceref 2.004916 -esolelref -0.0200563129 -esolcavref 0.0667246746 -totaltimeref 0.21 diff --git a/tests/02_NAO_Gamma/CASES_CPU.txt b/tests/02_NAO_Gamma/CASES_CPU.txt index e6ee5f51a94..9ba3fa93de9 100644 --- a/tests/02_NAO_Gamma/CASES_CPU.txt +++ b/tests/02_NAO_Gamma/CASES_CPU.txt @@ -40,4 +40,3 @@ scf_upf100 test.sum scf_pcc0d_water scf_pcc2d_water -scf_sccs_h2 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/INPUT b/tests/02_NAO_Gamma/scf_sccs_h2/INPUT deleted file mode 100644 index 3bba4ac7c1f..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_h2/INPUT +++ /dev/null @@ -1,23 +0,0 @@ -INPUT_PARAMETERS -suffix autotest -calculation scf -basis_type lcao -ks_solver genelpa -pseudo_dir ../../PP_ORB -orbital_dir ../../PP_ORB -ntype 1 -nbands 2 -ecutwfc 20 -scf_thr 1e-7 -scf_nmax 200 -mixing_type pulay -mixing_beta 0.2 -smearing_method fixed -symmetry 0 -cal_force 1 -init_wfc atomic -imp_sol 2 -sccs_preset water-neutral -assume_isolated none -nspin 2 -gamma_only 1 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/KPT b/tests/02_NAO_Gamma/scf_sccs_h2/KPT deleted file mode 100644 index c289c0158aa..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_h2/KPT +++ /dev/null @@ -1,4 +0,0 @@ -K_POINTS -0 -Gamma -1 1 1 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/README.md b/tests/02_NAO_Gamma/scf_sccs_h2/README.md deleted file mode 100644 index 198d6aa65cb..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_h2/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Periodic SCCS SCF for neutral H2 with the original water-neutral preset. -Checks total energy, atomic forces and electrostatic/non-electrostatic solvation components. -Uses ecutwfc=20 and the default density cutoff for short regression coverage; -this case does not establish physical cutoff convergence. Reference values -come from the original SCCS implementation at identical inputs and two MPI ranks. diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/STRU b/tests/02_NAO_Gamma/scf_sccs_h2/STRU deleted file mode 100644 index bf3cff73960..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_h2/STRU +++ /dev/null @@ -1,23 +0,0 @@ -ATOMIC_SPECIES -H 1.008 H_ONCV_PBE-1.0.upf - -NUMERICAL_ORBITAL -H_gga_8au_100Ry_2s1p.orb - -LATTICE_CONSTANT -1 - -LATTICE_VECTORS -10 0 0 -0 10 0 -0 0 10 - -ATOMIC_POSITIONS -Cartesian # Cartesian(Unit is LATTICE_CONSTANT) - - -H -0.0 -2 -0.00 0.00 -0.70 0 0 0 -0.00 0.01 0.70 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_sccs_h2/result.ref b/tests/02_NAO_Gamma/scf_sccs_h2/result.ref deleted file mode 100644 index 7ff831faf0a..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_h2/result.ref +++ /dev/null @@ -1,6 +0,0 @@ -etotref -31.67490984733957 -etotperatomref -15.8374549237 -totalforceref 1.216748 -esolelref -0.0091165409 -esolcavref 0.0640459014 -totaltimeref 0.67 From b2f17f1278ed26eef20a359c985bf1374d3fcf0b Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Mon, 5 Oct 2026 21:03:23 -0600 Subject: [PATCH 21/41] refactor(pcc): name computed call arguments --- source/source_cell/cell_geometry.cpp | 6 ++++-- source/source_cell/test/test_cell_geometry.cpp | 9 ++++++--- source/source_estate/module_pot/pot_pcc.cpp | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/source/source_cell/cell_geometry.cpp b/source/source_cell/cell_geometry.cpp index 896b1839b74..fb1b212fe2e 100644 --- a/source/source_cell/cell_geometry.cpp +++ b/source/source_cell/cell_geometry.cpp @@ -188,7 +188,8 @@ bool make_slab_cell(const ModuleBase::Matrix3& lattice, double relative_coordinate(const ModuleBase::Vector3& position, const SlabCell& cell) { const double displacement = position * cell.normal - cell.origin; - const double image = std::floor(displacement / cell.length + 0.5); + const double image_coordinate = displacement / cell.length + 0.5; + const double image = std::floor(image_coordinate); return displacement - cell.length * image; } @@ -220,7 +221,8 @@ bool weighted_center(const std::vector>& positions, total_weight += weights[index]; } const double unwrapped = reference.origin + weighted_displacement / total_weight; - const double image = std::floor(unwrapped / cell.length); + const double image_coordinate = unwrapped / cell.length; + const double image = std::floor(image_coordinate); const double candidate = unwrapped - image * cell.length; if (!std::isfinite(total_weight) || !std::isfinite(weighted_displacement) || !std::isfinite(candidate)) { diff --git a/source/source_cell/test/test_cell_geometry.cpp b/source/source_cell/test/test_cell_geometry.cpp index df9703a67ad..b9a39fec6a3 100644 --- a/source/source_cell/test/test_cell_geometry.cpp +++ b/source/source_cell/test/test_cell_geometry.cpp @@ -86,9 +86,12 @@ TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) std::vector> translated = original; for (auto& position : translated) { - position.x = std::fmod(position.x + 1.4, 10.0); - position.y = std::fmod(position.y + 1.4, 10.0); - position.z = std::fmod(position.z + 1.4, 10.0); + const double translated_x = position.x + 1.4; + const double translated_y = position.y + 1.4; + const double translated_z = position.z + 1.4; + position.x = std::fmod(translated_x, 10.0); + position.y = std::fmod(translated_y, 10.0); + position.z = std::fmod(translated_z, 10.0); } ASSERT_TRUE(unitcell::weighted_center(translated, weights, cell, center, error)); unitcell::OrthogonalCell second = cell; diff --git a/source/source_estate/module_pot/pot_pcc.cpp b/source/source_estate/module_pot/pot_pcc.cpp index 14fdc69824e..c5e9cf29c31 100644 --- a/source/source_estate/module_pot/pot_pcc.cpp +++ b/source/source_estate/module_pot/pot_pcc.cpp @@ -32,8 +32,11 @@ bool fractional_translation(const ModuleBase::Matrix3& rotation, const ModuleBase::Vector3& translation) { const double tolerance = 1.0e-6; - const double identity_error = std::abs(rotation.e11 - 1.0) + std::abs(rotation.e22 - 1.0) - + std::abs(rotation.e33 - 1.0) + std::abs(rotation.e12) + const double diagonal_error_x = rotation.e11 - 1.0; + const double diagonal_error_y = rotation.e22 - 1.0; + const double diagonal_error_z = rotation.e33 - 1.0; + const double identity_error = std::abs(diagonal_error_x) + std::abs(diagonal_error_y) + + std::abs(diagonal_error_z) + std::abs(rotation.e12) + std::abs(rotation.e13) + std::abs(rotation.e21) + std::abs(rotation.e23) + std::abs(rotation.e31) + std::abs(rotation.e32); From fa8ca28ba35c68041d222e9b1cf5ab739cbbb4b9 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Mon, 5 Oct 2026 21:16:17 -0600 Subject: [PATCH 22/41] test(sccs): name expected accumulated force --- source/source_estate/test/test_pot_sccs.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp index 32069c591bc..1b42d15f21e 100644 --- a/source/source_estate/test/test_pot_sccs.cpp +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -153,7 +153,8 @@ TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) { for (int axis = 0; axis < 3; ++axis) { - EXPECT_NEAR(accumulated(ia, axis), 7.0 + force(ia, axis), 1e-10); + const double expected_force = 7.0 + force(ia, axis); + EXPECT_NEAR(accumulated(ia, axis), expected_force, 1e-10); } } } From 6024f505fe73f689990dd1017e7073788b20d0ee Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Mon, 5 Oct 2026 21:17:32 -0600 Subject: [PATCH 23/41] test(sccs): cover neutral water in PW and LCAO integration suites --- tests/01_PW/CASES_CPU.txt | 1 + tests/01_PW/scf_sccs_water/INPUT | 24 ++++++++++++++++++ tests/01_PW/scf_sccs_water/KPT | 4 +++ tests/01_PW/scf_sccs_water/README.md | 3 +++ tests/01_PW/scf_sccs_water/STRU | 24 ++++++++++++++++++ tests/01_PW/scf_sccs_water/result.ref | 6 +++++ tests/02_NAO_Gamma/CASES_CPU.txt | 1 + tests/02_NAO_Gamma/scf_sccs_water/INPUT | 26 ++++++++++++++++++++ tests/02_NAO_Gamma/scf_sccs_water/KPT | 4 +++ tests/02_NAO_Gamma/scf_sccs_water/README.md | 3 +++ tests/02_NAO_Gamma/scf_sccs_water/STRU | 24 ++++++++++++++++++ tests/02_NAO_Gamma/scf_sccs_water/result.ref | 6 +++++ 12 files changed, 126 insertions(+) create mode 100644 tests/01_PW/scf_sccs_water/INPUT create mode 100644 tests/01_PW/scf_sccs_water/KPT create mode 100644 tests/01_PW/scf_sccs_water/README.md create mode 100644 tests/01_PW/scf_sccs_water/STRU create mode 100644 tests/01_PW/scf_sccs_water/result.ref create mode 100644 tests/02_NAO_Gamma/scf_sccs_water/INPUT create mode 100644 tests/02_NAO_Gamma/scf_sccs_water/KPT create mode 100644 tests/02_NAO_Gamma/scf_sccs_water/README.md create mode 100644 tests/02_NAO_Gamma/scf_sccs_water/STRU create mode 100644 tests/02_NAO_Gamma/scf_sccs_water/result.ref diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index 324309c3fb2..af4ee0db752 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -140,3 +140,4 @@ scf_deltaspin2 scf_deltaspin4 221_PW_PCC0D_H3O 222_PW_PCC2D_H3O +scf_sccs_water diff --git a/tests/01_PW/scf_sccs_water/INPUT b/tests/01_PW/scf_sccs_water/INPUT new file mode 100644 index 00000000000..3e89820ac73 --- /dev/null +++ b/tests/01_PW/scf_sccs_water/INPUT @@ -0,0 +1,24 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type pw +ks_solver cg +pseudo_dir ../../PP_ORB +ntype 2 +nbands 8 +ecutwfc 20 +scf_thr 1e-9 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 1 +init_wfc atomic +imp_sol 2 +sccs_preset custom +sccs_epsilon 1.1 +sccs_maxiter 200 +sccs_tol_rms 1e-10 +sccs_tol_max 1e-8 +pw_seed 1 diff --git a/tests/01_PW/scf_sccs_water/KPT b/tests/01_PW/scf_sccs_water/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/01_PW/scf_sccs_water/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/01_PW/scf_sccs_water/README.md b/tests/01_PW/scf_sccs_water/README.md new file mode 100644 index 00000000000..ace2344ea57 --- /dev/null +++ b/tests/01_PW/scf_sccs_water/README.md @@ -0,0 +1,3 @@ +Neutral periodic H2O SCCS regression. + +Uses epsilon=1.1 for a short 20 Ry CI regression. Higher-dielectric force and cell convergence are validated separately. No explicit ecutrho. The integration harness checks total energy, force magnitudes, solvation energies and PCC energy where enabled. References use 2 MPI ranks and OMP_NUM_THREADS=1. diff --git a/tests/01_PW/scf_sccs_water/STRU b/tests/01_PW/scf_sccs_water/STRU new file mode 100644 index 00000000000..3a364a9a95e --- /dev/null +++ b/tests/01_PW/scf_sccs_water/STRU @@ -0,0 +1,24 @@ +ATOMIC_SPECIES +H 1.008 H_ONCV_PBE-1.0.upf +O 15.9994 O_ONCV_PBE-1.0.upf + +LATTICE_CONSTANT +1.0 + +LATTICE_VECTORS +12.0 0.0 0.0 +0.0 12.0 0.0 +0.0 0.0 12.0 + +ATOMIC_POSITIONS +Cartesian + +H +0.0 +2 +7.433 7.111 6.0 1 1 1 +4.567 7.111 6.0 1 1 1 +O +0.0 +1 +6.0 6.0 6.0 1 1 1 diff --git a/tests/01_PW/scf_sccs_water/result.ref b/tests/01_PW/scf_sccs_water/result.ref new file mode 100644 index 00000000000..cb2eba3394b --- /dev/null +++ b/tests/01_PW/scf_sccs_water/result.ref @@ -0,0 +1,6 @@ +etotref -441.5071772039756297 +etotperatomref -147.1690590680 +totalforceref 16.915180 +esolelref -0.0092634327 +esolcavref 0.0000000000 +totaltimeref 0.51 diff --git a/tests/02_NAO_Gamma/CASES_CPU.txt b/tests/02_NAO_Gamma/CASES_CPU.txt index 9ba3fa93de9..37a3e8404fc 100644 --- a/tests/02_NAO_Gamma/CASES_CPU.txt +++ b/tests/02_NAO_Gamma/CASES_CPU.txt @@ -40,3 +40,4 @@ scf_upf100 test.sum scf_pcc0d_water scf_pcc2d_water +scf_sccs_water diff --git a/tests/02_NAO_Gamma/scf_sccs_water/INPUT b/tests/02_NAO_Gamma/scf_sccs_water/INPUT new file mode 100644 index 00000000000..420f8a2145f --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_water/INPUT @@ -0,0 +1,26 @@ +INPUT_PARAMETERS +suffix autotest +calculation scf +basis_type lcao +ks_solver genelpa +pseudo_dir ../../PP_ORB +ntype 2 +nbands 8 +ecutwfc 20 +scf_thr 1e-7 +scf_nmax 200 +mixing_type pulay +mixing_beta 0.2 +smearing_method fixed +symmetry 0 +cal_force 1 +init_wfc atomic +imp_sol 2 +sccs_preset custom +sccs_epsilon 1.01 +sccs_maxiter 200 +sccs_tol_rms 1e-10 +sccs_tol_max 1e-8 +gamma_only 1 +orbital_dir ../../PP_ORB +assume_isolated none diff --git a/tests/02_NAO_Gamma/scf_sccs_water/KPT b/tests/02_NAO_Gamma/scf_sccs_water/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_water/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_sccs_water/README.md b/tests/02_NAO_Gamma/scf_sccs_water/README.md new file mode 100644 index 00000000000..f0c89c7dd42 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_water/README.md @@ -0,0 +1,3 @@ +Neutral periodic H2O SCCS regression. + +Uses epsilon=1.01 for a short 20 Ry CI regression. Higher-dielectric force and cell convergence are validated separately. No explicit ecutrho. The integration harness checks total energy, force magnitudes, solvation energies and PCC energy where enabled. References use 2 MPI ranks and OMP_NUM_THREADS=1. diff --git a/tests/02_NAO_Gamma/scf_sccs_water/STRU b/tests/02_NAO_Gamma/scf_sccs_water/STRU new file mode 100644 index 00000000000..331f96c9743 --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_water/STRU @@ -0,0 +1,24 @@ +ATOMIC_SPECIES +O 15.999 O_ONCV_PBE-1.0.upf +H 1.008 H_ONCV_PBE-1.0.upf +NUMERICAL_ORBITAL +O_gga_7au_100Ry_2s2p1d.orb +H_gga_8au_100Ry_2s1p.orb + +LATTICE_CONSTANT +1.8897261258369282 +LATTICE_VECTORS +12.0 0 0 +0 12.0 0 +0 0 12.0 +ATOMIC_POSITIONS +Cartesian +O +0 +1 +6.0 6.0 6.0 1 1 1 +H +0 +2 +6.75716000000000 6.0 6.58626 1 1 1 +5.24284 6.0 6.58626 1 1 1 diff --git a/tests/02_NAO_Gamma/scf_sccs_water/result.ref b/tests/02_NAO_Gamma/scf_sccs_water/result.ref new file mode 100644 index 00000000000..f98097e646e --- /dev/null +++ b/tests/02_NAO_Gamma/scf_sccs_water/result.ref @@ -0,0 +1,6 @@ +etotref -466.4985831363314 +etotperatomref -155.4995277121 +totalforceref 3.901056 +esolelref 0.0479655039 +esolcavref 0.0000000000 +totaltimeref 4.08 From 139482ef0fc11d6235aefd1821bfa6b0d2add73c Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Mon, 5 Oct 2026 21:26:55 -0600 Subject: [PATCH 24/41] Revert "test(sccs): cover neutral water in PW and LCAO integration suites" This reverts commit 6024f505fe73f689990dd1017e7073788b20d0ee. --- tests/01_PW/CASES_CPU.txt | 1 - tests/01_PW/scf_sccs_water/INPUT | 24 ------------------ tests/01_PW/scf_sccs_water/KPT | 4 --- tests/01_PW/scf_sccs_water/README.md | 3 --- tests/01_PW/scf_sccs_water/STRU | 24 ------------------ tests/01_PW/scf_sccs_water/result.ref | 6 ----- tests/02_NAO_Gamma/CASES_CPU.txt | 1 - tests/02_NAO_Gamma/scf_sccs_water/INPUT | 26 -------------------- tests/02_NAO_Gamma/scf_sccs_water/KPT | 4 --- tests/02_NAO_Gamma/scf_sccs_water/README.md | 3 --- tests/02_NAO_Gamma/scf_sccs_water/STRU | 24 ------------------ tests/02_NAO_Gamma/scf_sccs_water/result.ref | 6 ----- 12 files changed, 126 deletions(-) delete mode 100644 tests/01_PW/scf_sccs_water/INPUT delete mode 100644 tests/01_PW/scf_sccs_water/KPT delete mode 100644 tests/01_PW/scf_sccs_water/README.md delete mode 100644 tests/01_PW/scf_sccs_water/STRU delete mode 100644 tests/01_PW/scf_sccs_water/result.ref delete mode 100644 tests/02_NAO_Gamma/scf_sccs_water/INPUT delete mode 100644 tests/02_NAO_Gamma/scf_sccs_water/KPT delete mode 100644 tests/02_NAO_Gamma/scf_sccs_water/README.md delete mode 100644 tests/02_NAO_Gamma/scf_sccs_water/STRU delete mode 100644 tests/02_NAO_Gamma/scf_sccs_water/result.ref diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index af4ee0db752..324309c3fb2 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -140,4 +140,3 @@ scf_deltaspin2 scf_deltaspin4 221_PW_PCC0D_H3O 222_PW_PCC2D_H3O -scf_sccs_water diff --git a/tests/01_PW/scf_sccs_water/INPUT b/tests/01_PW/scf_sccs_water/INPUT deleted file mode 100644 index 3e89820ac73..00000000000 --- a/tests/01_PW/scf_sccs_water/INPUT +++ /dev/null @@ -1,24 +0,0 @@ -INPUT_PARAMETERS -suffix autotest -calculation scf -basis_type pw -ks_solver cg -pseudo_dir ../../PP_ORB -ntype 2 -nbands 8 -ecutwfc 20 -scf_thr 1e-9 -scf_nmax 200 -mixing_type pulay -mixing_beta 0.2 -smearing_method fixed -symmetry 0 -cal_force 1 -init_wfc atomic -imp_sol 2 -sccs_preset custom -sccs_epsilon 1.1 -sccs_maxiter 200 -sccs_tol_rms 1e-10 -sccs_tol_max 1e-8 -pw_seed 1 diff --git a/tests/01_PW/scf_sccs_water/KPT b/tests/01_PW/scf_sccs_water/KPT deleted file mode 100644 index c289c0158aa..00000000000 --- a/tests/01_PW/scf_sccs_water/KPT +++ /dev/null @@ -1,4 +0,0 @@ -K_POINTS -0 -Gamma -1 1 1 0 0 0 diff --git a/tests/01_PW/scf_sccs_water/README.md b/tests/01_PW/scf_sccs_water/README.md deleted file mode 100644 index ace2344ea57..00000000000 --- a/tests/01_PW/scf_sccs_water/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Neutral periodic H2O SCCS regression. - -Uses epsilon=1.1 for a short 20 Ry CI regression. Higher-dielectric force and cell convergence are validated separately. No explicit ecutrho. The integration harness checks total energy, force magnitudes, solvation energies and PCC energy where enabled. References use 2 MPI ranks and OMP_NUM_THREADS=1. diff --git a/tests/01_PW/scf_sccs_water/STRU b/tests/01_PW/scf_sccs_water/STRU deleted file mode 100644 index 3a364a9a95e..00000000000 --- a/tests/01_PW/scf_sccs_water/STRU +++ /dev/null @@ -1,24 +0,0 @@ -ATOMIC_SPECIES -H 1.008 H_ONCV_PBE-1.0.upf -O 15.9994 O_ONCV_PBE-1.0.upf - -LATTICE_CONSTANT -1.0 - -LATTICE_VECTORS -12.0 0.0 0.0 -0.0 12.0 0.0 -0.0 0.0 12.0 - -ATOMIC_POSITIONS -Cartesian - -H -0.0 -2 -7.433 7.111 6.0 1 1 1 -4.567 7.111 6.0 1 1 1 -O -0.0 -1 -6.0 6.0 6.0 1 1 1 diff --git a/tests/01_PW/scf_sccs_water/result.ref b/tests/01_PW/scf_sccs_water/result.ref deleted file mode 100644 index cb2eba3394b..00000000000 --- a/tests/01_PW/scf_sccs_water/result.ref +++ /dev/null @@ -1,6 +0,0 @@ -etotref -441.5071772039756297 -etotperatomref -147.1690590680 -totalforceref 16.915180 -esolelref -0.0092634327 -esolcavref 0.0000000000 -totaltimeref 0.51 diff --git a/tests/02_NAO_Gamma/CASES_CPU.txt b/tests/02_NAO_Gamma/CASES_CPU.txt index 37a3e8404fc..9ba3fa93de9 100644 --- a/tests/02_NAO_Gamma/CASES_CPU.txt +++ b/tests/02_NAO_Gamma/CASES_CPU.txt @@ -40,4 +40,3 @@ scf_upf100 test.sum scf_pcc0d_water scf_pcc2d_water -scf_sccs_water diff --git a/tests/02_NAO_Gamma/scf_sccs_water/INPUT b/tests/02_NAO_Gamma/scf_sccs_water/INPUT deleted file mode 100644 index 420f8a2145f..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_water/INPUT +++ /dev/null @@ -1,26 +0,0 @@ -INPUT_PARAMETERS -suffix autotest -calculation scf -basis_type lcao -ks_solver genelpa -pseudo_dir ../../PP_ORB -ntype 2 -nbands 8 -ecutwfc 20 -scf_thr 1e-7 -scf_nmax 200 -mixing_type pulay -mixing_beta 0.2 -smearing_method fixed -symmetry 0 -cal_force 1 -init_wfc atomic -imp_sol 2 -sccs_preset custom -sccs_epsilon 1.01 -sccs_maxiter 200 -sccs_tol_rms 1e-10 -sccs_tol_max 1e-8 -gamma_only 1 -orbital_dir ../../PP_ORB -assume_isolated none diff --git a/tests/02_NAO_Gamma/scf_sccs_water/KPT b/tests/02_NAO_Gamma/scf_sccs_water/KPT deleted file mode 100644 index c289c0158aa..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_water/KPT +++ /dev/null @@ -1,4 +0,0 @@ -K_POINTS -0 -Gamma -1 1 1 0 0 0 diff --git a/tests/02_NAO_Gamma/scf_sccs_water/README.md b/tests/02_NAO_Gamma/scf_sccs_water/README.md deleted file mode 100644 index f0c89c7dd42..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_water/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Neutral periodic H2O SCCS regression. - -Uses epsilon=1.01 for a short 20 Ry CI regression. Higher-dielectric force and cell convergence are validated separately. No explicit ecutrho. The integration harness checks total energy, force magnitudes, solvation energies and PCC energy where enabled. References use 2 MPI ranks and OMP_NUM_THREADS=1. diff --git a/tests/02_NAO_Gamma/scf_sccs_water/STRU b/tests/02_NAO_Gamma/scf_sccs_water/STRU deleted file mode 100644 index 331f96c9743..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_water/STRU +++ /dev/null @@ -1,24 +0,0 @@ -ATOMIC_SPECIES -O 15.999 O_ONCV_PBE-1.0.upf -H 1.008 H_ONCV_PBE-1.0.upf -NUMERICAL_ORBITAL -O_gga_7au_100Ry_2s2p1d.orb -H_gga_8au_100Ry_2s1p.orb - -LATTICE_CONSTANT -1.8897261258369282 -LATTICE_VECTORS -12.0 0 0 -0 12.0 0 -0 0 12.0 -ATOMIC_POSITIONS -Cartesian -O -0 -1 -6.0 6.0 6.0 1 1 1 -H -0 -2 -6.75716000000000 6.0 6.58626 1 1 1 -5.24284 6.0 6.58626 1 1 1 diff --git a/tests/02_NAO_Gamma/scf_sccs_water/result.ref b/tests/02_NAO_Gamma/scf_sccs_water/result.ref deleted file mode 100644 index f98097e646e..00000000000 --- a/tests/02_NAO_Gamma/scf_sccs_water/result.ref +++ /dev/null @@ -1,6 +0,0 @@ -etotref -466.4985831363314 -etotperatomref -155.4995277121 -totalforceref 3.901056 -esolelref 0.0479655039 -esolcavref 0.0000000000 -totaltimeref 4.08 From 2487df4e2a1ee91f108b4ce50a8a7eb984c27d74 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Mon, 5 Oct 2026 21:03:23 -0600 Subject: [PATCH 25/41] refactor(pcc): name computed call arguments --- source/source_cell/cell_geometry.cpp | 6 ++++-- source/source_cell/test/test_cell_geometry.cpp | 9 ++++++--- source/source_estate/module_pot/pot_pcc.cpp | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/source/source_cell/cell_geometry.cpp b/source/source_cell/cell_geometry.cpp index 896b1839b74..fb1b212fe2e 100644 --- a/source/source_cell/cell_geometry.cpp +++ b/source/source_cell/cell_geometry.cpp @@ -188,7 +188,8 @@ bool make_slab_cell(const ModuleBase::Matrix3& lattice, double relative_coordinate(const ModuleBase::Vector3& position, const SlabCell& cell) { const double displacement = position * cell.normal - cell.origin; - const double image = std::floor(displacement / cell.length + 0.5); + const double image_coordinate = displacement / cell.length + 0.5; + const double image = std::floor(image_coordinate); return displacement - cell.length * image; } @@ -220,7 +221,8 @@ bool weighted_center(const std::vector>& positions, total_weight += weights[index]; } const double unwrapped = reference.origin + weighted_displacement / total_weight; - const double image = std::floor(unwrapped / cell.length); + const double image_coordinate = unwrapped / cell.length; + const double image = std::floor(image_coordinate); const double candidate = unwrapped - image * cell.length; if (!std::isfinite(total_weight) || !std::isfinite(weighted_displacement) || !std::isfinite(candidate)) { diff --git a/source/source_cell/test/test_cell_geometry.cpp b/source/source_cell/test/test_cell_geometry.cpp index df9703a67ad..b9a39fec6a3 100644 --- a/source/source_cell/test/test_cell_geometry.cpp +++ b/source/source_cell/test/test_cell_geometry.cpp @@ -86,9 +86,12 @@ TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) std::vector> translated = original; for (auto& position : translated) { - position.x = std::fmod(position.x + 1.4, 10.0); - position.y = std::fmod(position.y + 1.4, 10.0); - position.z = std::fmod(position.z + 1.4, 10.0); + const double translated_x = position.x + 1.4; + const double translated_y = position.y + 1.4; + const double translated_z = position.z + 1.4; + position.x = std::fmod(translated_x, 10.0); + position.y = std::fmod(translated_y, 10.0); + position.z = std::fmod(translated_z, 10.0); } ASSERT_TRUE(unitcell::weighted_center(translated, weights, cell, center, error)); unitcell::OrthogonalCell second = cell; diff --git a/source/source_estate/module_pot/pot_pcc.cpp b/source/source_estate/module_pot/pot_pcc.cpp index 14fdc69824e..c5e9cf29c31 100644 --- a/source/source_estate/module_pot/pot_pcc.cpp +++ b/source/source_estate/module_pot/pot_pcc.cpp @@ -32,8 +32,11 @@ bool fractional_translation(const ModuleBase::Matrix3& rotation, const ModuleBase::Vector3& translation) { const double tolerance = 1.0e-6; - const double identity_error = std::abs(rotation.e11 - 1.0) + std::abs(rotation.e22 - 1.0) - + std::abs(rotation.e33 - 1.0) + std::abs(rotation.e12) + const double diagonal_error_x = rotation.e11 - 1.0; + const double diagonal_error_y = rotation.e22 - 1.0; + const double diagonal_error_z = rotation.e33 - 1.0; + const double identity_error = std::abs(diagonal_error_x) + std::abs(diagonal_error_y) + + std::abs(diagonal_error_z) + std::abs(rotation.e12) + std::abs(rotation.e13) + std::abs(rotation.e21) + std::abs(rotation.e23) + std::abs(rotation.e31) + std::abs(rotation.e32); From 0fb799095127b96ae3c17693608aaedbf8d9c655 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 07:35:08 +0000 Subject: [PATCH 26/41] refactor(pcc): address review on Potential layout, references and tests - Document the PCC 0D/2D kernels, their references (Andreussi and Marzari 2014, Dabo et al. 2008, Rutter 2021 for the charged-slab constant) and the ENVIRON implementation they follow in pot_pcc.h; cite Rutter 2021 next to the 2D gauge in pcc_2d.h. - Define the Potential PCC accessors after the constructor/destructor, declare them with the other public getters, and keep the pcc_component() helper with the private member functions. - Drop the stubbed-energy PCC test from elecstate_energy_test.cpp. - Add physical checks: periodic Hartree energy + E_pcc recovers the isolated energy of a charged Gaussian ion and of a polar Gaussian pair (0D), and of a charged layer and a polar neutral slab (2D), independently of the cell size. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0141raGGLbA6qqg3srvRcPnz --- source/source_estate/module_pot/pot_pcc.h | 36 ++++++- .../module_pot/potential_new.cpp | 85 ++++++++-------- .../source_estate/module_pot/potential_new.h | 15 +-- source/source_estate/pcc_2d.h | 3 +- .../test/elecstate_energy_test.cpp | 19 +--- source/source_estate/test/test_pcc_0d.cpp | 97 +++++++++++++++++++ source/source_estate/test/test_pcc_2d.cpp | 88 +++++++++++++++++ 7 files changed, 274 insertions(+), 69 deletions(-) diff --git a/source/source_estate/module_pot/pot_pcc.h b/source/source_estate/module_pot/pot_pcc.h index 378809c2f8d..1774940510b 100644 --- a/source/source_estate/module_pot/pot_pcc.h +++ b/source/source_estate/module_pot/pot_pcc.h @@ -9,7 +9,41 @@ namespace elecstate { -/// Self-consistent vacuum PCC. Owns the result of its last density update. +/** + * @brief Self-consistent vacuum point-counter-charge (PCC) correction. + * + * Owns the result of its last density update. The correction removes the + * spurious interaction of the total charge with its periodic images by + * expanding that charge to second order in its multipoles about the + * mass-weighted ionic center (charge q, dipole d, second moment Q). The + * charge seen here is ionic + electronic only; any solvent polarization charge + * must be corrected by its own component. + * In Hartree atomic units, for a unit positive test charge at r (or at the + * normal coordinate z for slabs): + * + * 0D (cubic cell of edge L, volume V = L^3, Madelung constant alpha of the + * simple cubic lattice): + * v(r) = alpha q / L - 2 pi / (3 V) * (q r^2 - 2 d.r + Q) + * + * 2D (periodic area A, normal period L): + * v(z) = -pi q L / (3 A) - 2 pi / (A L) * (q z^2 - 2 d_z z + Q_zz) + * + * The correction energy is E = 1/2 * integral rho_total(r) v(r) dr and the + * forces follow from the gradient of v at the ionic positions. The kernels are + * implemented in pcc_0d.cpp and pcc_2d.cpp; see pcc_2d.h for the gauge of the + * charged-slab constant. + * + * References: + * - O. Andreussi and N. Marzari, Phys. Rev. B 90, 245101 (2014) + * (parabolic 0D and 2D point-counter-charge corrections). + * - I. Dabo, B. Kozinsky, N. E. Singh-Miller and N. Marzari, + * Phys. Rev. B 77, 115139 (2008) (density-countercharge corrections). + * - M. J. Rutter, Electron. Struct. 3, 015002 (2021) (charged slabs: the + * zero-mean periodic potential of a charged plane fixes the 2D constant). + * + * The implementation follows the PCC corrections of the ENVIRON library + * (www.quantum-environ.org). + */ class PotPcc : public PotBase { public: diff --git a/source/source_estate/module_pot/potential_new.cpp b/source/source_estate/module_pot/potential_new.cpp index 201af74e730..b23b8710bb5 100644 --- a/source/source_estate/module_pot/potential_new.cpp +++ b/source/source_estate/module_pot/potential_new.cpp @@ -15,49 +15,6 @@ namespace elecstate { -const PotPcc* Potential::pcc_component() const -{ - for (const PotBase* component : this->components) - { - const PotPcc* pcc = dynamic_cast(component); - if (pcc != nullptr) - { - return pcc; - } - } - return nullptr; -} - -double Potential::pcc_energy_rydberg() const -{ - const PotPcc* pcc = this->pcc_component(); - if (pcc == nullptr) - { - return 0.0; - } - return pcc->get_energy(); -} - -const std::vector* Potential::pcc_electron_potential() const -{ - const PotPcc* pcc = this->pcc_component(); - if (pcc == nullptr) - { - return nullptr; - } - return &pcc->electron_potential(); -} - -void Potential::add_pcc_force(const UnitCell& cell, ModuleBase::matrix& force) const -{ - const PotPcc* pcc = this->pcc_component(); - if (pcc != nullptr) - { - pcc->add_force(cell, force); - } -} - - Potential::Potential(const ModulePW::PW_Basis* rho_basis_in, const ModulePW::PW_Basis* rho_basis_smooth_in, const UnitCell* ucell_in, @@ -399,4 +356,46 @@ double Potential::get_ml_exx_energy() const #endif } +const PotPcc* Potential::pcc_component() const +{ + for (const PotBase* component : this->components) + { + const PotPcc* pcc = dynamic_cast(component); + if (pcc != nullptr) + { + return pcc; + } + } + return nullptr; +} + +double Potential::pcc_energy_rydberg() const +{ + const PotPcc* pcc = this->pcc_component(); + if (pcc == nullptr) + { + return 0.0; + } + return pcc->get_energy(); +} + +const std::vector* Potential::pcc_electron_potential() const +{ + const PotPcc* pcc = this->pcc_component(); + if (pcc == nullptr) + { + return nullptr; + } + return &pcc->electron_potential(); +} + +void Potential::add_pcc_force(const UnitCell& cell, ModuleBase::matrix& force) const +{ + const PotPcc* pcc = this->pcc_component(); + if (pcc != nullptr) + { + pcc->add_force(cell, force); + } +} + } // namespace elecstate diff --git a/source/source_estate/module_pot/potential_new.h b/source/source_estate/module_pot/potential_new.h index 77cc8e210b5..e1af8269816 100644 --- a/source/source_estate/module_pot/potential_new.h +++ b/source/source_estate/module_pot/potential_new.h @@ -80,11 +80,6 @@ class Potential : public PotBase PotBase* get_pot_type(const std::string& pot_type); - /// PCC results belong to this electronic state's registered potential. - double pcc_energy_rydberg() const; - const std::vector* pcc_electron_potential() const; - void add_pcc_force(const UnitCell& cell, ModuleBase::matrix& force) const; - /** * @brief Inject the shared RT-TDDFT field state before potential setup. * @@ -217,6 +212,12 @@ class Potential : public PotBase /// @return E_ML-EXX double get_ml_exx_energy() const; + /// Results of the registered point-counter-charge (PCC) component, used by + /// the energy, force and output stages; neutral values when PCC is off. + double pcc_energy_rydberg() const; + const std::vector* pcc_electron_potential() const; + void add_pcc_force(const UnitCell& cell, ModuleBase::matrix& force) const; + private: void cal_v_eff(const Charge* const chg, const UnitCell* const ucell, ModuleBase::matrix& v_eff) override; void cal_fixed_v(double* vl_pseudo) override; @@ -225,6 +226,9 @@ class Potential : public PotBase void allocate(); + // the registered PCC component, or nullptr when PCC is off + const PotPcc* pcc_component() const; + std::vector v_eff_fixed; ModuleBase::matrix v_eff; @@ -249,7 +253,6 @@ class Potential : public PotBase double vl_of_0 = 0.0; std::vector components; - const PotPcc* pcc_component() const; const UnitCell* ucell_ = nullptr; const ModuleBase::matrix* vloc_ = nullptr; diff --git a/source/source_estate/pcc_2d.h b/source/source_estate/pcc_2d.h index ed902148613..4b9e88c9be5 100644 --- a/source/source_estate/pcc_2d.h +++ b/source/source_estate/pcc_2d.h @@ -23,7 +23,8 @@ bool make_pcc_2d_parameters(const unitcell::SlabCell& cell, /// Hartree kernels. Parameters must be validated; moments use normal-projected /// positions r = normal * u, so second_moment is the normal second moment. /// The open planar kernel is zero on the source plane; its periodic counterpart -/// has zero mean. This gauge fixes the charged-slab constant to -pi*L/(3*A). +/// has zero mean. This gauge fixes the charged-slab constant to -pi*L/(3*A); +/// see M. J. Rutter, Electron. Struct. 3, 015002 (2021). double pcc_2d_potential(const ChargeMoments& moments, double coordinate, const ModuleBase::Vector3& normal, diff --git a/source/source_estate/test/elecstate_energy_test.cpp b/source/source_estate/test/elecstate_energy_test.cpp index 86f1334d7c3..0bb1fed1b15 100644 --- a/source/source_estate/test/elecstate_energy_test.cpp +++ b/source/source_estate/test/elecstate_energy_test.cpp @@ -14,7 +14,7 @@ namespace elecstate { double Potential::pcc_energy_rydberg() const { - return 0.25; + return 0.0; } void Potential::get_vnew(Charge const*, ModuleBase::matrix&) { @@ -150,23 +150,6 @@ TEST_F(ElecStateEnergyTest, CalEnergiesEtotDFTU) EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 1.3); } -TEST_F(ElecStateEnergyTest, AddsPccEnergyOnce) -{ - elecstate->f_en.deband = 0.1; - assume_isolated = "pcc_0d"; - elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); - EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.25); - EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.95); - assume_isolated = "pcc_2d"; - elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); - EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.25); - EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.95); - assume_isolated = "none"; - elecstate->cal_energies(2, imp_sol, sc_mag_switch, dft_plus_u, assume_isolated); - EXPECT_DOUBLE_EQ(elecstate->f_en.correction_el, 0.0); - EXPECT_DOUBLE_EQ(elecstate->f_en.etot, 0.7); -} - TEST_F(ElecStateEnergyTest, CalConverged) { elecstate->cal_converged(); diff --git a/source/source_estate/test/test_pcc_0d.cpp b/source/source_estate/test/test_pcc_0d.cpp index a9b4f7c5268..9135e532579 100644 --- a/source/source_estate/test/test_pcc_0d.cpp +++ b/source/source_estate/test/test_pcc_0d.cpp @@ -5,8 +5,50 @@ #include +#include + namespace { +/// Hartree energy per cell of periodic Gaussian charges (width sigma) under the +/// usual G = 0 exclusion. One charge at the origin, or a +q/-q pair separated +/// by `separation` along z when `dipole_pair` is true. +double periodic_gaussian_energy(const double length, + const double sigma, + const double charge, + const double separation, + const bool dipole_pair) +{ + const double step = 2.0 * ModuleBase::PI / length; + const int max_index = static_cast(8.0 / (sigma * step)) + 1; + const double volume = length * length * length; + double sum = 0.0; + for (int i = -max_index; i <= max_index; ++i) + { + for (int j = -max_index; j <= max_index; ++j) + { + for (int k = -max_index; k <= max_index; ++k) + { + if (i == 0 && j == 0 && k == 0) + { + continue; + } + const double gx = step * i; + const double gy = step * j; + const double gz = step * k; + const double g2 = gx * gx + gy * gy + gz * gz; + double structure = 1.0; + if (dipole_pair) + { + const double phase = std::sin(0.5 * gz * separation); + structure = 4.0 * phase * phase; + } + sum += charge * charge * structure * std::exp(-g2 * sigma * sigma) / g2; + } + } + } + return 2.0 * ModuleBase::PI * sum / volume; +} + class Pcc0dTest : public testing::Test { protected: @@ -156,3 +198,58 @@ TEST_F(Pcc0dTest, EnergyIsIndependentOfMultipoleOrigin) const double shifted_energy = elecstate::pcc_0d_energy(moments, parameters); EXPECT_NEAR(original_energy, shifted_energy, 1.0e-14); } + +// Physical check of the model: a charged Gaussian ion (H3O+-like) in cubic +// cells of growing size. The periodic Hartree energy drifts as 1/L, while +// periodic + PCC reproduces the isolated self-energy q^2 / (2 sigma sqrt(pi)). +TEST_F(Pcc0dTest, RecoversIsolatedEnergyOfChargedGaussianForAnyCellSize) +{ + const double sigma = 1.0; + const double charge = 1.0; + const double isolated = charge * charge / (2.0 * sigma * std::sqrt(ModuleBase::PI)); + const double lengths[3] = {16.0, 20.0, 24.0}; + for (int index = 0; index < 3; ++index) + { + const double length = lengths[index]; + parameters.length = length; + elecstate::ChargeMoments gaussian; + gaussian.charge = charge; + gaussian.second_moment = 3.0 * sigma * sigma * charge; + const double periodic = periodic_gaussian_energy(length, sigma, charge, 0.0, false); + const double correction = elecstate::pcc_0d_energy(gaussian, parameters); + const double periodic_error = std::abs(periodic - isolated); + EXPECT_GT(periodic_error, 1.0e-2); + EXPECT_NEAR(periodic + correction, isolated, 1.0e-10); + } +} + +// Neutral polar molecule modelled as a +q/-q Gaussian pair. PCC removes the +// leading 1/L^3 dipole interaction; the residual error comes from higher +// multipoles and must be far smaller and decay faster than the uncorrected one. +TEST_F(Pcc0dTest, RemovesDipoleImageInteractionOfNeutralGaussianPair) +{ + const double sigma = 1.0; + const double charge = 1.0; + const double separation = 2.0; + const double self_energy = charge * charge / (2.0 * sigma * std::sqrt(ModuleBase::PI)); + const double attraction = charge * charge * std::erf(0.5 * separation / sigma) / separation; + const double isolated = 2.0 * self_energy - attraction; + elecstate::ChargeMoments pair; + pair.dipole.z = charge * separation; + const double lengths[2] = {16.0, 24.0}; + double corrected_errors[2] = {0.0, 0.0}; + for (int index = 0; index < 2; ++index) + { + const double length = lengths[index]; + parameters.length = length; + const double periodic = periodic_gaussian_energy(length, sigma, charge, separation, true); + const double correction = elecstate::pcc_0d_energy(pair, parameters); + const double periodic_error = std::abs(periodic - isolated); + const double corrected_error = std::abs(periodic + correction - isolated); + EXPECT_LT(corrected_error, 0.05 * periodic_error); + corrected_errors[index] = corrected_error; + } + const double ratio = lengths[1] / lengths[0]; + const double dipole_decay = std::pow(ratio, 3.0); + EXPECT_GT(corrected_errors[0] / corrected_errors[1], dipole_decay); +} diff --git a/source/source_estate/test/test_pcc_2d.cpp b/source/source_estate/test/test_pcc_2d.cpp index 39b18590da1..5487a4842c4 100644 --- a/source/source_estate/test/test_pcc_2d.cpp +++ b/source/source_estate/test/test_pcc_2d.cpp @@ -4,6 +4,39 @@ #include +#include + +namespace +{ +/// Hartree energy per cell of periodic Gaussian sheets (surface charge +/// `density`, normal width sigma) with the G = 0 term excluded. One sheet at +/// the origin, or a +density/-density pair separated by `separation` along the +/// normal when `dipole_pair` is true. Only G parallel to the normal contributes. +double periodic_sheet_energy(const double area, + const double length, + const double sigma, + const double density, + const double separation, + const bool dipole_pair) +{ + const double step = 2.0 * ModuleBase::PI / length; + const int max_index = static_cast(8.0 / (sigma * step)) + 1; + double sum = 0.0; + for (int k = 1; k <= max_index; ++k) + { + const double g = step * k; + double structure = 1.0; + if (dipole_pair) + { + const double phase = std::sin(0.5 * g * separation); + structure = 4.0 * phase * phase; + } + sum += density * density * structure * std::exp(-g * g * sigma * sigma) / (g * g); + } + return 4.0 * ModuleBase::PI * area * sum / length; +} +} // namespace + TEST(Pcc2d, MatchesChargedSheetGaugeAndNeutralDipoleEnergy) { unitcell::SlabCell cell; @@ -82,3 +115,58 @@ TEST(Pcc2d, ElectronicPotentialMatchesEnergyDerivative) const double derivative = (plus_energy - minus_energy) / (2.0 * step); EXPECT_NEAR(derivative, expected, 1.0e-10); } + +// Physical check of the charged-slab gauge: a charged Gaussian layer in cells +// of growing vacuum. periodic + PCC must give the open-boundary energy with the +// potential referenced to zero on the plane of the charge, -2 sqrt(pi) A s^2 sigma, +// independently of the vacuum size. +TEST(Pcc2d, RecoversOpenBoundaryEnergyOfChargedLayerForAnyVacuum) +{ + const double area = 50.0; + const double sigma = 1.0; + const double density = 0.1; + const double charge = area * density; + const double isolated = -2.0 * std::sqrt(ModuleBase::PI) * area * density * density * sigma; + const double lengths[3] = {20.0, 30.0, 40.0}; + for (int index = 0; index < 3; ++index) + { + elecstate::Pcc2dParameters parameters; + parameters.area = area; + parameters.length = lengths[index]; + elecstate::ChargeMoments layer; + layer.charge = charge; + layer.second_moment = charge * sigma * sigma; + const double periodic = periodic_sheet_energy(area, lengths[index], sigma, density, 0.0, false); + const double correction = elecstate::pcc_2d_energy(layer, parameters); + EXPECT_NEAR(periodic + correction, isolated, 1.0e-10); + } +} + +// Polar neutral slab modelled as two opposite Gaussian layers. Periodic images +// impose a spurious zero-average field that changes the energy by ~1/L; PCC +// restores the isolated dipole-layer energy for every vacuum size. +TEST(Pcc2d, RecoversIsolatedEnergyOfPolarNeutralSlabForAnyVacuum) +{ + const double area = 50.0; + const double sigma = 1.0; + const double density = 0.1; + const double separation = 3.0; + const double same_layer = 2.0 * sigma / std::sqrt(ModuleBase::PI); + const double gap = 0.5 * separation / sigma; + const double cross_layer = separation * std::erf(gap) + same_layer * std::exp(-gap * gap); + const double isolated = -2.0 * ModuleBase::PI * area * density * density * (same_layer - cross_layer); + const double lengths[3] = {20.0, 30.0, 40.0}; + for (int index = 0; index < 3; ++index) + { + elecstate::Pcc2dParameters parameters; + parameters.area = area; + parameters.length = lengths[index]; + elecstate::ChargeMoments slab; + slab.dipole.z = area * density * separation; + const double periodic = periodic_sheet_energy(area, lengths[index], sigma, density, separation, true); + const double correction = elecstate::pcc_2d_energy(slab, parameters); + const double periodic_error = std::abs(periodic - isolated); + EXPECT_GT(periodic_error, 0.1); + EXPECT_NEAR(periodic + correction, isolated, 1.0e-10); + } +} From 77090b6c96ead7eeed28c1b3b057f69825568ab2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 16:44:22 +0000 Subject: [PATCH 27/41] refactor(sccs): use ABACUS error handling in the basic SCCS layer INPUT values are validated only by the INPUT reader. Internal invariants (cavity evaluation, CG breakdown or non-convergence, non-finite energies, Gaussian ionic normalization, non-neutral periodic cells) stop the run with ModuleBase::WARNING_QUIT instead of returning bool plus an error string. The CG non-convergence message reports sccs_maxiter and the final RMS/MAX residuals. Also add the cheap periodic water case 220_PW_SCCS_PERIODIC_WATER (ecutwfc 20, ecutrho 240), which reproduces the original implementation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BKqk2SNWXoZn2PRRGRzGca --- source/source_estate/module_pot/pot_sccs.cpp | 102 ++++---------- source/source_estate/module_pot/pot_sccs.h | 6 +- .../source_estate/module_pot/potential_new.h | 1 + .../module_pot/potential_types.cpp | 15 +- source/source_estate/test/test_pot_sccs.cpp | 13 +- .../source_hamilt/module_sccs/sccs_cavity.cpp | 46 +----- .../source_hamilt/module_sccs/sccs_cavity.h | 12 +- .../module_sccs/sccs_functional.cpp | 40 +----- .../module_sccs/sccs_functional.h | 8 +- .../module_sccs/sccs_ionic_charge.cpp | 42 +----- .../module_sccs/sccs_ionic_charge.h | 8 +- .../module_sccs/sccs_ionic_force.cpp | 46 +----- .../module_sccs/sccs_ionic_force.h | 8 +- .../module_sccs/sccs_parameters.cpp | 112 +++++---------- .../module_sccs/sccs_parameters.h | 9 +- .../module_sccs/sccs_pw_coulomb.cpp | 68 +-------- .../module_sccs/sccs_pw_coulomb.h | 16 +-- .../module_sccs/sccs_response.cpp | 132 ++++-------------- .../source_hamilt/module_sccs/sccs_response.h | 9 +- .../module_sccs/test/CMakeLists.txt | 1 + .../module_sccs/test/test_sccs_cavity.cpp | 47 ++----- .../module_sccs/test/test_sccs_functional.cpp | 15 +- .../test/test_sccs_ionic_charge.cpp | 27 +--- .../test/test_sccs_ionic_force.cpp | 50 ++----- .../module_sccs/test/test_sccs_parameters.cpp | 71 +++------- .../module_sccs/test/test_sccs_pw_coulomb.cpp | 46 ++---- .../module_sccs/test/test_sccs_response.cpp | 81 ++++------- tests/01_PW/220_PW_SCCS_PERIODIC_WATER/INPUT | 21 +++ tests/01_PW/220_PW_SCCS_PERIODIC_WATER/KPT | 4 + tests/01_PW/220_PW_SCCS_PERIODIC_WATER/README | 2 + tests/01_PW/220_PW_SCCS_PERIODIC_WATER/STRU | 24 ++++ .../220_PW_SCCS_PERIODIC_WATER/result.ref | 6 + .../220_PW_SCCS_PERIODIC_WATER/threshold | 3 + tests/01_PW/CASES_CPU.txt | 1 + 34 files changed, 302 insertions(+), 790 deletions(-) create mode 100644 tests/01_PW/220_PW_SCCS_PERIODIC_WATER/INPUT create mode 100644 tests/01_PW/220_PW_SCCS_PERIODIC_WATER/KPT create mode 100644 tests/01_PW/220_PW_SCCS_PERIODIC_WATER/README create mode 100644 tests/01_PW/220_PW_SCCS_PERIODIC_WATER/STRU create mode 100644 tests/01_PW/220_PW_SCCS_PERIODIC_WATER/result.ref create mode 100644 tests/01_PW/220_PW_SCCS_PERIODIC_WATER/threshold diff --git a/source/source_estate/module_pot/pot_sccs.cpp b/source/source_estate/module_pot/pot_sccs.cpp index 23f8d34b324..7eeff309d31 100644 --- a/source/source_estate/module_pot/pot_sccs.cpp +++ b/source/source_estate/module_pot/pot_sccs.cpp @@ -13,52 +13,30 @@ #include #include -namespace -{ -void require_valid_on_pool(bool valid, const std::string& error) -{ - double invalid = valid ? 0.0 : 1.0; - Parallel_Reduce::reduce_pool(invalid); - if (invalid != 0.0) - { - const std::string message = error.empty() ? "Invalid SCCS input on another pool rank" : error; - ModuleBase::WARNING_QUIT("PotSccs", message); - } -} -} - namespace elecstate { -bool make_sccs_config_from_input(const Input_para& input, +void make_sccs_config_from_input(const Input_para& input, ModuleSccs::SccsConfig& config, - ModuleSccs::PolarizationSolverParameters& solver, - std::string& error) + ModuleSccs::PolarizationSolverParameters& solver) { - ModuleSccs::Preset preset; - if (!ModuleSccs::parse_preset(input.sccs_preset, preset, error)) { return false; } - ModuleSccs::SccsConfig candidate; + const ModuleSccs::Preset preset = ModuleSccs::parse_preset(input.sccs_preset); if (preset == ModuleSccs::Preset::Custom) { - candidate.cavity.density_min = input.sccs_rho_min; - candidate.cavity.density_max = input.sccs_rho_max; - candidate.cavity.epsilon_bulk = input.sccs_epsilon; - candidate.surface_tension = ModuleSccs::dyn_per_cm_to_hartree_per_bohr2(input.sccs_gamma); - candidate.pressure = ModuleSccs::gpa_to_hartree_per_bohr3(input.sccs_pressure); + config = ModuleSccs::SccsConfig(); + config.cavity.density_min = input.sccs_rho_min; + config.cavity.density_max = input.sccs_rho_max; + config.cavity.epsilon_bulk = input.sccs_epsilon; + config.surface_tension = ModuleSccs::dyn_per_cm_to_hartree_per_bohr2(input.sccs_gamma); + config.pressure = ModuleSccs::gpa_to_hartree_per_bohr3(input.sccs_pressure); } - else if (!ModuleSccs::make_sccs_config(preset, candidate, error)) { return false; } - candidate.surface_regularization = input.sccs_surface_eta; - if (!ModuleSccs::validate_config(candidate, error)) { return false; } - if (input.sccs_maxiter <= 0 || !std::isfinite(input.sccs_tol_rms) || input.sccs_tol_rms <= 0.0 - || !std::isfinite(input.sccs_tol_max) || input.sccs_tol_max <= 0.0) + else { - error = "SCCS requires a positive iteration limit and finite positive residual tolerances"; - return false; + config = ModuleSccs::make_sccs_config(preset); } - config = candidate; + config.surface_regularization = input.sccs_surface_eta; solver.max_iterations = input.sccs_maxiter; solver.tolerance_rms = input.sccs_tol_rms; solver.tolerance_max = input.sccs_tol_max; - return true; } PotSccs::PotSccs(const ModulePW::PW_Basis* basis, @@ -73,29 +51,10 @@ PotSccs::PotSccs(const ModulePW::PW_Basis* basis, void PotSccs::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) { ModuleBase::timer::start("PotSccs", "cal_v_eff"); - const bool storage_valid = charge != nullptr && cell != nullptr && this->rho_basis_ != nullptr; - require_valid_on_pool(storage_valid, "SCCS requires charge, cell and PW basis storage"); const ModulePW::PW_Basis& basis = *this->rho_basis_; - const bool grid_valid = (charge->nspin == 1 || charge->nspin == 2) - && potential.nr == charge->nspin && potential.nc == basis.nrxx - && charge->rho != nullptr && cell->atoms != nullptr && cell->ntype > 0 - && std::isfinite(cell->lat0) && cell->lat0 > 0.0; - require_valid_on_pool(grid_valid, "SCCS requires initialized atom/density storage and an nspin=1/2 potential"); - bool density_valid = true; - for (int spin = 0; spin < charge->nspin; ++spin) - { - if (basis.nrxx > 0 && charge->rho[spin] == nullptr) { density_valid = false; } - } - require_valid_on_pool(density_valid, "SCCS charge density is not available"); const std::vector atoms = unitcell::get_atom_data(cell->atoms, cell->ntype, cell->lat0); - const int atom_count = atoms.size(); - const bool count_valid = atom_count == cell->nat; - require_valid_on_pool(count_valid, "SCCS atom count does not match UnitCell"); std::vector ions; - std::string error; - const bool ions_valid = ModuleSccs::gaussian_ionic_density(atoms, basis, cell->tpiba, - ModuleSccs::gaussian_ion_spread, ions, error); - require_valid_on_pool(ions_valid, error); + ModuleSccs::gaussian_ionic_density(atoms, basis, cell->tpiba, ModuleSccs::gaussian_ion_spread, ions); std::vector density(basis.nrxx, 0.0); std::vector solute_charge(basis.nrxx); double ionic_sum = 0.0; @@ -114,20 +73,22 @@ void PotSccs::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase:: net_charge *= dv; double expected_ionic_charge = 0.0; for (const unitcell::AtomData& atom : atoms) { expected_ionic_charge += atom.valence_charge; } + // Pool-reduced sums: every rank takes the same decision. const double normalization_error = ionic_sum - expected_ionic_charge; - const bool normalization_valid = std::isfinite(normalization_error) && std::abs(normalization_error) < 1e-6; - require_valid_on_pool(normalization_valid, "SCCS Gaussian ionic charge normalization failed"); + if (!std::isfinite(normalization_error) || std::abs(normalization_error) >= 1e-6) + { + ModuleBase::WARNING_QUIT("PotSccs::cal_v_eff", "SCCS Gaussian ionic charge normalization failed"); + } const bool neutral = std::isfinite(net_charge) && std::abs(net_charge) < 1e-6; - require_valid_on_pool(neutral, "Periodic SCCS currently requires a neutral cell"); + if (!neutral) + { + ModuleBase::WARNING_QUIT("PotSccs::cal_v_eff", "Periodic SCCS currently requires a neutral cell"); + } ModuleSccs::SccsResponse response; - const bool response_valid = ModuleSccs::solve_sccs_response(density, solute_charge, config_.cavity, - solver_, restart_potential_, basis, cell->tpiba, - response, error); - require_valid_on_pool(response_valid, error); + ModuleSccs::solve_sccs_response(density, solute_charge, config_.cavity, solver_, restart_potential_, basis, + cell->tpiba, response); ModuleSccs::FunctionalResult functional; - const bool functional_valid = ModuleSccs::evaluate_functional(solute_charge, response, config_, basis, - cell->tpiba, functional, error); - require_valid_on_pool(functional_valid, error); + ModuleSccs::evaluate_functional(solute_charge, response, config_, basis, cell->tpiba, functional); electrostatic_rydberg_ = 2.0 * functional.reaction_energy; non_electrostatic_rydberg_ = 2.0 * (functional.surface_energy + functional.volume_energy); electrostatic_potential_.resize(basis.nrxx); @@ -144,25 +105,16 @@ void PotSccs::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase:: void PotSccs::add_solvation_force(const UnitCell& cell, ModuleBase::matrix& force) const { ModuleBase::timer::start("PotSccs", "add_solvation_force"); - const bool shape_valid = force.nr == cell.nat && force.nc == 3 - && this->rho_basis_ != nullptr && cell.atoms != nullptr && cell.ntype > 0; - require_valid_on_pool(shape_valid, "SCCS force requires initialized cell and atom-major storage"); const ModulePW::PW_Basis& basis = *this->rho_basis_; - const bool result_valid = electrostatic_potential_.size() == static_cast(basis.nrxx); - require_valid_on_pool(result_valid, "SCCS force requires a completed response on the current grid"); const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); - const bool count_valid = atoms.size() == static_cast(cell.nat); - require_valid_on_pool(count_valid, "SCCS force atom count does not match UnitCell"); std::vector reaction(basis.nrxx); for (int ir = 0; ir < basis.nrxx; ++ir) { reaction[ir] = -0.5 * electrostatic_potential_[ir]; } std::vector> ionic_force; - std::string error; - const bool valid = ModuleSccs::gaussian_ionic_force(atoms, reaction, basis, cell.tpiba, - ModuleSccs::gaussian_ion_spread, ionic_force, error); - require_valid_on_pool(valid, error); + ModuleSccs::gaussian_ionic_force(atoms, reaction, basis, cell.tpiba, ModuleSccs::gaussian_ion_spread, + ionic_force); for (int ia = 0; ia < cell.nat; ++ia) { for (int axis = 0; axis < 3; ++axis) diff --git a/source/source_estate/module_pot/pot_sccs.h b/source/source_estate/module_pot/pot_sccs.h index 92bf8cab8ca..c1aa7a770a2 100644 --- a/source/source_estate/module_pot/pot_sccs.h +++ b/source/source_estate/module_pot/pot_sccs.h @@ -7,10 +7,10 @@ struct Input_para; namespace elecstate { -bool make_sccs_config_from_input(const Input_para& input, +// Map INPUT values, already validated by the INPUT reader, onto the solver. +void make_sccs_config_from_input(const Input_para& input, ModuleSccs::SccsConfig& config, - ModuleSccs::PolarizationSolverParameters& solver, - std::string& error); + ModuleSccs::PolarizationSolverParameters& solver); class PotSccs : public PotBase { diff --git a/source/source_estate/module_pot/potential_new.h b/source/source_estate/module_pot/potential_new.h index d017035b6e5..1e06d86b495 100644 --- a/source/source_estate/module_pot/potential_new.h +++ b/source/source_estate/module_pot/potential_new.h @@ -257,6 +257,7 @@ class Potential : public PotBase std::vector components; const PotPcc* pcc_component() const; + PotBase* make_sccs_potential(const Input_para& input); const UnitCell* ucell_ = nullptr; const ModuleBase::matrix* vloc_ = nullptr; diff --git a/source/source_estate/module_pot/potential_types.cpp b/source/source_estate/module_pot/potential_types.cpp index 7feab0cc7c5..f7841ac64da 100644 --- a/source/source_estate/module_pot/potential_types.cpp +++ b/source/source_estate/module_pot/potential_types.cpp @@ -22,6 +22,14 @@ namespace elecstate { +PotBase* Potential::make_sccs_potential(const Input_para& input) +{ + ModuleSccs::SccsConfig config; + ModuleSccs::PolarizationSolverParameters solver; + make_sccs_config_from_input(input, config, solver); + return new PotSccs(this->rho_basis_, config, solver); +} + PotBase* Potential::get_pot_type(const std::string& pot_type, const Input_para& input) { ModuleBase::TITLE("Potential", "get_pot_type"); @@ -48,12 +56,7 @@ PotBase* Potential::get_pot_type(const std::string& pot_type, const Input_para& } else if (pot_type == "sccs") { - ModuleSccs::SccsConfig config; - ModuleSccs::PolarizationSolverParameters solver; - std::string error; - const bool valid = make_sccs_config_from_input(input, config, solver, error); - if (!valid) { ModuleBase::WARNING_QUIT("Potential::get_pot_type", error); } - return new PotSccs(this->rho_basis_, config, solver); + return this->make_sccs_potential(input); } else if (pot_type == "surchem") { diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp index 1b42d15f21e..e47be13ec70 100644 --- a/source/source_estate/test/test_pot_sccs.cpp +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -38,7 +38,7 @@ TEST_F(PotSccsTest, IndependentInstancesAndSpinChannels) Input_para input; ModuleSccs::SccsConfig config; ModuleSccs::PolarizationSolverParameters solver; - ASSERT_TRUE(elecstate::make_sccs_config_from_input(input, config, solver, error)); + elecstate::make_sccs_config_from_input(input, config, solver); config.cavity.epsilon_bulk = 5.0; elecstate::PotSccs first(&basis, config, solver); config.cavity.epsilon_bulk = 1.0; @@ -70,24 +70,21 @@ TEST_F(PotSccsTest, IndependentInstancesAndSpinChannels) } } -TEST_F(PotSccsTest, PresetConfigurationIsExplicit) +TEST_F(PotSccsTest, InputMapsOntoSccsConfig) { Input_para input; input.sccs_preset = "water-neutral"; - input.sccs_epsilon = 2.0; + input.sccs_epsilon = 2.0; // replaced by the preset input.sccs_maxiter = 42; input.sccs_surface_eta = 1e-6; ModuleSccs::SccsConfig config; ModuleSccs::PolarizationSolverParameters solver; - ASSERT_TRUE(elecstate::make_sccs_config_from_input(input, config, solver, error)); + elecstate::make_sccs_config_from_input(input, config, solver); EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 78.3); EXPECT_DOUBLE_EQ(config.cavity.density_min, 1e-4); EXPECT_LT(config.pressure, 0.0); EXPECT_DOUBLE_EQ(config.surface_regularization, 1e-6); EXPECT_EQ(solver.max_iterations, 42); - input.sccs_maxiter = 0; - EXPECT_FALSE(elecstate::make_sccs_config_from_input(input, config, solver, error)); - EXPECT_EQ(solver.max_iterations, 42); } TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) @@ -116,7 +113,7 @@ TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) input.sccs_tol_max = 1e-12; ModuleSccs::SccsConfig config; ModuleSccs::PolarizationSolverParameters solver; - ASSERT_TRUE(elecstate::make_sccs_config_from_input(input, config, solver, error)); + elecstate::make_sccs_config_from_input(input, config, solver); elecstate::PotSccs component(&basis, config, solver); ModuleBase::matrix potential(1, basis.nrxx); component.cal_v_eff(&charge, &cell, potential); diff --git a/source/source_hamilt/module_sccs/sccs_cavity.cpp b/source/source_hamilt/module_sccs/sccs_cavity.cpp index ac9c1287d15..94f69ed0abc 100644 --- a/source/source_hamilt/module_sccs/sccs_cavity.cpp +++ b/source/source_hamilt/module_sccs/sccs_cavity.cpp @@ -5,38 +5,8 @@ namespace ModuleSccs { -bool validate_cavity_parameters(const CavityParameters& parameters, std::string& error) +CavityPoint evaluate_cavity(double density, const CavityParameters& parameters) { - error.clear(); - if (!std::isfinite(parameters.density_min) || !std::isfinite(parameters.density_max) - || parameters.density_min <= 0.0 || parameters.density_max <= parameters.density_min) - { - error = "SCCS requires finite density thresholds with 0 < density_min < density_max"; - return false; - } - if (!std::isfinite(parameters.epsilon_bulk) || parameters.epsilon_bulk < 1.0) - { - error = "SCCS requires a finite bulk dielectric constant >= 1"; - return false; - } - return true; -} - -bool evaluate_cavity(double density, - const CavityParameters& parameters, - CavityPoint& result, - std::string& error) -{ - if (!validate_cavity_parameters(parameters, error)) - { - return false; - } - if (!std::isfinite(density)) - { - error = "SCCS cavity requires a finite electronic density"; - return false; - } - CavityPoint point; if (density <= parameters.density_min) { @@ -50,11 +20,6 @@ bool evaluate_cavity(double density, else { const double density_ratio = parameters.density_max / parameters.density_min; - if (!std::isfinite(density_ratio)) - { - error = "SCCS cavity density ratio exceeds the numerical range"; - return false; - } const double log_width = std::log(density_ratio); const double relative_density = parameters.density_max / density; const double log_relative_density = std::log(relative_density); @@ -68,14 +33,7 @@ bool evaluate_cavity(double density, point.dsolute_drho = -dsolvent_drho; point.epsilon = std::exp(log_epsilon_point); point.depsilon_drho = point.epsilon * log_epsilon * dsolvent_drho; - if (!std::isfinite(point.solute) || !std::isfinite(point.dsolute_drho) - || !std::isfinite(point.epsilon) || !std::isfinite(point.depsilon_drho)) - { - error = "SCCS cavity transition exceeds the numerical range"; - return false; - } } - result = point; - return true; + return point; } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_cavity.h b/source/source_hamilt/module_sccs/sccs_cavity.h index bca86d19993..af3e4eeb9d9 100644 --- a/source/source_hamilt/module_sccs/sccs_cavity.h +++ b/source/source_hamilt/module_sccs/sccs_cavity.h @@ -1,8 +1,6 @@ #ifndef SCCS_CAVITY_H #define SCCS_CAVITY_H -#include - namespace ModuleSccs { // Densities are in electrons/Bohr^3. The cavity uses the electronic density. @@ -21,13 +19,9 @@ struct CavityPoint double depsilon_drho = 0.0; }; -bool validate_cavity_parameters(const CavityParameters& parameters, std::string& error); - -// Failure leaves result unchanged. Negative Fourier ringing uses the bulk limit. -bool evaluate_cavity(double density, - const CavityParameters& parameters, - CavityPoint& result, - std::string& error); +// Parameters are validated by the INPUT reader. Negative Fourier ringing +// uses the bulk limit. +CavityPoint evaluate_cavity(double density, const CavityParameters& parameters); } // namespace ModuleSccs #endif diff --git a/source/source_hamilt/module_sccs/sccs_functional.cpp b/source/source_hamilt/module_sccs/sccs_functional.cpp index 2b2b4d52cc4..294995e89c5 100644 --- a/source/source_hamilt/module_sccs/sccs_functional.cpp +++ b/source/source_hamilt/module_sccs/sccs_functional.cpp @@ -4,6 +4,7 @@ #include "sccs_pw_coulomb.h" #include "source_base/parallel_reduce.h" +#include "source_base/tool_quit.h" #include "source_basis/module_pw/pw_basis.h" #include "source_hamilt/module_xc/xc_functional.h" @@ -13,41 +14,18 @@ namespace ModuleSccs { -bool evaluate_functional(const std::vector& charge, +void evaluate_functional(const std::vector& charge, const SccsResponse& response, const SccsConfig& config, const ModulePW::PW_Basis& basis, double tpiba, - FunctionalResult& result, - std::string& error) + FunctionalResult& result) { - if (!validate_pw_grid(basis, tpiba, error) || !validate_grid_values(charge, basis, error) - || !validate_grid_values(response.polarization.potential, basis, error) - || !validate_grid_values(response.cavity_potential, basis, error) - || !validate_grid_values(response.solute, basis, error) - || !validate_grid_values(response.dsolute_drho, basis, error)) - { - return false; - } - double invalid = 0.0; - if (!validate_config(config, error)) - { - invalid = 1.0; - } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); - if (invalid != 0.0) - { - error = "SCCS functional requires valid parameters on every pool rank"; - return false; - } const std::size_t size = charge.size(); const double dv = basis.omega / basis.nxyz; PeriodicCoulombOperator coulomb(basis, tpiba); std::vector vacuum; - if (!coulomb.apply_potential(charge, vacuum, error)) - { - return false; - } + coulomb.apply_potential(charge, vacuum); FunctionalResult candidate; candidate.reaction_potential.resize(size); candidate.electron_potential.resize(size); @@ -86,18 +64,12 @@ bool evaluate_functional(const std::vector& charge, * response.dsolute_drho[i]; candidate.electron_potential[i] += nonel; } - if (!validate_grid_values(candidate.reaction_potential, basis, error) - || !validate_grid_values(candidate.electron_potential, basis, error)) - { - return false; - } + // The energies are pool-reduced, so every rank takes the same decision. if (!std::isfinite(candidate.reaction_energy) || !std::isfinite(candidate.surface_energy) || !std::isfinite(candidate.volume_energy)) { - error = "SCCS functional energy is not finite"; - return false; + ModuleBase::WARNING_QUIT("ModuleSccs::evaluate_functional", "SCCS functional energy is not finite"); } result = std::move(candidate); - return true; } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_functional.h b/source/source_hamilt/module_sccs/sccs_functional.h index 35ca17cdde7..ed880ef939d 100644 --- a/source/source_hamilt/module_sccs/sccs_functional.h +++ b/source/source_hamilt/module_sccs/sccs_functional.h @@ -1,7 +1,6 @@ #ifndef SCCS_FUNCTIONAL_H #define SCCS_FUNCTIONAL_H -#include #include namespace ModulePW @@ -25,14 +24,13 @@ struct FunctionalResult std::vector electron_potential; // derivative of all three energy terms }; -// Collective over the PW pool; failure leaves result unchanged. -bool evaluate_functional(const std::vector& charge, +// Collective over the PW pool; a non-finite energy stops the run. +void evaluate_functional(const std::vector& charge, const SccsResponse& response, const SccsConfig& config, const ModulePW::PW_Basis& basis, double tpiba, - FunctionalResult& result, - std::string& error); + FunctionalResult& result); } // namespace ModuleSccs #endif diff --git a/source/source_hamilt/module_sccs/sccs_ionic_charge.cpp b/source/source_hamilt/module_sccs/sccs_ionic_charge.cpp index 3d0f01d8cac..a1015081797 100644 --- a/source/source_hamilt/module_sccs/sccs_ionic_charge.cpp +++ b/source/source_hamilt/module_sccs/sccs_ionic_charge.cpp @@ -1,8 +1,6 @@ #include "sccs_ionic_charge.h" -#include "sccs_pw_coulomb.h" #include "source_base/constants.h" -#include "source_base/parallel_reduce.h" #include "source_basis/module_pw/pw_basis.h" #include "source_cell/cell_tools.h" @@ -11,38 +9,12 @@ namespace ModuleSccs { -bool gaussian_ionic_density(const std::vector& atoms, +void gaussian_ionic_density(const std::vector& atoms, const ModulePW::PW_Basis& basis, double tpiba, double spread, - std::vector& density, - std::string& error) + std::vector& density) { - if (!validate_pw_grid(basis, tpiba, error)) - { - return false; - } - double invalid = 0.0; - if (!std::isfinite(spread) || spread <= 0.0) - { - invalid = 1.0; - } - for (const unitcell::AtomData& atom : atoms) - { - if (!std::isfinite(atom.valence_charge) || atom.valence_charge < 0.0 - || !std::isfinite(atom.position.x) || !std::isfinite(atom.position.y) - || !std::isfinite(atom.position.z)) - { - invalid = 1.0; - } - } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); - if (invalid != 0.0) - { - error = "SCCS Gaussian ions require finite positions, nonnegative valence charges and positive finite spread"; - return false; - } - std::vector> charge(basis.npw, 0.0); const double tpiba2 = tpiba * tpiba; for (const unitcell::AtomData& atom : atoms) @@ -55,13 +27,7 @@ bool gaussian_ionic_density(const std::vector& atoms, charge[ig] += atom.valence_charge / basis.omega * std::exp(exponent) * std::exp(phase_factor); } } - std::vector candidate(basis.nrxx); - basis.recip2real(charge.data(), candidate.data()); - if (!validate_grid_values(candidate, basis, error)) - { - return false; - } - density.swap(candidate); - return true; + density.resize(basis.nrxx); + basis.recip2real(charge.data(), density.data()); } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_ionic_charge.h b/source/source_hamilt/module_sccs/sccs_ionic_charge.h index 3bba3ff565c..38cec3a8c9b 100644 --- a/source/source_hamilt/module_sccs/sccs_ionic_charge.h +++ b/source/source_hamilt/module_sccs/sccs_ionic_charge.h @@ -1,7 +1,6 @@ #ifndef SCCS_IONIC_CHARGE_H #define SCCS_IONIC_CHARGE_H -#include #include namespace ModulePW @@ -19,13 +18,12 @@ namespace ModuleSccs const double gaussian_ion_spread = 0.5; // AtomData is extracted by source_cell; positions are Cartesian Bohr. -// Collective over the PW pool. Failure leaves density unchanged. -bool gaussian_ionic_density(const std::vector& atoms, +// Collective over the PW pool. +void gaussian_ionic_density(const std::vector& atoms, const ModulePW::PW_Basis& basis, double tpiba, double spread, - std::vector& density, - std::string& error); + std::vector& density); } // namespace ModuleSccs #endif diff --git a/source/source_hamilt/module_sccs/sccs_ionic_force.cpp b/source/source_hamilt/module_sccs/sccs_ionic_force.cpp index 44bc25712c1..0d6ee4f6726 100644 --- a/source/source_hamilt/module_sccs/sccs_ionic_force.cpp +++ b/source/source_hamilt/module_sccs/sccs_ionic_force.cpp @@ -1,5 +1,4 @@ #include "sccs_ionic_force.h" -#include "sccs_pw_coulomb.h" #include "source_base/constants.h" #include "source_base/parallel_reduce.h" @@ -11,39 +10,16 @@ namespace ModuleSccs { -bool gaussian_ionic_force(const std::vector& atoms, +void gaussian_ionic_force(const std::vector& atoms, const std::vector& reaction_potential, const ModulePW::PW_Basis& basis, double tpiba, double spread, - std::vector>& forces, - std::string& error) + std::vector>& forces) { - if (!validate_pw_grid(basis, tpiba, error) - || !validate_grid_values(reaction_potential, basis, error)) - { - return false; - } - double invalid = 0.0; - if (!std::isfinite(spread) || spread <= 0.0) { invalid = 1.0; } - for (const unitcell::AtomData& atom : atoms) - { - if (!std::isfinite(atom.valence_charge) || atom.valence_charge < 0.0 - || !std::isfinite(atom.position.x) || !std::isfinite(atom.position.y) - || !std::isfinite(atom.position.z)) - { - invalid = 1.0; - } - } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); - if (invalid != 0.0) - { - error = "SCCS Gaussian forces require finite atoms and a positive finite spread"; - return false; - } std::vector> potential_g(basis.npw); basis.real2recip(reaction_potential.data(), potential_g.data()); - std::vector> candidate(atoms.size()); + forces.assign(atoms.size(), ModuleBase::Vector3(0.0, 0.0, 0.0)); const double tpiba2 = tpiba * tpiba; // F = -integral(v_reaction d rho_ion/d R), using ABACUS's normalized FFT. // The cell volume cancels the Z/omega in the Gaussian charge coefficients. @@ -59,19 +35,11 @@ bool gaussian_ionic_force(const std::vector& atoms, const std::complex phase_factor = std::exp(phase_argument); const std::complex weighted = std::conj(potential_g[ig]) * phase_factor; const double factor = -atom.valence_charge * tpiba * gaussian * weighted.imag(); - candidate[ia] += basis.gcar[ig] * factor; - } - Parallel_Reduce::reduce_pool(candidate[ia].x); - Parallel_Reduce::reduce_pool(candidate[ia].y); - Parallel_Reduce::reduce_pool(candidate[ia].z); - if (!std::isfinite(candidate[ia].x) || !std::isfinite(candidate[ia].y) - || !std::isfinite(candidate[ia].z)) - { - error = "SCCS Gaussian ionic force is not finite"; - return false; + forces[ia] += basis.gcar[ig] * factor; } + Parallel_Reduce::reduce_pool(forces[ia].x); + Parallel_Reduce::reduce_pool(forces[ia].y); + Parallel_Reduce::reduce_pool(forces[ia].z); } - forces.swap(candidate); - return true; } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_ionic_force.h b/source/source_hamilt/module_sccs/sccs_ionic_force.h index 400e0d2eda5..150781b03d8 100644 --- a/source/source_hamilt/module_sccs/sccs_ionic_force.h +++ b/source/source_hamilt/module_sccs/sccs_ionic_force.h @@ -1,7 +1,6 @@ #ifndef SCCS_IONIC_FORCE_H #define SCCS_IONIC_FORCE_H -#include #include namespace ModuleBase @@ -20,13 +19,12 @@ namespace ModuleSccs { // Explicit Gaussian-ion derivative of the reaction energy at fixed electron // density. reaction_potential is in Ha; force is in Ha/Bohr, atom-major. -// Collective over the PW pool; failure leaves forces unchanged. -bool gaussian_ionic_force(const std::vector& atoms, +// Collective over the PW pool. +void gaussian_ionic_force(const std::vector& atoms, const std::vector& reaction_potential, const ModulePW::PW_Basis& basis, double tpiba, double spread, - std::vector>& forces, - std::string& error); + std::vector>& forces); } // namespace ModuleSccs #endif diff --git a/source/source_hamilt/module_sccs/sccs_parameters.cpp b/source/source_hamilt/module_sccs/sccs_parameters.cpp index 8471bf75a8d..0a8712174f6 100644 --- a/source/source_hamilt/module_sccs/sccs_parameters.cpp +++ b/source/source_hamilt/module_sccs/sccs_parameters.cpp @@ -1,7 +1,7 @@ #include "sccs_parameters.h" -#include -#include +#include "source_base/tool_quit.h" + namespace ModuleSccs { @@ -25,102 +25,54 @@ double gpa_to_hartree_per_bohr3(double value) return pascal * bohr_metre * bohr_metre * bohr_metre / hartree_joule; } -bool parse_preset(const std::string& name, Preset& preset, std::string& error) +Preset parse_preset(const std::string& name) { - error.clear(); - std::string normalized = name; - for (char& character : normalized) - { - const unsigned char byte = static_cast(character); - const int lower = std::tolower(byte); - character = static_cast(lower); - } - if (normalized == "custom") - { - preset = Preset::Custom; - } - else if (normalized == "vacuum") - { - preset = Preset::Vacuum; - } - else if (normalized == "water-neutral") - { - preset = Preset::WaterNeutral; - } - else if (normalized == "water-cation") - { - preset = Preset::WaterCation; - } - else if (normalized == "water-anion") - { - preset = Preset::WaterAnion; - } - else - { - error = "Unknown SCCS preset: " + name; - return false; - } - return true; + if (name == "custom") { return Preset::Custom; } + if (name == "vacuum") { return Preset::Vacuum; } + if (name == "water-neutral") { return Preset::WaterNeutral; } + if (name == "water-cation") { return Preset::WaterCation; } + if (name == "water-anion") { return Preset::WaterAnion; } + const std::string message = "Unknown SCCS preset: " + name; + ModuleBase::WARNING_QUIT("ModuleSccs::parse_preset", message); } -bool make_sccs_config(Preset preset, SccsConfig& config, std::string& error) +SccsConfig make_sccs_config(Preset preset) { - error.clear(); - SccsConfig candidate; - candidate.cavity.epsilon_bulk = 78.3; - candidate.surface_regularization = 1e-8; + if (preset == Preset::Custom) + { + ModuleBase::WARNING_QUIT("ModuleSccs::make_sccs_config", + "SCCS custom configurations must be supplied explicitly"); + } + SccsConfig config; + config.cavity.epsilon_bulk = 78.3; + config.surface_regularization = 1e-8; if (preset == Preset::Vacuum || preset == Preset::WaterNeutral) { - candidate.cavity.density_min = 1e-4; - candidate.cavity.density_max = 5e-3; + config.cavity.density_min = 1e-4; + config.cavity.density_max = 5e-3; if (preset == Preset::Vacuum) { - candidate.cavity.epsilon_bulk = 1.0; + config.cavity.epsilon_bulk = 1.0; } else { - candidate.surface_tension = dyn_per_cm_to_hartree_per_bohr2(47.9); - candidate.pressure = gpa_to_hartree_per_bohr3(-0.36); + config.surface_tension = dyn_per_cm_to_hartree_per_bohr2(47.9); + config.pressure = gpa_to_hartree_per_bohr3(-0.36); } } else if (preset == Preset::WaterCation) { - candidate.cavity.density_min = 2e-4; - candidate.cavity.density_max = 3.5e-3; - candidate.surface_tension = dyn_per_cm_to_hartree_per_bohr2(5.0); - candidate.pressure = gpa_to_hartree_per_bohr3(0.125); - } - else if (preset == Preset::WaterAnion) - { - candidate.cavity.density_min = 2.4e-3; - candidate.cavity.density_max = 1.55e-2; - candidate.pressure = gpa_to_hartree_per_bohr3(0.45); + config.cavity.density_min = 2e-4; + config.cavity.density_max = 3.5e-3; + config.surface_tension = dyn_per_cm_to_hartree_per_bohr2(5.0); + config.pressure = gpa_to_hartree_per_bohr3(0.125); } else { - error = "SCCS custom configurations must be supplied explicitly"; - return false; - } - config = candidate; - return true; -} - -bool validate_config(const SccsConfig& config, std::string& error) -{ - if (!validate_cavity_parameters(config.cavity, error)) - { - return false; - } - if (!std::isfinite(config.surface_tension) || !std::isfinite(config.pressure)) - { - error = "SCCS requires finite surface tension and pressure"; - return false; - } - if (!std::isfinite(config.surface_regularization) || config.surface_regularization <= 0.0) - { - error = "SCCS requires finite positive surface regularization"; - return false; + config.cavity.density_min = 2.4e-3; + config.cavity.density_max = 1.55e-2; + config.pressure = gpa_to_hartree_per_bohr3(0.45); } - return true; + return config; } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_parameters.h b/source/source_hamilt/module_sccs/sccs_parameters.h index 6f16df44efe..707c5c0c386 100644 --- a/source/source_hamilt/module_sccs/sccs_parameters.h +++ b/source/source_hamilt/module_sccs/sccs_parameters.h @@ -24,10 +24,11 @@ struct PolarizationSolverParameters double tolerance_max = 1e-8; }; -// Failure leaves the output unchanged. Custom configurations are supplied by callers. -bool parse_preset(const std::string& name, Preset& preset, std::string& error); -bool make_sccs_config(Preset preset, SccsConfig& config, std::string& error); -bool validate_config(const SccsConfig& config, std::string& error); +// Exact, lower-case names as accepted by the INPUT reader; an unknown name +// stops the run. +Preset parse_preset(const std::string& name); +// Published presets only; custom configurations are supplied by callers. +SccsConfig make_sccs_config(Preset preset); double dyn_per_cm_to_hartree_per_bohr2(double value); double gpa_to_hartree_per_bohr3(double value); } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp b/source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp index 7879baac064..62fbebabd20 100644 --- a/source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp +++ b/source/source_hamilt/module_sccs/sccs_pw_coulomb.cpp @@ -1,73 +1,17 @@ #include "sccs_pw_coulomb.h" #include "source_base/constants.h" -#include "source_base/parallel_reduce.h" #include "source_basis/module_pw/pw_basis.h" -#include - namespace ModuleSccs { -bool validate_pw_grid(const ModulePW::PW_Basis& basis, double tpiba, std::string& error) -{ - error.clear(); - double invalid = 0.0; - if (!std::isfinite(tpiba) || tpiba <= 0.0 || !std::isfinite(basis.omega) || basis.omega <= 0.0 - || basis.gamma_only || basis.nxyz <= 0 || basis.nrxx < 0 || basis.npw < 0 - || basis.nmaxgr < basis.nrxx || basis.nmaxgr < basis.npw - || (basis.npw > 0 && (basis.gg == nullptr || basis.gcar == nullptr))) - { - invalid = 1.0; - } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); - if (invalid != 0.0) - { - error = "SCCS requires an initialized full-G PW grid and positive finite tpiba and volume on every pool rank"; - return false; - } - return true; -} - -bool validate_grid_values(const std::vector& values, - const ModulePW::PW_Basis& basis, - std::string& error) -{ - error.clear(); - const std::size_t local_size = basis.nrxx; - double invalid = 0.0; - if (values.size() != local_size) - { - invalid = 1.0; - } - for (double value : values) - { - if (!std::isfinite(value)) - { - invalid = 1.0; - } - } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); - if (invalid != 0.0) - { - error = "SCCS requires finite values matching the local PW grid on every pool rank"; - return false; - } - return true; -} - PeriodicCoulombOperator::PeriodicCoulombOperator(const ModulePW::PW_Basis& basis, double tpiba) : basis_(basis), tpiba_(tpiba) { } -bool PeriodicCoulombOperator::apply_potential(const std::vector& charge, - std::vector& potential, - std::string& error) +void PeriodicCoulombOperator::apply_potential(const std::vector& charge, std::vector& potential) { - if (!validate_pw_grid(basis_, tpiba_, error) || !validate_grid_values(charge, basis_, error)) - { - return false; - } reciprocal_work_.resize(basis_.npw); basis_.real2recip(charge.data(), reciprocal_work_.data()); const double tpiba2 = tpiba_ * tpiba_; @@ -82,13 +26,7 @@ bool PeriodicCoulombOperator::apply_potential(const std::vector& charge, reciprocal_work_[ig] = ModuleBase::FOUR_PI * reciprocal_work_[ig] / (tpiba2 * basis_.gg[ig]); } } - std::vector candidate(basis_.nrxx); - basis_.recip2real(reciprocal_work_.data(), candidate.data()); - if (!validate_grid_values(candidate, basis_, error)) - { - return false; - } - potential.swap(candidate); - return true; + potential.resize(basis_.nrxx); + basis_.recip2real(reciprocal_work_.data(), potential.data()); } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_pw_coulomb.h b/source/source_hamilt/module_sccs/sccs_pw_coulomb.h index 5368fa7624e..41245a45608 100644 --- a/source/source_hamilt/module_sccs/sccs_pw_coulomb.h +++ b/source/source_hamilt/module_sccs/sccs_pw_coulomb.h @@ -2,7 +2,6 @@ #define SCCS_PW_COULOMB_H #include -#include #include namespace ModulePW @@ -12,21 +11,14 @@ class PW_Basis; namespace ModuleSccs { -// All calls are collective over the ABACUS pool used to initialize basis. -bool validate_pw_grid(const ModulePW::PW_Basis& basis, double tpiba, std::string& error); -bool validate_grid_values(const std::vector& values, - const ModulePW::PW_Basis& basis, - std::string& error); - -// Potential in Hartree for a signed charge density in e/Bohr^3. -// G=0 is removed: this is the periodic Coulomb inverse with a uniform background. +// Potential in Hartree for a signed charge density in e/Bohr^3. Calls are +// collective over the ABACUS pool used to initialize basis. G=0 is removed: +// this is the periodic Coulomb inverse with a uniform background. class PeriodicCoulombOperator { public: PeriodicCoulombOperator(const ModulePW::PW_Basis& basis, double tpiba); - bool apply_potential(const std::vector& charge, - std::vector& potential, - std::string& error); + void apply_potential(const std::vector& charge, std::vector& potential); private: const ModulePW::PW_Basis& basis_; diff --git a/source/source_hamilt/module_sccs/sccs_response.cpp b/source/source_hamilt/module_sccs/sccs_response.cpp index dd79dada12d..1baa0fcc2af 100644 --- a/source/source_hamilt/module_sccs/sccs_response.cpp +++ b/source/source_hamilt/module_sccs/sccs_response.cpp @@ -4,25 +4,26 @@ #include "source_base/constants.h" #include "source_base/parallel_reduce.h" +#include "source_base/tool_quit.h" #include "source_basis/module_pw/pw_basis.h" #include "source_hamilt/module_xc/xc_functional.h" #include #include #include +#include #include namespace ModuleSccs { namespace { -bool prepare_cavity(const std::vector& density, +void prepare_cavity(const std::vector& density, const CavityParameters& cavity, const ModulePW::PW_Basis& basis, double tpiba, SccsResponse& response, - std::vector& coefficient, - std::string& error) + std::vector& coefficient) { const std::size_t size = density.size(); response.solute.resize(size); @@ -30,26 +31,14 @@ bool prepare_cavity(const std::vector& density, response.epsilon.resize(size); response.depsilon_drho.resize(size); response.grad_log_epsilon.resize(size); - double invalid = 0.0; for (std::size_t i = 0; i < size; ++i) { - CavityPoint point; - if (!evaluate_cavity(density[i], cavity, point, error)) - { - invalid = 1.0; - continue; - } + const CavityPoint point = evaluate_cavity(density[i], cavity); response.solute[i] = point.solute; response.dsolute_drho[i] = point.dsolute_drho; response.epsilon[i] = point.epsilon; response.depsilon_drho[i] = point.depsilon_drho; } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); - if (invalid != 0.0) - { - error = "SCCS cavity evaluation failed on a pool rank"; - return false; - } std::vector> density_g(basis.npw); basis.real2recip(density.data(), density_g.data()); @@ -81,39 +70,30 @@ bool prepare_cavity(const std::vector& density, coefficient[i] = response.epsilon[i] * (0.5 * lap_log + 0.25 * first_log * first_log * gradient_square) / ModuleBase::FOUR_PI; } - return validate_grid_values(coefficient, basis, error); } -bool residual_norms(const std::vector& values, +// Pool-reduced norms, so every rank takes the same convergence decision. +void residual_norms(const std::vector& values, const ModulePW::PW_Basis& basis, double& rms, - double& maximum, - std::string& error) + double& maximum) { double square = 0.0; maximum = 0.0; - double invalid = 0.0; for (double value : values) { - if (!std::isfinite(value)) - { - invalid = 1.0; - } square += value * value; const double magnitude = std::abs(value); maximum = std::max(maximum, magnitude); } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); Parallel_Reduce::reduce_pool(square); Parallel_Reduce::reduce_max_pool(basis.poolnproc, maximum); - if (invalid != 0.0 || !std::isfinite(square)) + if (!std::isfinite(square) || !std::isfinite(maximum)) { - error = "SCCS sqrt-CG residual is not finite"; - return false; + ModuleBase::WARNING_QUIT("ModuleSccs::solve_sccs_response", "SCCS sqrt-CG residual is not finite"); } const double mean_square = square / basis.nxyz; rms = std::sqrt(mean_square); - return true; } bool converged(const PolarizationResult& result, const PolarizationSolverParameters& solver) @@ -135,12 +115,11 @@ double grid_dot(const std::vector& left, } // P r = eps^-1/2 G eps^-1/2 r; only FFT scratch survives an application. -bool apply_preconditioner(const std::vector& rhs, +void apply_preconditioner(const std::vector& rhs, const std::vector& invsqrt, PeriodicCoulombOperator& coulomb, std::vector& weighted, - std::vector& value, - std::string& error) + std::vector& value) { const std::size_t size = rhs.size(); weighted.resize(size); @@ -148,62 +127,31 @@ bool apply_preconditioner(const std::vector& rhs, { weighted[i] = rhs[i] * invsqrt[i]; } - if (!coulomb.apply_potential(weighted, value, error)) - { - return false; - } + coulomb.apply_potential(weighted, value); for (std::size_t i = 0; i < size; ++i) { value[i] *= invsqrt[i]; } - return true; } } // namespace -bool solve_sccs_response(const std::vector& density, +void solve_sccs_response(const std::vector& density, const std::vector& charge, const CavityParameters& cavity, const PolarizationSolverParameters& solver, const std::vector& initial_potential, const ModulePW::PW_Basis& basis, double tpiba, - SccsResponse& result, - std::string& error) + SccsResponse& result) { - if (!validate_pw_grid(basis, tpiba, error) || !validate_grid_values(density, basis, error) - || !validate_grid_values(charge, basis, error)) - { - return false; - } - double invalid = 0.0; - if (!validate_cavity_parameters(cavity, error) || solver.max_iterations <= 0 - || !std::isfinite(solver.tolerance_rms) || solver.tolerance_rms <= 0.0 - || !std::isfinite(solver.tolerance_max) || solver.tolerance_max <= 0.0) - { - invalid = 1.0; - } - Parallel_Reduce::reduce_max_pool(basis.poolnproc, invalid); - if (invalid != 0.0) - { - error = "SCCS requires valid cavity parameters, a positive iteration limit and finite positive tolerances"; - return false; - } - // Determine start mode collectively, including ranks with no real-space points. double initial_count = initial_potential.size(); Parallel_Reduce::reduce_pool(initial_count); const bool warm_start = initial_count != 0.0; - if (warm_start && !validate_grid_values(initial_potential, basis, error)) - { - return false; - } SccsResponse candidate; std::vector coefficient; - if (!prepare_cavity(density, cavity, basis, tpiba, candidate, coefficient, error)) - { - return false; - } + prepare_cavity(density, cavity, basis, tpiba, candidate, coefficient); const std::size_t size = density.size(); std::vector invsqrt(size); for (std::size_t i = 0; i < size; ++i) @@ -218,10 +166,7 @@ bool solve_sccs_response(const std::vector& density, std::vector weighted; std::vector z; PolarizationResult& polarization = candidate.polarization; - if (!residual_norms(residual, basis, polarization.residual_rms, polarization.residual_max, error)) - { - return false; - } + residual_norms(residual, basis, polarization.residual_rms, polarization.residual_max); // Retain the old fixed-point warm start only when it reduces the charge residual. if (!converged(polarization, solver) && warm_start) { @@ -230,20 +175,14 @@ bool solve_sccs_response(const std::vector& density, { guess_residual[i] = charge[i] - coefficient[i] * initial_potential[i]; } - if (!apply_preconditioner(guess_residual, invsqrt, coulomb, weighted, z, error)) - { - return false; - } + apply_preconditioner(guess_residual, invsqrt, coulomb, weighted, z); for (std::size_t i = 0; i < size; ++i) { guess_residual[i] = coefficient[i] * (initial_potential[i] - z[i]); } double guess_rms = 0.0; double guess_max = 0.0; - if (!residual_norms(guess_residual, basis, guess_rms, guess_max, error)) - { - return false; - } + residual_norms(guess_residual, basis, guess_rms, guess_max); if (guess_rms < polarization.residual_rms) { potential.swap(z); @@ -256,15 +195,12 @@ bool solve_sccs_response(const std::vector& density, double old_rz = 0.0; for (int iteration = 1; !converged(polarization, solver) && iteration <= solver.max_iterations; ++iteration) { - if (!apply_preconditioner(residual, invsqrt, coulomb, weighted, z, error)) - { - return false; - } + apply_preconditioner(residual, invsqrt, coulomb, weighted, z); const double rz = grid_dot(residual, z, basis); if (!std::isfinite(rz) || std::abs(rz) < 1e-30) { - error = "SCCS sqrt-CG has a null or nonfinite preconditioned residual"; - return false; + ModuleBase::WARNING_QUIT("ModuleSccs::solve_sccs_response", + "SCCS sqrt-CG has a null or nonfinite preconditioned residual"); } const double beta = std::abs(old_rz) > 1e-30 ? rz / old_rz : 0.0; old_rz = rz; @@ -276,8 +212,7 @@ bool solve_sccs_response(const std::vector& density, const double curvature = grid_dot(direction, image, basis); if (!std::isfinite(curvature) || curvature == 0.0) { - error = "SCCS sqrt-CG has invalid curvature"; - return false; + ModuleBase::WARNING_QUIT("ModuleSccs::solve_sccs_response", "SCCS sqrt-CG has invalid curvature"); } const double alpha = rz / curvature; for (std::size_t i = 0; i < size; ++i) @@ -286,21 +221,17 @@ bool solve_sccs_response(const std::vector& density, residual[i] -= alpha * image[i]; } polarization.iterations = iteration; - if (!residual_norms(residual, basis, polarization.residual_rms, polarization.residual_max, error)) - { - return false; - } + residual_norms(residual, basis, polarization.residual_rms, polarization.residual_max); } if (!converged(polarization, solver)) { - error = "SCCS sqrt-CG did not reach both residual tolerances within the iteration limit"; - return false; + std::ostringstream message; + message << "SCCS sqrt-CG did not reach both residual tolerances within sccs_maxiter = " + << solver.max_iterations << " iterations (RMS " << polarization.residual_rms << ", MAX " + << polarization.residual_max << ")"; + ModuleBase::WARNING_QUIT("ModuleSccs::solve_sccs_response", message.str()); } - if (!validate_grid_values(potential, basis, error)) - { - return false; - } candidate.restart_potential = potential; double mean = 0.0; for (double value : potential) @@ -325,11 +256,6 @@ bool solve_sccs_response(const std::vector& density, const double gradient_square = gradient * gradient; candidate.cavity_potential[i] = -candidate.depsilon_drho[i] * gradient_square / (8.0 * ModuleBase::PI); } - if (!validate_grid_values(candidate.cavity_potential, basis, error)) - { - return false; - } result = std::move(candidate); - return true; } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_response.h b/source/source_hamilt/module_sccs/sccs_response.h index 0befd59f7ff..bc81ad622b9 100644 --- a/source/source_hamilt/module_sccs/sccs_response.h +++ b/source/source_hamilt/module_sccs/sccs_response.h @@ -4,7 +4,6 @@ #include "sccs_cavity.h" #include "source_base/vector3.h" -#include #include namespace ModulePW @@ -41,16 +40,16 @@ struct SccsResponse // Original chain-derivative sqrt-CG periodic response; charge is ions minus electrons. // All ranks in the PW pool must call together with the same configuration. // initial_potential is empty on all ranks for a cold start, otherwise a local grid. -// Failure leaves result unchanged. No state or INPUT globals are read or retained. -bool solve_sccs_response(const std::vector& cavity_density, +// A solver breakdown or missed tolerance stops the run with WARNING_QUIT. +// No state or INPUT globals are read or retained. +void solve_sccs_response(const std::vector& cavity_density, const std::vector& solute_charge, const CavityParameters& cavity, const PolarizationSolverParameters& solver, const std::vector& initial_potential, const ModulePW::PW_Basis& basis, double tpiba, - SccsResponse& result, - std::string& error); + SccsResponse& result); } // namespace ModuleSccs #endif diff --git a/source/source_hamilt/module_sccs/test/CMakeLists.txt b/source/source_hamilt/module_sccs/test/CMakeLists.txt index 1bd0619b719..722c01ebd01 100644 --- a/source/source_hamilt/module_sccs/test/CMakeLists.txt +++ b/source/source_hamilt/module_sccs/test/CMakeLists.txt @@ -1,6 +1,7 @@ AddTest(TARGET MODULE_HAMILT_sccs_cavity SOURCES test_sccs_cavity.cpp ../sccs_cavity.cpp) AddTest(TARGET MODULE_HAMILT_sccs_parameters + LIBS base device SOURCES test_sccs_parameters.cpp ../sccs_parameters.cpp ../sccs_cavity.cpp) AddTest(TARGET MODULE_HAMILT_sccs_poisson diff --git a/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp b/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp index 72050a2299f..108296255f6 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp @@ -2,7 +2,6 @@ #include #include -#include namespace { @@ -10,7 +9,6 @@ class SccsCavityTest : public ::testing::Test { protected: ModuleSccs::CavityParameters parameters; - std::string error; void SetUp() override { @@ -26,7 +24,7 @@ TEST_F(SccsCavityTest, BulkAndSoluteLimits) for (double density : bulk_densities) { ModuleSccs::CavityPoint point; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; + point = ModuleSccs::evaluate_cavity(density, parameters); EXPECT_DOUBLE_EQ(point.solute, 0.0); EXPECT_DOUBLE_EQ(point.epsilon, 78.3); EXPECT_DOUBLE_EQ(point.dsolute_drho, 0.0); @@ -36,7 +34,7 @@ TEST_F(SccsCavityTest, BulkAndSoluteLimits) for (double density : solute_densities) { ModuleSccs::CavityPoint point; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; + point = ModuleSccs::evaluate_cavity(density, parameters); EXPECT_DOUBLE_EQ(point.solute, 1.0); EXPECT_DOUBLE_EQ(point.epsilon, 1.0); EXPECT_DOUBLE_EQ(point.dsolute_drho, 0.0); @@ -55,7 +53,7 @@ TEST_F(SccsCavityTest, LogarithmicMidpoint) const double dsolute = 2.0 / (width * density); const double depsilon = -epsilon * log_epsilon * dsolute; ModuleSccs::CavityPoint point; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; + point = ModuleSccs::evaluate_cavity(density, parameters); EXPECT_NEAR(point.solute, 0.5, 1e-14); EXPECT_NEAR(point.epsilon, epsilon, 1e-13); EXPECT_NEAR(point.dsolute_drho, dsolute, 1e-10); @@ -76,9 +74,9 @@ TEST_F(SccsCavityTest, DerivativesAcrossTransition) ModuleSccs::CavityPoint point; ModuleSccs::CavityPoint lower; ModuleSccs::CavityPoint upper; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(density, parameters, point, error)) << error; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(lower_density, parameters, lower, error)) << error; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(upper_density, parameters, upper, error)) << error; + point = ModuleSccs::evaluate_cavity(density, parameters); + lower = ModuleSccs::evaluate_cavity(lower_density, parameters); + upper = ModuleSccs::evaluate_cavity(upper_density, parameters); const double solute_fd = (upper.solute - lower.solute) / (2.0 * step); const double epsilon_fd = (upper.epsilon - lower.epsilon) / (2.0 * step); const double solute_ratio = solute_fd / point.dsolute_drho; @@ -96,8 +94,8 @@ TEST_F(SccsCavityTest, SmoothThresholds) const double near_max = parameters.density_max * (1.0 - 1e-8); ModuleSccs::CavityPoint lower; ModuleSccs::CavityPoint upper; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(near_min, parameters, lower, error)) << error; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(near_max, parameters, upper, error)) << error; + lower = ModuleSccs::evaluate_cavity(near_min, parameters); + upper = ModuleSccs::evaluate_cavity(near_max, parameters); EXPECT_NEAR(lower.solute, 0.0, 1e-14); EXPECT_NEAR(upper.solute, 1.0, 1e-14); EXPECT_NEAR(lower.epsilon, 78.3, 1e-12); @@ -112,37 +110,10 @@ TEST_F(SccsCavityTest, VacuumHasNoDielectricResponse) { parameters.epsilon_bulk = 1.0; ModuleSccs::CavityPoint point; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(1e-3, parameters, point, error)) << error; + point = ModuleSccs::evaluate_cavity(1e-3, parameters); EXPECT_GT(point.solute, 0.0); EXPECT_LT(point.solute, 1.0); EXPECT_DOUBLE_EQ(point.epsilon, 1.0); EXPECT_DOUBLE_EQ(point.depsilon_drho, 0.0); } - -TEST_F(SccsCavityTest, InvalidInputPreservesResult) -{ - ModuleSccs::CavityPoint point; - point.solute = 0.25; - point.epsilon = 12.0; - const double nan = std::numeric_limits::quiet_NaN(); - EXPECT_FALSE(ModuleSccs::evaluate_cavity(nan, parameters, point, error)); - EXPECT_FALSE(error.empty()); - EXPECT_DOUBLE_EQ(point.solute, 0.25); - EXPECT_DOUBLE_EQ(point.epsilon, 12.0); - parameters.density_min = parameters.density_max; - EXPECT_FALSE(ModuleSccs::evaluate_cavity(1e-3, parameters, point, error)); - EXPECT_DOUBLE_EQ(point.epsilon, 12.0); - parameters.density_min = 1e-4; - parameters.epsilon_bulk = 0.5; - EXPECT_FALSE(ModuleSccs::validate_cavity_parameters(parameters, error)); - parameters.epsilon_bulk = nan; - EXPECT_FALSE(ModuleSccs::validate_cavity_parameters(parameters, error)); - parameters.epsilon_bulk = 78.3; - parameters.density_min = std::numeric_limits::min(); - parameters.density_max = std::numeric_limits::max(); - EXPECT_FALSE(ModuleSccs::evaluate_cavity(1.0, parameters, point, error)); - EXPECT_FALSE(error.empty()); - EXPECT_DOUBLE_EQ(point.solute, 0.25); - EXPECT_DOUBLE_EQ(point.epsilon, 12.0); -} } // namespace diff --git a/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp b/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp index 2e92399542c..c25cde778d4 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_functional.cpp @@ -23,10 +23,10 @@ TEST_F(SccsFunctionalTest, UniformDielectricEnergyAndElectronDerivative) const std::vector density(basis.nrxx, 0.0); const std::vector cold; ModuleSccs::SccsResponse response; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, config.cavity, solver, cold, - basis, tpiba, response, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, config.cavity, solver, cold, + basis, tpiba, response); ModuleSccs::FunctionalResult result; - ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, result, error)) << error; + ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, result); const double kernel = ModuleBase::FOUR_PI / (tpiba * tpiba); const double expected = 0.25 * basis.omega * 1e-6 * kernel * (0.2 - 1.0); EXPECT_NEAR(result.reaction_energy, expected, 1e-13); @@ -41,8 +41,7 @@ TEST_F(SccsFunctionalTest, UniformDielectricEnergyAndElectronDerivative) TEST_F(SccsFunctionalTest, NonElectrostaticDiscreteEnergyDerivative) { - ModuleSccs::SccsConfig config; - ASSERT_TRUE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum, config, error)); + ModuleSccs::SccsConfig config = ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum); config.surface_tension = 0.01; config.pressure = -0.001; config.surface_regularization = 0.02; @@ -58,7 +57,7 @@ TEST_F(SccsFunctionalTest, NonElectrostaticDiscreteEnergyDerivative) response.solute[ir] = 0.5 + 0.2 * mode[ir]; } ModuleSccs::FunctionalResult center; - ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, center, error)); + ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, center); // Avoid cancellation of grid-integrated energies at excessively small steps. const double step = 1e-5; double predicted = 0.0; @@ -71,12 +70,12 @@ TEST_F(SccsFunctionalTest, NonElectrostaticDiscreteEnergyDerivative) predicted *= basis.omega / basis.nxyz; ModuleSccs::FunctionalResult plus; ModuleSccs::FunctionalResult minus; - ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, plus, error)); + ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, plus); for (int ir = 0; ir < basis.nrxx; ++ir) { response.solute[ir] -= 2.0 * step * mode[ir]; } - ASSERT_TRUE(ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, minus, error)); + ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, minus); const double fd = (plus.surface_energy + plus.volume_energy - minus.surface_energy - minus.volume_energy) / (2.0 * step); EXPECT_NEAR(fd, predicted, 1e-8); diff --git a/source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp b/source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp index 64a0a2cc617..27c2b795746 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_ionic_charge.cpp @@ -14,7 +14,7 @@ TEST_F(SccsIonicChargeTest, ValenceNormalizationAndFourierPhase) atoms[1].valence_charge = 1.0; const double spread = ModuleSccs::gaussian_ion_spread; std::vector density; - ASSERT_TRUE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, spread, density, error)) << error; + ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, spread, density); double integral = 0.0; for (double value : density) { @@ -45,32 +45,9 @@ TEST_F(SccsIonicChargeTest, ValenceNormalizationAndFourierPhase) EXPECT_DOUBLE_EQ(selected, 1.0); atoms[0].position.x += length; std::vector translated; - ASSERT_TRUE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, spread, translated, error)) << error; + ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, spread, translated); for (int ir = 0; ir < basis.nrxx; ++ir) { EXPECT_NEAR(translated[ir], density[ir], 1e-13); } } - -TEST_F(SccsIonicChargeTest, InvalidIonAndSpreadPreserveOutput) -{ - std::vector atoms(1); - atoms[0].valence_charge = 1.0; - if (basis.poolrank == 0) - { - atoms[0].valence_charge = -1.0; - } - std::vector density(1, 12.0); - EXPECT_FALSE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, 0.5, density, error)); - EXPECT_FALSE(error.empty()); - EXPECT_DOUBLE_EQ(density[0], 12.0); - atoms[0].valence_charge = 1.0; - EXPECT_FALSE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, 0.0, density, error)); - EXPECT_DOUBLE_EQ(density[0], 12.0); - atoms.clear(); - ASSERT_TRUE(ModuleSccs::gaussian_ionic_density(atoms, basis, tpiba, 0.5, density, error)) << error; - for (double value : density) - { - EXPECT_DOUBLE_EQ(value, 0.0); - } -} diff --git a/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp b/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp index 846c9502571..9b482dec854 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_ionic_force.cpp @@ -6,7 +6,6 @@ #include "../sccs_functional.h" #include "source_cell/cell_tools.h" -#include using SccsIonicForceTest = SccsTest::PwTest; @@ -18,7 +17,7 @@ TEST_F(SccsIonicForceTest, AnalyticFourierForceAndGaugeInvariance) std::vector potential = cosine_mode(0); const double spread = ModuleSccs::gaussian_ion_spread; std::vector> forces; - ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces, error)) << error; + ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces); const double exponent = -0.25 * spread * spread * tpiba * tpiba; const double expected = 2.0 * tpiba * std::exp(exponent); EXPECT_NEAR(forces[0].x, expected, 1e-12); @@ -26,10 +25,10 @@ TEST_F(SccsIonicForceTest, AnalyticFourierForceAndGaugeInvariance) EXPECT_NEAR(forces[0].z, 0.0, 1e-12); atoms[0].position.x += length; for (double& value : potential) { value += 4.0; } - ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces, error)) << error; + ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces); EXPECT_NEAR(forces[0].x, expected, 1e-12); potential.assign(basis.nrxx, 3.0); - ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces, error)) << error; + ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, spread, forces); EXPECT_NEAR(forces[0].x, 0.0, 1e-12); EXPECT_NEAR(forces[0].y, 0.0, 1e-12); EXPECT_NEAR(forces[0].z, 0.0, 1e-12); @@ -55,20 +54,19 @@ TEST_F(SccsIonicForceTest, ReactionEnergyFiniteDifferenceAtFixedNonuniformCavity const double spread = ModuleSccs::gaussian_ion_spread; const std::vector cold_start; auto evaluate = [&](const std::vector& displaced, - ModuleSccs::FunctionalResult& functional) -> bool { + ModuleSccs::FunctionalResult& functional) { std::vector charge; - if (!ModuleSccs::gaussian_ionic_density(displaced, basis, tpiba, spread, charge, error)) { return false; } + ModuleSccs::gaussian_ionic_density(displaced, basis, tpiba, spread, charge); for (int ir = 0; ir < basis.nrxx; ++ir) { charge[ir] -= density[ir]; } ModuleSccs::SccsResponse response; - if (!ModuleSccs::solve_sccs_response(density, charge, config.cavity, solver, cold_start, - basis, tpiba, response, error)) { return false; } - return ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, functional, error); + ModuleSccs::solve_sccs_response(density, charge, config.cavity, solver, cold_start, basis, tpiba, response); + ModuleSccs::evaluate_functional(charge, response, config, basis, tpiba, functional); }; ModuleSccs::FunctionalResult baseline; - ASSERT_TRUE(evaluate(atoms, baseline)) << error; + evaluate(atoms, baseline); std::vector> forces; - ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, baseline.reaction_potential, basis, - tpiba, spread, forces, error)) << error; + ModuleSccs::gaussian_ionic_force(atoms, baseline.reaction_potential, basis, + tpiba, spread, forces); const double step = 1e-4; for (std::size_t ia = 0; ia < atoms.size(); ++ia) { @@ -80,16 +78,16 @@ TEST_F(SccsIonicForceTest, ReactionEnergyFiniteDifferenceAtFixedNonuniformCavity minus[ia].position[axis] -= step; ModuleSccs::FunctionalResult positive; ModuleSccs::FunctionalResult negative; - ASSERT_TRUE(evaluate(plus, positive)) << error; - ASSERT_TRUE(evaluate(minus, negative)) << error; + evaluate(plus, positive); + evaluate(minus, negative); const double finite_difference = -(positive.reaction_energy - negative.reaction_energy) / (2.0 * step); EXPECT_NEAR(forces[ia][axis], finite_difference, 1e-8); } } config.cavity.epsilon_bulk = 1.0; - ASSERT_TRUE(evaluate(atoms, baseline)) << error; - ASSERT_TRUE(ModuleSccs::gaussian_ionic_force(atoms, baseline.reaction_potential, basis, - tpiba, spread, forces, error)) << error; + evaluate(atoms, baseline); + ModuleSccs::gaussian_ionic_force(atoms, baseline.reaction_potential, basis, + tpiba, spread, forces); for (const ModuleBase::Vector3& force : forces) { EXPECT_NEAR(force.x, 0.0, 1e-12); @@ -97,21 +95,3 @@ TEST_F(SccsIonicForceTest, ReactionEnergyFiniteDifferenceAtFixedNonuniformCavity EXPECT_NEAR(force.z, 0.0, 1e-12); } } - -TEST_F(SccsIonicForceTest, DistributedInvalidInputPreservesOutput) -{ - std::vector atoms(1); - atoms[0].valence_charge = 1.0; - std::vector potential(basis.nrxx, 0.0); - if (basis.poolrank == 0) { atoms[0].position.x = std::numeric_limits::quiet_NaN(); } - const ModuleBase::Vector3 sentinel(7.0, 8.0, 9.0); - std::vector> forces(1, sentinel); - EXPECT_FALSE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, 0.5, forces, error)); - EXPECT_DOUBLE_EQ(forces[0].x, 7.0); - atoms[0].position.x = 0.0; - EXPECT_FALSE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, 0.0, forces, error)); - EXPECT_DOUBLE_EQ(forces[0].y, 8.0); - if (basis.poolrank == 0 && basis.nrxx > 0) { potential[0] = std::numeric_limits::infinity(); } - EXPECT_FALSE(ModuleSccs::gaussian_ionic_force(atoms, potential, basis, tpiba, 0.5, forces, error)); - EXPECT_DOUBLE_EQ(forces[0].z, 9.0); -} diff --git a/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp b/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp index c120d8bd053..47ac6fc713a 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp @@ -1,26 +1,21 @@ #include "../sccs_parameters.h" #include -#include +TEST(SccsParameters, PresetNames) +{ + EXPECT_EQ(ModuleSccs::parse_preset("water-neutral"), ModuleSccs::Preset::WaterNeutral); + EXPECT_EQ(ModuleSccs::parse_preset("water-cation"), ModuleSccs::Preset::WaterCation); + EXPECT_EQ(ModuleSccs::parse_preset("water-anion"), ModuleSccs::Preset::WaterAnion); + EXPECT_EQ(ModuleSccs::parse_preset("vacuum"), ModuleSccs::Preset::Vacuum); + EXPECT_EQ(ModuleSccs::parse_preset("custom"), ModuleSccs::Preset::Custom); +} -TEST(SccsParameters, PresetNamesAndFailure) +TEST(SccsParameters, UnknownPresetStops) { - std::string error; - ModuleSccs::Preset preset = ModuleSccs::Preset::Custom; - ASSERT_TRUE(ModuleSccs::parse_preset("Water-Neutral", preset, error)); - EXPECT_EQ(preset, ModuleSccs::Preset::WaterNeutral); - ASSERT_TRUE(ModuleSccs::parse_preset("WATER-CATION", preset, error)); - EXPECT_EQ(preset, ModuleSccs::Preset::WaterCation); - ASSERT_TRUE(ModuleSccs::parse_preset("water-anion", preset, error)); - EXPECT_EQ(preset, ModuleSccs::Preset::WaterAnion); - EXPECT_FALSE(ModuleSccs::parse_preset("unknown", preset, error)); - EXPECT_FALSE(error.empty()); - EXPECT_EQ(preset, ModuleSccs::Preset::WaterAnion); - ASSERT_TRUE(ModuleSccs::parse_preset("vacuum", preset, error)); - EXPECT_EQ(preset, ModuleSccs::Preset::Vacuum); - EXPECT_TRUE(error.empty()); - ASSERT_TRUE(ModuleSccs::parse_preset("custom", preset, error)); - EXPECT_EQ(preset, ModuleSccs::Preset::Custom); + testing::internal::CaptureStdout(); + EXPECT_EXIT(ModuleSccs::parse_preset("unknown"), ::testing::ExitedWithCode(1), ""); + EXPECT_EXIT(ModuleSccs::parse_preset("Water-Neutral"), ::testing::ExitedWithCode(1), ""); + testing::internal::GetCapturedStdout(); } TEST(SccsParameters, OriginalWaterPresets) @@ -32,12 +27,9 @@ TEST(SccsParameters, OriginalWaterPresets) const double maxima[] = {5e-3, 3.5e-3, 1.55e-2}; const double tensions[] = {3.076640259577260e-5, 3.211524279308205e-6, 0.0}; const double pressures[] = {-1.223615131827540e-5, 4.248663652178958e-6, 1.529518914784425e-5}; - std::string error; for (int index = 0; index < 3; ++index) { - ModuleSccs::SccsConfig config; - ASSERT_TRUE(ModuleSccs::make_sccs_config(presets[index], config, error)) << error; - ASSERT_TRUE(ModuleSccs::validate_config(config, error)) << error; + const ModuleSccs::SccsConfig config = ModuleSccs::make_sccs_config(presets[index]); EXPECT_DOUBLE_EQ(config.cavity.density_min, minima[index]); EXPECT_DOUBLE_EQ(config.cavity.density_max, maxima[index]); EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 78.3); @@ -47,20 +39,12 @@ TEST(SccsParameters, OriginalWaterPresets) } } -TEST(SccsParameters, VacuumAndExplicitCustom) +TEST(SccsParameters, Vacuum) { - ModuleSccs::SccsConfig config; - std::string error; - ASSERT_TRUE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum, config, error)); - ASSERT_TRUE(ModuleSccs::validate_config(config, error)); + const ModuleSccs::SccsConfig config = ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum); EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 1.0); EXPECT_DOUBLE_EQ(config.surface_tension, 0.0); EXPECT_DOUBLE_EQ(config.pressure, 0.0); - config.cavity.epsilon_bulk = 12.0; - EXPECT_FALSE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::Custom, config, error)); - EXPECT_FALSE(error.empty()); - EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 12.0); - EXPECT_TRUE(ModuleSccs::validate_config(config, error)); } TEST(SccsParameters, PhysicalUnitConversions) @@ -70,26 +54,3 @@ TEST(SccsParameters, PhysicalUnitConversions) EXPECT_NEAR(tension, 6.423048558616410e-7, 1e-20); EXPECT_NEAR(pressure, 3.398930921743166e-5, 1e-18); } - -TEST(SccsParameters, InvalidConfiguration) -{ - ModuleSccs::SccsConfig config; - std::string error; - ASSERT_TRUE(ModuleSccs::make_sccs_config(ModuleSccs::Preset::WaterNeutral, config, error)); - config.surface_regularization = 0.0; - EXPECT_FALSE(ModuleSccs::validate_config(config, error)); - EXPECT_FALSE(error.empty()); - config.surface_regularization = 1e-8; - const double nan = std::numeric_limits::quiet_NaN(); - config.pressure = nan; - EXPECT_FALSE(ModuleSccs::validate_config(config, error)); - config.pressure = -0.1; - config.surface_tension = nan; - EXPECT_FALSE(ModuleSccs::validate_config(config, error)); - config.surface_tension = 0.0; - config.cavity.density_min = -1.0; - EXPECT_FALSE(ModuleSccs::validate_config(config, error)); - config.cavity.density_min = 1e-4; - EXPECT_TRUE(ModuleSccs::validate_config(config, error)); - EXPECT_TRUE(error.empty()); -} diff --git a/source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp b/source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp index 40e3d3c4c98..a36b7247fef 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_pw_coulomb.cpp @@ -3,8 +3,6 @@ #include "source_base/parallel_global.h" -#include - namespace SccsTest { int pool_size = 1; @@ -23,7 +21,7 @@ TEST_F(SccsPwCoulombTest, FourierModeAndConstantBackground) } ModuleSccs::PeriodicCoulombOperator coulomb(basis, tpiba); std::vector potential; - ASSERT_TRUE(coulomb.apply_potential(charge, potential, error)) << error; + coulomb.apply_potential(charge, potential); const double kernel = ModuleBase::FOUR_PI / (tpiba * tpiba); for (int ir = 0; ir < basis.nrxx; ++ir) { @@ -31,35 +29,13 @@ TEST_F(SccsPwCoulombTest, FourierModeAndConstantBackground) EXPECT_NEAR(potential[ir], expected, 1e-11); } charge.assign(basis.nrxx, 1.0); - ASSERT_TRUE(coulomb.apply_potential(charge, potential, error)) << error; + coulomb.apply_potential(charge, potential); for (double value : potential) { EXPECT_NEAR(value, 0.0, 1e-12); } } -TEST_F(SccsPwCoulombTest, RankLocalInvalidInputReturnsCollectively) -{ - std::vector charge(basis.nrxx, 0.0); - if (basis.poolrank == 0) - { - charge[0] = std::numeric_limits::quiet_NaN(); - } - ModuleSccs::PeriodicCoulombOperator coulomb(basis, tpiba); - std::vector potential(1, 12.0); - EXPECT_FALSE(coulomb.apply_potential(charge, potential, error)); - EXPECT_FALSE(error.empty()); - ASSERT_EQ(potential.size(), 1u); - EXPECT_DOUBLE_EQ(potential[0], 12.0); - charge.assign(basis.nrxx, 0.0); - if (basis.poolrank == 0) - { - charge.pop_back(); - } - EXPECT_FALSE(coulomb.apply_potential(charge, potential, error)); - EXPECT_DOUBLE_EQ(potential[0], 12.0); -} - int main(int argc, char** argv) { int threads = 1; @@ -73,15 +49,15 @@ int main(int argc, char** argv) GRID_WORLD = MPI_COMM_NULL; DIAG_WORLD = MPI_COMM_NULL; Parallel_Global::init_pools(SccsTest::pool_size, - SccsTest::pool_rank, - 1, - 1, - band_size, - band_rank, - band_group, - SccsTest::pool_size, - SccsTest::pool_rank, - pool); + SccsTest::pool_rank, + 1, + 1, + band_size, + band_rank, + band_group, + SccsTest::pool_size, + SccsTest::pool_rank, + pool); #endif testing::InitGoogleTest(&argc, argv); const int status = RUN_ALL_TESTS(); diff --git a/source/source_hamilt/module_sccs/test/test_sccs_response.cpp b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp index da061b95b35..d7f65667898 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_response.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp @@ -24,8 +24,8 @@ TEST_F(SccsResponseTest, VacuumAndUniformDielectricAnalyticLimits) { cavity.epsilon_bulk = epsilon; ModuleSccs::SccsResponse response; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, response, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, response); const double kernel = ModuleBase::FOUR_PI / (epsilon * tpiba * tpiba); EXPECT_EQ(response.polarization.iterations, 1); for (int ir = 0; ir < basis.nrxx; ++ir) @@ -50,8 +50,7 @@ TEST_F(SccsResponseTest, ManufacturedNonuniformDielectricSolution) for (int ir = 0; ir < basis.nrxx; ++ir) { density[ir] = 1e-3 + 2e-5 * mode_x[ir]; - ModuleSccs::CavityPoint point; - ASSERT_TRUE(ModuleSccs::evaluate_cavity(density[ir], cavity, point, error)); + const ModuleSccs::CavityPoint point = ModuleSccs::evaluate_cavity(density[ir], cavity); // v=cos(ky), eps=eps(x): -div(eps grad v)/(4 pi)=eps k^2 cos(ky)/(4 pi). charge[ir] = point.epsilon * tpiba * tpiba * mode_y[ir] / ModuleBase::FOUR_PI; } @@ -60,8 +59,8 @@ TEST_F(SccsResponseTest, ManufacturedNonuniformDielectricSolution) solver.tolerance_max = 1e-12; const std::vector cold_start; ModuleSccs::SccsResponse response; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, response, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, response); double maximum_error = 0.0; for (int ir = 0; ir < basis.nrxx; ++ir) { @@ -95,15 +94,19 @@ TEST_F(SccsResponseTest, ConvergenceFailureAndExplicitWarmStart) solver.tolerance_max = 1e-10; const std::vector cold_start; ModuleSccs::SccsResponse response; - solver.max_iterations = 1; - response.polarization.potential.assign(1, 12.0); - EXPECT_FALSE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, response, error)); - EXPECT_NE(error.find("iteration limit"), std::string::npos); - EXPECT_DOUBLE_EQ(response.polarization.potential[0], 12.0); + // A missed tolerance stops the run; a forked death test cannot share the + // pool collectives of a multi-rank run. + if (SccsTest::pool_size == 1) + { + solver.max_iterations = 1; + testing::internal::CaptureStdout(); + EXPECT_EXIT(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, basis, tpiba, + response), + ::testing::ExitedWithCode(1), ""); + testing::internal::GetCapturedStdout(); + } solver.max_iterations = 200; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, response, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, basis, tpiba, response); EXPECT_LE(response.polarization.residual_rms, solver.tolerance_rms); EXPECT_LE(response.polarization.residual_max, solver.tolerance_max); for (double& value : density) @@ -112,10 +115,10 @@ TEST_F(SccsResponseTest, ConvergenceFailureAndExplicitWarmStart) } ModuleSccs::SccsResponse cold; ModuleSccs::SccsResponse warm; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, cold, error)) << error; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, response.restart_potential, - basis, tpiba, warm, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, cold); + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, response.restart_potential, + basis, tpiba, warm); EXPECT_TRUE(warm.polarization.warm_started); EXPECT_LT(warm.polarization.iterations, cold.polarization.iterations); for (int ir = 0; ir < basis.nrxx; ++ir) @@ -124,36 +127,6 @@ TEST_F(SccsResponseTest, ConvergenceFailureAndExplicitWarmStart) } } -TEST_F(SccsResponseTest, RankLocalInvalidParametersAndRestartReturnCollectively) -{ - ModuleSccs::CavityParameters cavity; - cavity.density_min = 1e-4; - cavity.density_max = 5e-3; - cavity.epsilon_bulk = 5.0; - ModuleSccs::PolarizationSolverParameters solver; - const std::vector density(basis.nrxx, 0.0); - const std::vector charge = cosine_mode(0); - const std::vector cold_start; - ModuleSccs::SccsResponse response; - response.polarization.potential.assign(1, 12.0); - if (basis.poolrank == 0) - { - solver.max_iterations = 0; - } - EXPECT_FALSE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, response, error)); - EXPECT_DOUBLE_EQ(response.polarization.potential[0], 12.0); - solver.max_iterations = 200; - std::vector initial(basis.nrxx, 0.0); - if (basis.poolrank == 0) - { - initial.pop_back(); - } - EXPECT_FALSE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, initial, - basis, tpiba, response, error)); - EXPECT_DOUBLE_EQ(response.polarization.potential[0], 12.0); -} - TEST_F(SccsResponseTest, RequiresBothResidualTolerances) { const std::vector mode_x = cosine_mode(0); @@ -175,15 +148,15 @@ TEST_F(SccsResponseTest, RequiresBothResidualTolerances) ModuleSccs::SccsResponse loose; ModuleSccs::SccsResponse tight_maximum; ModuleSccs::SccsResponse tight_rms; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, loose, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, loose); solver.tolerance_max = 1e-11; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, tight_maximum, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, tight_maximum); solver.tolerance_rms = 1e-11; solver.tolerance_max = 1.0; - ASSERT_TRUE(ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, - basis, tpiba, tight_rms, error)) << error; + ModuleSccs::solve_sccs_response(density, charge, cavity, solver, cold_start, + basis, tpiba, tight_rms); EXPECT_LE(tight_maximum.polarization.residual_max, 1e-11); EXPECT_LE(tight_rms.polarization.residual_rms, 1e-11); EXPECT_GT(tight_maximum.polarization.iterations, loose.polarization.iterations); diff --git a/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/INPUT b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/INPUT new file mode 100644 index 00000000000..8eb0415ef14 --- /dev/null +++ b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/INPUT @@ -0,0 +1,21 @@ +INPUT_PARAMETERS +suffix autotest +pseudo_dir ../../PP_ORB +calculation scf +basis_type pw +init_wfc random +pw_seed 1 +nbands 8 +ecutwfc 20 +ecutrho 240 +scf_nmax 80 +scf_thr 1.0e-8 +cal_force 1 + +imp_sol 2 +assume_isolated none +sccs_preset water-neutral +sccs_maxiter 200 +sccs_tol_rms 1.0e-10 +sccs_tol_max 1.0e-8 +sccs_surface_eta 1.0e-8 diff --git a/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/KPT b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/KPT new file mode 100644 index 00000000000..c289c0158aa --- /dev/null +++ b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/KPT @@ -0,0 +1,4 @@ +K_POINTS +0 +Gamma +1 1 1 0 0 0 diff --git a/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/README b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/README new file mode 100644 index 00000000000..44f4976649c --- /dev/null +++ b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/README @@ -0,0 +1,2 @@ +PW neutral water with periodic SCCS (water-neutral preset), ecutwfc 20 and ecutrho 240. +Checks total energy, forces and electrostatic, surface and volume solvation terms. diff --git a/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/STRU b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/STRU new file mode 100644 index 00000000000..ed974dd43a5 --- /dev/null +++ b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/STRU @@ -0,0 +1,24 @@ +ATOMIC_SPECIES +H 1.008 H_ONCV_PBE-1.0.upf +O 15.9994 O_ONCV_PBE-1.0.upf + +LATTICE_CONSTANT +1.0 + +LATTICE_VECTORS +12.0 0.0 0.0 +0.0 12.0 0.0 +0.0 0.0 12.0 + +ATOMIC_POSITIONS +Cartesian + +H +0.0 +2 +7.433 7.111 6.0 0 0 0 +4.567 7.111 6.0 0 0 0 +O +0.0 +1 +6.0 6.0 6.0 0 0 0 diff --git a/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/result.ref b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/result.ref new file mode 100644 index 00000000000..a1c90a54215 --- /dev/null +++ b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/result.ref @@ -0,0 +1,6 @@ +etotref -441.6484359463214560 +etotperatomref -147.2161453154 +totalforceref 16.644169 +esolelref -0.2883000404 +esolcavref 0.0807425541 +totaltimeref 3.33 diff --git a/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/threshold b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/threshold new file mode 100644 index 00000000000..a125b952d62 --- /dev/null +++ b/tests/01_PW/220_PW_SCCS_PERIODIC_WATER/threshold @@ -0,0 +1,3 @@ +threshold 0.000001 +force_threshold 0.0001 +fatal_threshold 0.01 diff --git a/tests/01_PW/CASES_CPU.txt b/tests/01_PW/CASES_CPU.txt index 324309c3fb2..f07cac3c682 100644 --- a/tests/01_PW/CASES_CPU.txt +++ b/tests/01_PW/CASES_CPU.txt @@ -140,3 +140,4 @@ scf_deltaspin2 scf_deltaspin4 221_PW_PCC0D_H3O 222_PW_PCC2D_H3O +220_PW_SCCS_PERIODIC_WATER From f983f77f2c1ccfffb1297a91306b6fd1bb6f7ad7 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Tue, 6 Oct 2026 17:49:39 +0000 Subject: [PATCH 28/41] refactor(pcc): drop defensive checks on internal values The geometry, grid and moment helpers only receive values that ABACUS has already built from STRU and INPUT, so their finite/size/storage checks and bool+error returns could never fire. - grid_positions and charge_moments return their result directly. - weighted_center returns the center; make_pcc_2d_parameters returns the parameters. - make_orthogonal_cell, make_slab_cell and make_pcc_0d_parameters keep a bool for the structure conditions a user can violate (perpendicular, cubic); PotPcc turns them into WARNING_QUIT messages. - PotPcc no longer reduces replicated structure checks over the pool (every rank sees the same cell and stops together), and drops the storage, nonfinite-result and stale-result checks. validate_kpoints keeps its cross-pool reduction, since each pool holds its own k points. - write_elecstat_pot drops the PCC potential size check. Tests for the removed failure paths are removed; a death test covers the non-cubic 0D and tilted 2D cells. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BKqk2SNWXoZn2PRRGRzGca --- .../module_pw/pw_grid_geometry.cpp | 32 +--- .../source_basis/module_pw/pw_grid_geometry.h | 12 +- .../test_serial/test_pw_grid_geometry.cpp | 13 +- source/source_cell/cell_geometry.cpp | 141 +++------------- source/source_cell/cell_geometry.h | 34 ++-- .../source_cell/test/test_cell_geometry.cpp | 92 ++--------- source/source_estate/charge_moments.cpp | 43 +---- source/source_estate/charge_moments.h | 14 +- source/source_estate/module_pot/pot_pcc.cpp | 154 ++++-------------- source/source_estate/module_pot/pot_pcc.h | 1 - source/source_estate/pcc_0d.cpp | 21 +-- source/source_estate/pcc_0d.h | 7 +- source/source_estate/pcc_2d.cpp | 13 +- source/source_estate/pcc_2d.h | 4 +- .../test/test_charge_moments.cpp | 40 +---- source/source_estate/test/test_pcc_0d.cpp | 22 +-- source/source_estate/test/test_pcc_2d.cpp | 14 +- source/source_estate/test/test_pot_pcc.cpp | 31 +--- source/source_estate/write_elecstat_pot.cpp | 4 - 19 files changed, 144 insertions(+), 548 deletions(-) diff --git a/source/source_basis/module_pw/pw_grid_geometry.cpp b/source/source_basis/module_pw/pw_grid_geometry.cpp index ccac0c0b3f7..e9a9cfc2a4e 100644 --- a/source/source_basis/module_pw/pw_grid_geometry.cpp +++ b/source/source_basis/module_pw/pw_grid_geometry.cpp @@ -3,29 +3,17 @@ #include "pw_basis.h" #include "source_base/matrix3.h" -#include - namespace ModulePW { -bool grid_positions(const PW_Basis& basis, - const ModuleBase::Matrix3& lattice, - const double lattice_scale, - std::vector>& positions, - std::string& error) +void grid_positions(const PW_Basis& basis, + const ModuleBase::Matrix3& lattice, + const double lattice_scale, + std::vector>& positions) { - error.clear(); - if (basis.nx <= 0 || basis.ny <= 0 || basis.nz <= 0 || basis.nplane < 0 - || basis.nrxx != basis.nx * basis.ny * basis.nplane - || basis.startz_current < 0 || basis.startz_current + basis.nplane > basis.nz - || !std::isfinite(lattice_scale) || lattice_scale <= 0.0) - { - error = "PW grid coordinates require valid dimensions, slab offset and lattice scale"; - return false; - } const ModuleBase::Vector3 a1(lattice.e11, lattice.e12, lattice.e13); const ModuleBase::Vector3 a2(lattice.e21, lattice.e22, lattice.e23); const ModuleBase::Vector3 a3(lattice.e31, lattice.e32, lattice.e33); - std::vector> candidate(basis.nrxx); + positions.resize(basis.nrxx); for (int ir = 0; ir < basis.nrxx; ++ir) { const int ix = ir / (basis.ny * basis.nplane); @@ -34,15 +22,7 @@ bool grid_positions(const PW_Basis& basis, const double fx = static_cast(ix) / basis.nx; const double fy = static_cast(iy) / basis.ny; const double fz = static_cast(iz) / basis.nz; - const ModuleBase::Vector3 position = (a1 * fx + a2 * fy + a3 * fz) * lattice_scale; - if (!std::isfinite(position.x) || !std::isfinite(position.y) || !std::isfinite(position.z)) - { - error = "PW grid coordinates must be finite"; - return false; - } - candidate[ir] = position; + positions[ir] = (a1 * fx + a2 * fy + a3 * fz) * lattice_scale; } - positions.swap(candidate); - return true; } } // namespace ModulePW diff --git a/source/source_basis/module_pw/pw_grid_geometry.h b/source/source_basis/module_pw/pw_grid_geometry.h index 52390ad19ce..c1b46e641e1 100644 --- a/source/source_basis/module_pw/pw_grid_geometry.h +++ b/source/source_basis/module_pw/pw_grid_geometry.h @@ -3,7 +3,6 @@ #include "source_base/vector3.h" -#include #include namespace ModuleBase @@ -16,12 +15,11 @@ namespace ModulePW class PW_Basis; /// Cartesian coordinates in Bohr of this rank's integer FFT grid nodes. -/// Supports an empty local slab; on failure, leave positions unchanged. -bool grid_positions(const PW_Basis& basis, - const ModuleBase::Matrix3& lattice, - double lattice_scale, - std::vector>& positions, - std::string& error); +/// Supports an empty local slab. +void grid_positions(const PW_Basis& basis, + const ModuleBase::Matrix3& lattice, + double lattice_scale, + std::vector>& positions); } // namespace ModulePW #endif diff --git a/source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp b/source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp index 89077bcc1ee..a0aa25ec9ac 100644 --- a/source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp +++ b/source/source_basis/module_pw/test_serial/test_pw_grid_geometry.cpp @@ -17,8 +17,7 @@ TEST(PwGridGeometry, UsesGlobalSlabOffsetAndRotatedLattice) -2.0, 0.0, 0.0, 0.0, 0.0, 3.0); std::vector> positions; - std::string error; - ASSERT_TRUE(ModulePW::grid_positions(basis, lattice, 10.0, positions, error)); + ModulePW::grid_positions(basis, lattice, 10.0, positions); ASSERT_EQ(positions.size(), 8u); EXPECT_DOUBLE_EQ(positions[0].x, 0.0); EXPECT_DOUBLE_EQ(positions[0].y, 0.0); @@ -28,7 +27,7 @@ TEST(PwGridGeometry, UsesGlobalSlabOffsetAndRotatedLattice) EXPECT_DOUBLE_EQ(positions[7].z, 22.5); } -TEST(PwGridGeometry, PermitsEmptySlabsAndRejectsInvalidOffsets) +TEST(PwGridGeometry, PermitsEmptySlabs) { ModulePW::PW_Basis basis("cpu", "double"); basis.nx = 2; @@ -39,12 +38,6 @@ TEST(PwGridGeometry, PermitsEmptySlabsAndRejectsInvalidOffsets) basis.nrxx = 0; const ModuleBase::Matrix3 lattice; std::vector> positions; - std::string error; - ASSERT_TRUE(ModulePW::grid_positions(basis, lattice, 10.0, positions, error)); + ModulePW::grid_positions(basis, lattice, 10.0, positions); EXPECT_TRUE(positions.empty()); - basis.nplane = 1; - basis.nrxx = 4; - EXPECT_FALSE(ModulePW::grid_positions(basis, lattice, 10.0, positions, error)); - EXPECT_TRUE(positions.empty()); - EXPECT_FALSE(error.empty()); } diff --git a/source/source_cell/cell_geometry.cpp b/source/source_cell/cell_geometry.cpp index fb1b212fe2e..cbe439ccadf 100644 --- a/source/source_cell/cell_geometry.cpp +++ b/source/source_cell/cell_geometry.cpp @@ -4,69 +4,39 @@ #include -namespace -{ -bool finite_vector(const ModuleBase::Vector3& value) -{ - return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); -} -} // namespace - namespace unitcell { bool make_orthogonal_cell(const ModuleBase::Matrix3& lattice, const double lattice_scale, const double relative_tolerance, - OrthogonalCell& cell, - std::string& error) + OrthogonalCell& cell) { - error.clear(); - if (!std::isfinite(lattice_scale) || lattice_scale <= 0.0 - || !std::isfinite(relative_tolerance) || relative_tolerance <= 0.0) - { - error = "cell scale and relative tolerance must be positive and finite"; - return false; - } - const ModuleBase::Vector3 a(lattice.e11, lattice.e12, lattice.e13); const ModuleBase::Vector3 b(lattice.e21, lattice.e22, lattice.e23); const ModuleBase::Vector3 c(lattice.e31, lattice.e32, lattice.e33); const std::array, 3> vectors = {{a, b, c}}; - OrthogonalCell candidate; for (int axis = 0; axis < 3; ++axis) { const ModuleBase::Vector3 physical_vector = vectors[axis] * lattice_scale; const double length = physical_vector.norm(); - if (!finite_vector(physical_vector) || !std::isfinite(length) || length <= 0.0) - { - error = "cell lattice vectors must have positive finite lengths"; - return false; - } - candidate.lengths[axis] = length; - candidate.axes[axis] = physical_vector / length; + cell.lengths[axis] = length; + cell.axes[axis] = physical_vector / length; } + const ModuleBase::Vector3 vector_sum = a + b + c; + const double half_scale = 0.5 * lattice_scale; + cell.origin = vector_sum * half_scale; for (int left = 0; left < 3; ++left) { for (int right = left + 1; right < 3; ++right) { - const double projection = candidate.axes[left] * candidate.axes[right]; + const double projection = cell.axes[left] * cell.axes[right]; if (std::abs(projection) > relative_tolerance) { - error = "cell lattice vectors must be mutually perpendicular"; return false; } } } - const ModuleBase::Vector3 vector_sum = a + b + c; - const double half_scale = 0.5 * lattice_scale; - candidate.origin = vector_sum * half_scale; - if (!finite_vector(candidate.origin)) - { - error = "cell center must be finite"; - return false; - } - cell = candidate; return true; } @@ -87,65 +57,32 @@ ModuleBase::Vector3 relative_position(const ModuleBase::Vector3& return relative; } -bool weighted_center(const std::vector>& positions, - const std::vector& weights, - const OrthogonalCell& cell, - ModuleBase::Vector3& center, - std::string& error) +ModuleBase::Vector3 weighted_center(const std::vector>& positions, + const std::vector& weights, + const OrthogonalCell& cell) { - error.clear(); - if (positions.empty() || positions.size() != weights.size()) - { - error = "weighted center requires matching non-empty positions and weights"; - return false; - } OrthogonalCell reference = cell; reference.origin = positions.front(); double total_weight = 0.0; ModuleBase::Vector3 weighted_displacement; for (std::size_t index = 0; index < positions.size(); ++index) { - if (!finite_vector(positions[index]) || !std::isfinite(weights[index]) || weights[index] <= 0.0) - { - error = "weighted center requires finite positions and positive finite weights"; - return false; - } const ModuleBase::Vector3 displacement = relative_position(positions[index], reference); weighted_displacement += displacement * weights[index]; total_weight += weights[index]; } - if (!std::isfinite(total_weight) || !finite_vector(weighted_displacement)) - { - error = "weighted center accumulation must be finite"; - return false; - } const ModuleBase::Vector3 mean_displacement = weighted_displacement / total_weight; const ModuleBase::Vector3 unwrapped_center = positions.front() + mean_displacement; const ModuleBase::Vector3 wrapped_center = relative_position(unwrapped_center, cell); - const ModuleBase::Vector3 candidate = cell.origin + wrapped_center; - if (!finite_vector(candidate)) - { - error = "weighted center must be finite"; - return false; - } - center = candidate; - return true; + return cell.origin + wrapped_center; } bool make_slab_cell(const ModuleBase::Matrix3& lattice, const double lattice_scale, const int open_axis, const double relative_tolerance, - SlabCell& cell, - std::string& error) + SlabCell& cell) { - error.clear(); - if (open_axis < 0 || open_axis > 2 || !std::isfinite(lattice_scale) || lattice_scale <= 0.0 - || !std::isfinite(relative_tolerance) || relative_tolerance <= 0.0) - { - error = "slab geometry requires open axis 0/1/2 and positive finite scale and tolerance"; - return false; - } const ModuleBase::Vector3 a(lattice.e11, lattice.e12, lattice.e13); const ModuleBase::Vector3 b(lattice.e21, lattice.e22, lattice.e23); const ModuleBase::Vector3 c(lattice.e31, lattice.e32, lattice.e33); @@ -156,33 +93,17 @@ bool make_slab_cell(const ModuleBase::Matrix3& lattice, const ModuleBase::Vector3 first = vectors[first_axis] * lattice_scale; const ModuleBase::Vector3 second = vectors[second_axis] * lattice_scale; const double length = open.norm(); - const double first_length = first.norm(); - const double second_length = second.norm(); const ModuleBase::Vector3 plane_normal = first ^ second; const double area = plane_normal.norm(); - if (!finite_vector(open) || !finite_vector(first) || !finite_vector(second) - || !std::isfinite(length) || length <= 0.0 || !std::isfinite(area) - || area <= relative_tolerance * first_length * second_length) - { - error = "slab geometry requires finite nonzero open and independent periodic lattice vectors"; - return false; - } const double orientation = (plane_normal * open < 0.0) ? -1.0 : 1.0; const double normal_scale = orientation / area; const ModuleBase::Vector3 normal = plane_normal * normal_scale; + cell.normal = normal; + cell.length = length; + cell.area = area; + cell.origin = 0.5 * length; const ModuleBase::Vector3 alignment = open ^ normal; - if (alignment.norm() > relative_tolerance * length) - { - error = "slab open lattice vector must be perpendicular to the periodic plane"; - return false; - } - SlabCell candidate; - candidate.normal = normal; - candidate.length = length; - candidate.area = area; - candidate.origin = 0.5 * length; - cell = candidate; - return true; + return alignment.norm() <= relative_tolerance * length; } double relative_coordinate(const ModuleBase::Vector3& position, const SlabCell& cell) @@ -193,29 +114,16 @@ double relative_coordinate(const ModuleBase::Vector3& position, const Sl return displacement - cell.length * image; } -bool weighted_center(const std::vector>& positions, - const std::vector& weights, - const SlabCell& cell, - double& center, - std::string& error) +double weighted_center(const std::vector>& positions, + const std::vector& weights, + const SlabCell& cell) { - error.clear(); - if (positions.empty() || positions.size() != weights.size()) - { - error = "slab center requires matching non-empty positions and weights"; - return false; - } SlabCell reference = cell; reference.origin = positions.front() * cell.normal; double total_weight = 0.0; double weighted_displacement = 0.0; for (std::size_t index = 0; index < positions.size(); ++index) { - if (!finite_vector(positions[index]) || !std::isfinite(weights[index]) || weights[index] <= 0.0) - { - error = "slab center requires finite positions and positive finite weights"; - return false; - } const double displacement = relative_coordinate(positions[index], reference); weighted_displacement += weights[index] * displacement; total_weight += weights[index]; @@ -223,14 +131,7 @@ bool weighted_center(const std::vector>& positions, const double unwrapped = reference.origin + weighted_displacement / total_weight; const double image_coordinate = unwrapped / cell.length; const double image = std::floor(image_coordinate); - const double candidate = unwrapped - image * cell.length; - if (!std::isfinite(total_weight) || !std::isfinite(weighted_displacement) || !std::isfinite(candidate)) - { - error = "slab center accumulation must be finite"; - return false; - } - center = candidate; - return true; + return unwrapped - image * cell.length; } } // namespace unitcell diff --git a/source/source_cell/cell_geometry.h b/source/source_cell/cell_geometry.h index 591476bdbd1..639bfad09cf 100644 --- a/source/source_cell/cell_geometry.h +++ b/source/source_cell/cell_geometry.h @@ -4,7 +4,6 @@ #include "source_base/vector3.h" #include -#include #include namespace ModuleBase @@ -24,13 +23,12 @@ struct OrthogonalCell ModuleBase::Vector3 origin; }; -/// Accept rectangular cells, including rotated cells. On failure, leave cell -/// unchanged and explain the invalid input in error. No model parameters are read. +/// Accept rectangular cells, including rotated cells. Return false if the +/// lattice vectors are not mutually perpendicular within relative_tolerance. bool make_orthogonal_cell(const ModuleBase::Matrix3& lattice, double lattice_scale, double relative_tolerance, - OrthogonalCell& cell, - std::string& error); + OrthogonalCell& cell); /// Minimum-image displacement from origin, with projections in [-L/2, L/2). /// Requires a cell constructed above and a finite Cartesian position. @@ -40,13 +38,11 @@ ModuleBase::Vector3 relative_position(const ModuleBase::Vector3& /// Unwrap positions about the first position, take their positive-weight center, /// then wrap the center about cell.origin. Intended for localized systems whose /// extent about the first position is less than half a cell in each direction. -/// Requires a cell constructed by make_orthogonal_cell. -/// On failure, leave center unchanged and set error. -bool weighted_center(const std::vector>& positions, - const std::vector& weights, - const OrthogonalCell& cell, - ModuleBase::Vector3& center, - std::string& error); +/// Requires a cell constructed by make_orthogonal_cell, at least one position +/// and positive weights. +ModuleBase::Vector3 weighted_center(const std::vector>& positions, + const std::vector& weights, + const OrthogonalCell& cell); /// A periodic plane and its perpendicular open lattice vector, in Bohr. /// The two in-plane lattice vectors need not be orthogonal. @@ -58,24 +54,22 @@ struct SlabCell double origin = 0.0; }; +/// Return false if the open lattice vector is not perpendicular to the +/// periodic plane within relative_tolerance. bool make_slab_cell(const ModuleBase::Matrix3& lattice, double lattice_scale, int open_axis, double relative_tolerance, - SlabCell& cell, - std::string& error); + SlabCell& cell); /// Normal displacement in [-L/2, L/2); requires a validated cell and position. double relative_coordinate(const ModuleBase::Vector3& position, const SlabCell& cell); /// Mass/weight center along the normal, unwrapped about the first position. /// Requires a localized slab of extent less than half the normal period. -/// On failure, leave center unchanged and set error. -bool weighted_center(const std::vector>& positions, - const std::vector& weights, - const SlabCell& cell, - double& center, - std::string& error); +double weighted_center(const std::vector>& positions, + const std::vector& weights, + const SlabCell& cell); } // namespace unitcell diff --git a/source/source_cell/test/test_cell_geometry.cpp b/source/source_cell/test/test_cell_geometry.cpp index b9a39fec6a3..07aca405f8d 100644 --- a/source/source_cell/test/test_cell_geometry.cpp +++ b/source/source_cell/test/test_cell_geometry.cpp @@ -4,7 +4,6 @@ #include #include -#include namespace { @@ -16,11 +15,10 @@ class CellGeometryTest : public testing::Test const ModuleBase::Matrix3 lattice(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); - ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell, error)); + ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell)); } unitcell::OrthogonalCell cell; - std::string error; }; void expect_vector(const ModuleBase::Vector3& actual, @@ -37,7 +35,7 @@ TEST_F(CellGeometryTest, BuildsRotatedRectangularCell) const ModuleBase::Matrix3 lattice(0.0, 1.0, 0.0, -2.0, 0.0, 0.0, 0.0, 0.0, 3.0); - ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell, error)); + ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell)); EXPECT_DOUBLE_EQ(cell.lengths[0], 10.0); EXPECT_DOUBLE_EQ(cell.lengths[1], 20.0); EXPECT_DOUBLE_EQ(cell.lengths[2], 30.0); @@ -66,8 +64,7 @@ TEST_F(CellGeometryTest, UnwrapsWeightedCenterAcrossCellBoundary) ModuleBase::Vector3(9.8, 9.7, 9.6), ModuleBase::Vector3(0.2, 0.3, 0.4)}; const std::vector weights = {1.0, 3.0}; - ModuleBase::Vector3 center; - ASSERT_TRUE(unitcell::weighted_center(positions, weights, cell, center, error)); + const ModuleBase::Vector3 center = unitcell::weighted_center(positions, weights, cell); const ModuleBase::Vector3 expected(0.1, 0.15, 0.2); expect_vector(center, expected); } @@ -78,10 +75,8 @@ TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) ModuleBase::Vector3(9.8, 9.6, 0.1), ModuleBase::Vector3(0.3, 0.4, 9.7)}; const std::vector weights = {2.0, 1.0}; - ModuleBase::Vector3 center; - ASSERT_TRUE(unitcell::weighted_center(original, weights, cell, center, error)); unitcell::OrthogonalCell first = cell; - first.origin = center; + first.origin = unitcell::weighted_center(original, weights, cell); std::vector> translated = original; for (auto& position : translated) @@ -93,9 +88,8 @@ TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) position.y = std::fmod(translated_y, 10.0); position.z = std::fmod(translated_z, 10.0); } - ASSERT_TRUE(unitcell::weighted_center(translated, weights, cell, center, error)); unitcell::OrthogonalCell second = cell; - second.origin = center; + second.origin = unitcell::weighted_center(translated, weights, cell); for (std::size_t index = 0; index < original.size(); ++index) { const ModuleBase::Vector3 expected = unitcell::relative_position(original[index], first); @@ -104,67 +98,12 @@ TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) } } -TEST_F(CellGeometryTest, RejectsSkewAndDegenerateCellsWithoutChangingResult) +TEST_F(CellGeometryTest, RejectsSkewCell) { - const ModuleBase::Vector3 original_origin = cell.origin; const ModuleBase::Matrix3 skew(1.0, 0.0, 0.0, 0.1, 1.0, 0.0, 0.0, 0.0, 1.0); - EXPECT_FALSE(unitcell::make_orthogonal_cell(skew, 10.0, 1.0e-10, cell, error)); - EXPECT_FALSE(error.empty()); - expect_vector(cell.origin, original_origin); - const ModuleBase::Matrix3 degenerate(1.0, 0.0, 0.0, - 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0); - EXPECT_FALSE(unitcell::make_orthogonal_cell(degenerate, 10.0, 1.0e-10, cell, error)); - expect_vector(cell.origin, original_origin); -} - -TEST_F(CellGeometryTest, RejectsInvalidScaleToleranceAndNonfiniteLattice) -{ - const ModuleBase::Matrix3 identity(1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0); - const double infinity = std::numeric_limits::infinity(); - EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, 0.0, 1.0e-10, cell, error)); - EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, infinity, 1.0e-10, cell, error)); - EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, 10.0, 0.0, cell, error)); - EXPECT_FALSE(unitcell::make_orthogonal_cell(identity, 10.0, infinity, cell, error)); - const ModuleBase::Matrix3 nonfinite(infinity, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0); - EXPECT_FALSE(unitcell::make_orthogonal_cell(nonfinite, 10.0, 1.0e-10, cell, error)); - ASSERT_TRUE(unitcell::make_orthogonal_cell(identity, 10.0, 1.0e-10, cell, error)); - EXPECT_TRUE(error.empty()); -} - -TEST_F(CellGeometryTest, RejectsInvalidCenterInputsWithoutChangingResult) -{ - std::vector> positions = { - ModuleBase::Vector3(1.0, 2.0, 3.0)}; - const std::vector empty_weights; - const std::vector zero_weights = {0.0}; - const std::vector weights = {1.0}; - const ModuleBase::Vector3 original(4.0, 5.0, 6.0); - ModuleBase::Vector3 center = original; - EXPECT_FALSE(unitcell::weighted_center(positions, empty_weights, cell, center, error)); - EXPECT_FALSE(unitcell::weighted_center(positions, zero_weights, cell, center, error)); - positions[0].x = std::numeric_limits::quiet_NaN(); - EXPECT_FALSE(unitcell::weighted_center(positions, weights, cell, center, error)); - expect_vector(center, original); - EXPECT_FALSE(error.empty()); -} - -TEST_F(CellGeometryTest, RejectsOverflowingCenterAccumulation) -{ - const std::vector> positions = { - ModuleBase::Vector3(1.0, 1.0, 1.0), - ModuleBase::Vector3(2.0, 2.0, 2.0)}; - const double maximum = std::numeric_limits::max(); - const std::vector weights = {maximum, maximum}; - ModuleBase::Vector3 center; - EXPECT_FALSE(unitcell::weighted_center(positions, weights, cell, center, error)); - EXPECT_FALSE(error.empty()); + EXPECT_FALSE(unitcell::make_orthogonal_cell(skew, 10.0, 1.0e-10, cell)); } TEST(SlabGeometry, SupportsSkewPeriodicPlaneAndRotatedNormal) @@ -173,8 +112,7 @@ TEST(SlabGeometry, SupportsSkewPeriodicPlaneAndRotatedNormal) 0.0, 1.0, 3.0, 8.0, 0.0, 0.0); unitcell::SlabCell cell; - std::string error; - ASSERT_TRUE(unitcell::make_slab_cell(lattice, 1.0, 2, 1.0e-6, cell, error)); + ASSERT_TRUE(unitcell::make_slab_cell(lattice, 1.0, 2, 1.0e-6, cell)); EXPECT_DOUBLE_EQ(cell.length, 8.0); EXPECT_DOUBLE_EQ(cell.area, 6.0); EXPECT_DOUBLE_EQ(cell.normal.x, 1.0); @@ -184,22 +122,20 @@ TEST(SlabGeometry, SupportsSkewPeriodicPlaneAndRotatedNormal) ModuleBase::Vector3(7.8, 0.0, 0.0), ModuleBase::Vector3(0.2, 40.0, 50.0)}; const std::vector weights = {1.0, 3.0}; - double center = 5.0; - ASSERT_TRUE(unitcell::weighted_center(positions, weights, cell, center, error)); + const double center = unitcell::weighted_center(positions, weights, cell); EXPECT_NEAR(center, 0.1, 1.0e-14); } -TEST(SlabGeometry, SelectsAllAxesAndRejectsTiltWithoutChangingResult) +TEST(SlabGeometry, SelectsAllAxesAndRejectsTilt) { const ModuleBase::Matrix3 lattice(2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 8.0); const double lengths[3] = {2.0, 3.0, 8.0}; unitcell::SlabCell cell; - std::string error; for (int axis = 0; axis < 3; ++axis) { - ASSERT_TRUE(unitcell::make_slab_cell(lattice, 1.0, axis, 1.0e-6, cell, error)); + ASSERT_TRUE(unitcell::make_slab_cell(lattice, 1.0, axis, 1.0e-6, cell)); EXPECT_DOUBLE_EQ(cell.length, lengths[axis]); const double expected_area = 48.0 / lengths[axis]; EXPECT_DOUBLE_EQ(cell.area, expected_area); @@ -207,9 +143,5 @@ TEST(SlabGeometry, SelectsAllAxesAndRejectsTiltWithoutChangingResult) const ModuleBase::Matrix3 tilted(2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.1, 0.0, 8.0); - EXPECT_FALSE(unitcell::make_slab_cell(tilted, 1.0, 2, 1.0e-6, cell, error)); - EXPECT_DOUBLE_EQ(cell.length, 8.0); - EXPECT_FALSE(unitcell::make_slab_cell(lattice, 1.0, 3, 1.0e-6, cell, error)); - EXPECT_FALSE(unitcell::make_slab_cell(lattice, 0.0, 2, 1.0e-6, cell, error)); - EXPECT_FALSE(error.empty()); + EXPECT_FALSE(unitcell::make_slab_cell(tilted, 1.0, 2, 1.0e-6, cell)); } diff --git a/source/source_estate/charge_moments.cpp b/source/source_estate/charge_moments.cpp index 054f742fda2..ed905fa7d5c 100644 --- a/source/source_estate/charge_moments.cpp +++ b/source/source_estate/charge_moments.cpp @@ -1,48 +1,23 @@ #include "charge_moments.h" -#include - namespace elecstate { -bool charge_moments(const double* charges, - const ModuleBase::Vector3* relative_positions, - const int count, - const double weight, - ChargeMoments& moments, - std::string& error) +ChargeMoments charge_moments(const double* charges, + const ModuleBase::Vector3* relative_positions, + const int count, + const double weight) { - error.clear(); - if (count < 0 || (count > 0 && (charges == nullptr || relative_positions == nullptr)) - || !std::isfinite(weight) || weight <= 0.0) - { - error = "charge moments require valid sample storage and a positive finite weight"; - return false; - } - ChargeMoments candidate; + ChargeMoments moments; for (int index = 0; index < count; ++index) { const double charge = charges[index] * weight; const ModuleBase::Vector3& relative = relative_positions[index]; - if (!std::isfinite(charge) || !std::isfinite(relative.x) - || !std::isfinite(relative.y) || !std::isfinite(relative.z)) - { - error = "charge moments require finite charges and relative positions"; - return false; - } - candidate.charge += charge; - candidate.dipole += relative * charge; - candidate.second_moment += charge * relative.norm2(); - } - if (!std::isfinite(candidate.charge) || !std::isfinite(candidate.dipole.x) - || !std::isfinite(candidate.dipole.y) || !std::isfinite(candidate.dipole.z) - || !std::isfinite(candidate.second_moment)) - { - error = "charge moment accumulation must be finite"; - return false; + moments.charge += charge; + moments.dipole += relative * charge; + moments.second_moment += charge * relative.norm2(); } - moments = candidate; - return true; + return moments; } ChargeMoments add_charge_moments(const ChargeMoments& left, const ChargeMoments& right) diff --git a/source/source_estate/charge_moments.h b/source/source_estate/charge_moments.h index f3fac2e9251..7dcf20cf319 100644 --- a/source/source_estate/charge_moments.h +++ b/source/source_estate/charge_moments.h @@ -3,8 +3,6 @@ #include "source_base/vector3.h" -#include - namespace elecstate { @@ -18,13 +16,11 @@ struct ChargeMoments /// Local moments only; no MPI or cell/model dependencies. Use weight=1 for /// point charges and the volume element for a signed grid density. -/// Zero samples are permitted. On failure, leave moments unchanged and set error. -bool charge_moments(const double* charges, - const ModuleBase::Vector3* relative_positions, - int count, - double weight, - ChargeMoments& moments, - std::string& error); +/// Zero samples are permitted. +ChargeMoments charge_moments(const double* charges, + const ModuleBase::Vector3* relative_positions, + int count, + double weight); ChargeMoments add_charge_moments(const ChargeMoments& left, const ChargeMoments& right); diff --git a/source/source_estate/module_pot/pot_pcc.cpp b/source/source_estate/module_pot/pot_pcc.cpp index c5e9cf29c31..fc8be2a1292 100644 --- a/source/source_estate/module_pot/pot_pcc.cpp +++ b/source/source_estate/module_pot/pot_pcc.cpp @@ -11,23 +11,6 @@ namespace { -void require_valid_on_pool(const bool valid, const std::string& error, const std::string& function) -{ - double invalid = valid ? 0.0 : 1.0; -#ifdef __MPI - Parallel_Reduce::reduce_pool(invalid); -#endif - if (invalid != 0) - { - std::string message = error; - if (message.empty()) - { - message = "invalid PCC input or result on another process in this pool"; - } - ModuleBase::WARNING_QUIT(function, message); - } -} - bool fractional_translation(const ModuleBase::Matrix3& rotation, const ModuleBase::Vector3& translation) { @@ -137,10 +120,6 @@ PotPcc::PotPcc(const ModulePW::PW_Basis* basis, const Dimension dimension, const { this->rho_basis_ = basis; this->dynamic_mode = true; - if (open_axis < 0 || open_axis > 2) - { - ModuleBase::WARNING_QUIT("PotPcc", "pcc_2d_axis must be 0, 1 or 2"); - } } void PotPcc::validate_kpoints(const std::vector>& points, @@ -148,23 +127,14 @@ void PotPcc::validate_kpoints(const std::vector>& po const int open_axis) { double maximum = 0.0; - if (open_axis < 0 || open_axis > 2 || count < 0 || count > static_cast(points.size())) + for (int ik = 0; ik < count; ++ik) { - maximum = 1.0; - } - else - { - for (int ik = 0; ik < count; ++ik) - { - const double components[3] = {points[ik].x, points[ik].y, points[ik].z}; - const double coordinate = components[open_axis]; - const double absolute = std::abs(coordinate); - if (!std::isfinite(coordinate)) { maximum = 1.0; } - else { maximum = std::max(maximum, absolute); } - } + const double components[3] = {points[ik].x, points[ik].y, points[ik].z}; + const double absolute = std::abs(components[open_axis]); + maximum = std::max(maximum, absolute); } #ifdef __MPI - // All pools must reject a forbidden open-direction k point together. + // Each pool holds its own k points; all pools must stop together. Parallel_Reduce::reduce_max(maximum); #endif if (maximum > 1.0e-12) @@ -207,30 +177,39 @@ ModuleBase::Vector3 PotPcc::correction_force(const double charge, void PotPcc::prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments) { ModuleBase::timer::start("PotPcc", "prepare_ions"); - std::string error; + // The cell, symmetry and atoms are replicated, so every rank stops together. if (dimension_ == Dimension::molecule) { - const bool geometry_valid = unitcell::make_orthogonal_cell(cell.latvec, cell.lat0, 1.0e-10, geometry_, error); - require_valid_on_pool(geometry_valid, error, "PotPcc::prepare_ions"); - const bool parameters_valid = make_pcc_0d_parameters(geometry_, 1.0e-10, parameters_, error); - require_valid_on_pool(parameters_valid, error, "PotPcc::prepare_ions"); - const bool symmetry_valid = primitive_symmetry(cell.symm); - require_valid_on_pool(symmetry_valid, "PCC 0D requires a primitive cell without fractional-translation symmetry", "PotPcc::prepare_ions"); + const bool orthogonal = unitcell::make_orthogonal_cell(cell.latvec, cell.lat0, 1.0e-10, geometry_); + const bool cubic = orthogonal && make_pcc_0d_parameters(geometry_, 1.0e-10, parameters_); + if (!cubic) + { + ModuleBase::WARNING_QUIT("PotPcc::prepare_ions", "PCC 0D requires an equal-edge cubic cell"); + } + if (!primitive_symmetry(cell.symm)) + { + ModuleBase::WARNING_QUIT("PotPcc::prepare_ions", + "PCC 0D requires a primitive cell without fractional-translation symmetry"); + } } else { - const bool geometry_valid = unitcell::make_slab_cell(cell.latvec, cell.lat0, open_axis_, 1.0e-6, slab_, error); - require_valid_on_pool(geometry_valid, error, "PotPcc::prepare_ions"); - const bool parameters_valid = make_pcc_2d_parameters(slab_, slab_parameters_, error); - require_valid_on_pool(parameters_valid, error, "PotPcc::prepare_ions"); - const bool symmetry_valid = slab_symmetry_valid(cell.symm, open_axis_); - require_valid_on_pool(symmetry_valid, "PCC 2D symmetry must preserve the open axis without fractional translations along it", "PotPcc::prepare_ions"); + const bool perpendicular = unitcell::make_slab_cell(cell.latvec, cell.lat0, open_axis_, 1.0e-6, slab_); + if (!perpendicular) + { + ModuleBase::WARNING_QUIT("PotPcc::prepare_ions", + "PCC 2D requires the open lattice vector to be perpendicular to the periodic plane"); + } + slab_parameters_ = make_pcc_2d_parameters(slab_); + if (!slab_symmetry_valid(cell.symm, open_axis_)) + { + ModuleBase::WARNING_QUIT("PotPcc::prepare_ions", + "PCC 2D symmetry must preserve the open axis without fractional translations along it"); + } } const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); const int atom_count = static_cast(atoms.size()); - const bool atom_count_valid = atom_count == cell.nat; - require_valid_on_pool(atom_count_valid, "PCC atom count does not match UnitCell", "PotPcc::prepare_ions"); std::vector masses(atom_count); ionic_positions_.resize(atom_count); ionic_charges_.resize(atom_count); @@ -242,17 +221,11 @@ void PotPcc::prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments) } if (dimension_ == Dimension::molecule) { - ModuleBase::Vector3 center; - const bool center_valid = unitcell::weighted_center(ionic_positions_, masses, geometry_, center, error); - require_valid_on_pool(center_valid, error, "PotPcc::prepare_ions"); - geometry_.origin = center; + geometry_.origin = unitcell::weighted_center(ionic_positions_, masses, geometry_); } else { - double center = 0.0; - const bool center_valid = unitcell::weighted_center(ionic_positions_, masses, slab_, center, error); - require_valid_on_pool(center_valid, error, "PotPcc::prepare_ions"); - slab_.origin = center; + slab_.origin = unitcell::weighted_center(ionic_positions_, masses, slab_); } std::vector> relative_ions(atom_count); for (int atom = 0; atom < atom_count; ++atom) @@ -261,13 +234,7 @@ void PotPcc::prepare_ions(const UnitCell& cell, ChargeMoments& ionic_moments) } const double* ionic_charge_data = ionic_charges_.data(); const ModuleBase::Vector3* ionic_position_data = relative_ions.data(); - const bool ions_valid = charge_moments(ionic_charge_data, - ionic_position_data, - atom_count, - 1.0, - ionic_moments, - error); - require_valid_on_pool(ions_valid, error, "PotPcc::prepare_ions"); + ionic_moments = charge_moments(ionic_charge_data, ionic_position_data, atom_count, 1.0); ModuleBase::timer::end("PotPcc", "prepare_ions"); } @@ -279,10 +246,8 @@ ChargeMoments PotPcc::collect_electrons( { ModuleBase::timer::start("PotPcc", "collect_electrons"); const ModulePW::PW_Basis& basis = *this->rho_basis_; - std::string error; std::vector electronic_charge(basis.nrxx, 0.0); - const bool positions_valid = ModulePW::grid_positions(basis, cell.latvec, cell.lat0, positions, error); - require_valid_on_pool(positions_valid, error, "PotPcc::collect_electrons"); + ModulePW::grid_positions(basis, cell.latvec, cell.lat0, positions); for (int ir = 0; ir < basis.nrxx; ++ir) { for (int spin = 0; spin < charge.nspin; ++spin) @@ -291,17 +256,10 @@ ChargeMoments PotPcc::collect_electrons( } positions[ir] = this->relative_position(positions[ir]); } - ChargeMoments electronic_moments; const double volume_element = cell.omega / basis.nxyz; const double* electronic_data = electronic_charge.data(); const ModuleBase::Vector3* position_data = positions.data(); - const bool density_valid = charge_moments(electronic_data, - position_data, - basis.nrxx, - volume_element, - electronic_moments, - error); - require_valid_on_pool(density_valid, error, "PotPcc::collect_electrons"); + ChargeMoments electronic_moments = charge_moments(electronic_data, position_data, basis.nrxx, volume_element); double reduced[5] = {electronic_moments.charge, electronic_moments.dipole.x, electronic_moments.dipole.y, electronic_moments.dipole.z, electronic_moments.second_moment}; @@ -318,25 +276,7 @@ ChargeMoments PotPcc::collect_electrons( void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) { ModuleBase::timer::start("PotPcc", "cal_v_eff"); - result_valid_ = false; - const bool storage_valid = charge != nullptr && cell != nullptr && this->rho_basis_ != nullptr; - require_valid_on_pool(storage_valid, "PCC requires charge, cell and PW basis storage", "PotPcc::cal_v_eff"); const ModulePW::PW_Basis& basis = *this->rho_basis_; - const bool grid_valid = basis.nx > 0 && basis.ny > 0 && basis.nz > 0 && basis.nxyz > 0 - && basis.nplane >= 0 && basis.nrxx == basis.nx * basis.ny * basis.nplane - && (charge->nspin == 1 || charge->nspin == 2) - && potential.nr == charge->nspin && potential.nc == basis.nrxx - && std::isfinite(cell->omega) && cell->omega > 0.0; - require_valid_on_pool(grid_valid, "PCC requires an initialized grid and nspin=1/2 potential", "PotPcc::cal_v_eff"); - bool density_storage_valid = charge->rho != nullptr; - if (density_storage_valid && basis.nrxx > 0) - { - for (int spin = 0; spin < charge->nspin; ++spin) - { - density_storage_valid = density_storage_valid && charge->rho[spin] != nullptr; - } - } - require_valid_on_pool(density_storage_valid, "PCC charge density is not available", "PotPcc::cal_v_eff"); ChargeMoments ionic_moments; this->prepare_ions(*cell, ionic_moments); std::vector> positions; @@ -345,14 +285,11 @@ void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::m moments_ = add_charge_moments(ionic_moments, electronic_moments); energy_rydberg_ = 2.0 * this->correction_energy(); electron_potential_.resize(basis.nrxx); - bool result_finite = std::isfinite(energy_rydberg_); for (int ir = 0; ir < basis.nrxx; ++ir) { const double positive_potential = this->correction_potential(positions[ir]); electron_potential_[ir] = -2.0 * positive_potential; - result_finite = result_finite && std::isfinite(electron_potential_[ir]); } - require_valid_on_pool(result_finite, "PCC energy or potential is nonfinite", "PotPcc::cal_v_eff"); for (int spin = 0; spin < charge->nspin; ++spin) { for (int ir = 0; ir < basis.nrxx; ++ir) @@ -360,47 +297,24 @@ void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::m potential(spin, ir) += electron_potential_[ir]; } } - result_valid_ = true; ModuleBase::timer::end("PotPcc", "cal_v_eff"); } double PotPcc::get_energy() const { - if (!result_valid_) - { - ModuleBase::WARNING_QUIT("PotPcc::get_energy", "PCC energy requires an updated potential"); - } return energy_rydberg_; } const std::vector& PotPcc::electron_potential() const { - if (!result_valid_) - { - ModuleBase::WARNING_QUIT("PotPcc::electron_potential", "PCC output requires an updated potential"); - } return electron_potential_; } void PotPcc::add_force(const UnitCell& cell, ModuleBase::matrix& force) const { ModuleBase::timer::start("PotPcc", "add_force"); - if (!result_valid_ || force.nr != cell.nat || force.nc != 3) - { - ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC force requires a current potential and nat-by-3 output"); - } - const std::vector atoms = unitcell::get_atom_data(cell.atoms, cell.ntype, cell.lat0); - if (atoms.size() != ionic_positions_.size() || atoms.size() != static_cast(cell.nat)) - { - ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC atom count changed since the potential update"); - } for (int atom = 0; atom < cell.nat; ++atom) { - const ModuleBase::Vector3 difference = atoms[atom].position - ionic_positions_[atom]; - if (difference.norm2() > 1.0e-24 || atoms[atom].valence_charge != ionic_charges_[atom]) - { - ModuleBase::WARNING_QUIT("PotPcc::add_force", "PCC atoms changed since the potential update"); - } const ModuleBase::Vector3 relative = this->relative_position(ionic_positions_[atom]); const ModuleBase::Vector3 correction = this->correction_force(ionic_charges_[atom], relative); force(atom, 0) += 2.0 * correction.x; diff --git a/source/source_estate/module_pot/pot_pcc.h b/source/source_estate/module_pot/pot_pcc.h index 1774940510b..a69a14d1587 100644 --- a/source/source_estate/module_pot/pot_pcc.h +++ b/source/source_estate/module_pot/pot_pcc.h @@ -81,7 +81,6 @@ class PotPcc : public PotBase std::vector ionic_charges_; std::vector electron_potential_; double energy_rydberg_ = 0.0; - bool result_valid_ = false; }; } // namespace elecstate diff --git a/source/source_estate/pcc_0d.cpp b/source/source_estate/pcc_0d.cpp index 84a4c72039b..132ac499a43 100644 --- a/source/source_estate/pcc_0d.cpp +++ b/source/source_estate/pcc_0d.cpp @@ -10,29 +10,18 @@ namespace elecstate bool make_pcc_0d_parameters(const unitcell::OrthogonalCell& cell, const double relative_tolerance, - Pcc0dParameters& parameters, - std::string& error) + Pcc0dParameters& parameters) { - error.clear(); - if (!std::isfinite(relative_tolerance) || relative_tolerance <= 0.0) - { - error = "PCC 0D requires a positive finite geometry tolerance"; - return false; - } const double length = cell.lengths[0]; - for (int axis = 0; axis < 3; ++axis) + parameters.length = length; + for (int axis = 1; axis < 3; ++axis) { - const double edge = cell.lengths[axis]; - const double difference = edge - length; - if (!std::isfinite(edge) || edge <= 0.0 || std::abs(difference) > relative_tolerance * length) + const double difference = cell.lengths[axis] - length; + if (std::abs(difference) > relative_tolerance * length) { - error = "PCC 0D requires an equal-edge cubic cell"; return false; } } - Pcc0dParameters candidate; - candidate.length = length; - parameters = candidate; return true; } diff --git a/source/source_estate/pcc_0d.h b/source/source_estate/pcc_0d.h index 8d0bbd994d2..8931c6ac73e 100644 --- a/source/source_estate/pcc_0d.h +++ b/source/source_estate/pcc_0d.h @@ -17,12 +17,11 @@ struct Pcc0dParameters double madelung = 2.837297479480619; }; -/// Check the equal-edge constraint after constructing an OrthogonalCell. -/// Leave parameters unchanged on failure. No cell geometry is stored here. +/// Return false unless the OrthogonalCell has equal edges within +/// relative_tolerance. No cell geometry is stored here. bool make_pcc_0d_parameters(const unitcell::OrthogonalCell& cell, double relative_tolerance, - Pcc0dParameters& parameters, - std::string& error); + Pcc0dParameters& parameters); /// Numerical kernels require validated parameters and moments about a common /// origin. Coordinates are relative to that origin. Energies/potentials are in diff --git a/source/source_estate/pcc_2d.cpp b/source/source_estate/pcc_2d.cpp index 33d40f47f2b..63fbd9289bd 100644 --- a/source/source_estate/pcc_2d.cpp +++ b/source/source_estate/pcc_2d.cpp @@ -7,19 +7,12 @@ namespace elecstate { -bool make_pcc_2d_parameters(const unitcell::SlabCell& cell, - Pcc2dParameters& parameters, - std::string& error) +Pcc2dParameters make_pcc_2d_parameters(const unitcell::SlabCell& cell) { - error.clear(); - if (!std::isfinite(cell.area) || cell.area <= 0.0 || !std::isfinite(cell.length) || cell.length <= 0.0) - { - error = "PCC 2D requires positive finite periodic area and normal length"; - return false; - } + Pcc2dParameters parameters; parameters.area = cell.area; parameters.length = cell.length; - return true; + return parameters; } double pcc_2d_potential(const ChargeMoments& moments, diff --git a/source/source_estate/pcc_2d.h b/source/source_estate/pcc_2d.h index 4b9e88c9be5..1c1afb12b98 100644 --- a/source/source_estate/pcc_2d.h +++ b/source/source_estate/pcc_2d.h @@ -16,9 +16,7 @@ struct Pcc2dParameters double length = 0.0; ///< Normal period in Bohr }; -bool make_pcc_2d_parameters(const unitcell::SlabCell& cell, - Pcc2dParameters& parameters, - std::string& error); +Pcc2dParameters make_pcc_2d_parameters(const unitcell::SlabCell& cell); /// Hartree kernels. Parameters must be validated; moments use normal-projected /// positions r = normal * u, so second_moment is the normal second moment. diff --git a/source/source_estate/test/test_charge_moments.cpp b/source/source_estate/test/test_charge_moments.cpp index 766e411045a..6c4bb5ac8f0 100644 --- a/source/source_estate/test/test_charge_moments.cpp +++ b/source/source_estate/test/test_charge_moments.cpp @@ -2,17 +2,13 @@ #include -#include - TEST(ChargeMoments, IntegratesSignedDensityWithVolumeElement) { const double charges[2] = {2.0, -1.0}; const ModuleBase::Vector3 positions[2] = { ModuleBase::Vector3(1.0, 0.0, 0.0), ModuleBase::Vector3(0.0, 2.0, 0.0)}; - elecstate::ChargeMoments moments; - std::string error; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 0.5, moments, error)); + const elecstate::ChargeMoments moments = elecstate::charge_moments(charges, positions, 2, 0.5); EXPECT_DOUBLE_EQ(moments.charge, 0.5); EXPECT_DOUBLE_EQ(moments.dipole.x, 1.0); EXPECT_DOUBLE_EQ(moments.dipole.y, -1.0); @@ -36,39 +32,7 @@ TEST(ChargeMoments, AddsIndependentContributionsAndAcceptsEmptyLocalGrid) EXPECT_DOUBLE_EQ(total.dipole.y, -2.0); EXPECT_DOUBLE_EQ(total.second_moment, 1.0); - std::string error; - ASSERT_TRUE(elecstate::charge_moments(nullptr, nullptr, 0, 1.0, electrons, error)); + electrons = elecstate::charge_moments(nullptr, nullptr, 0, 1.0); EXPECT_DOUBLE_EQ(electrons.charge, 0.0); EXPECT_DOUBLE_EQ(electrons.second_moment, 0.0); } - -TEST(ChargeMoments, RejectsInvalidStorageAndNonfiniteValuesWithoutChangingResult) -{ - double charge = 1.0; - ModuleBase::Vector3 position(1.0, 2.0, 3.0); - elecstate::ChargeMoments moments; - moments.charge = 7.0; - std::string error; - EXPECT_FALSE(elecstate::charge_moments(nullptr, &position, 1, 1.0, moments, error)); - EXPECT_FALSE(elecstate::charge_moments(&charge, nullptr, 1, 1.0, moments, error)); - EXPECT_FALSE(elecstate::charge_moments(&charge, &position, -1, 1.0, moments, error)); - EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 0.0, moments, error)); - charge = std::numeric_limits::infinity(); - EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 1.0, moments, error)); - charge = 1.0; - position.x = std::numeric_limits::quiet_NaN(); - EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 1.0, moments, error)); - EXPECT_DOUBLE_EQ(moments.charge, 7.0); - EXPECT_FALSE(error.empty()); -} - -TEST(ChargeMoments, RejectsOverflowingMoments) -{ - const double charge = 1.0; - const double maximum = std::numeric_limits::max(); - const ModuleBase::Vector3 position(maximum, 0.0, 0.0); - elecstate::ChargeMoments moments; - std::string error; - EXPECT_FALSE(elecstate::charge_moments(&charge, &position, 1, 1.0, moments, error)); - EXPECT_DOUBLE_EQ(moments.charge, 0.0); -} diff --git a/source/source_estate/test/test_pcc_0d.cpp b/source/source_estate/test/test_pcc_0d.cpp index 9135e532579..cd65bfcb653 100644 --- a/source/source_estate/test/test_pcc_0d.cpp +++ b/source/source_estate/test/test_pcc_0d.cpp @@ -57,8 +57,8 @@ class Pcc0dTest : public testing::Test const ModuleBase::Matrix3 lattice(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); - ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell, error)); - ASSERT_TRUE(elecstate::make_pcc_0d_parameters(cell, 1.0e-10, parameters, error)); + ASSERT_TRUE(unitcell::make_orthogonal_cell(lattice, 10.0, 1.0e-10, cell)); + ASSERT_TRUE(elecstate::make_pcc_0d_parameters(cell, 1.0e-10, parameters)); moments.charge = 1.2; moments.dipole = ModuleBase::Vector3(0.1, -0.2, 0.3); moments.second_moment = 0.8; @@ -67,7 +67,6 @@ class Pcc0dTest : public testing::Test unitcell::OrthogonalCell cell; elecstate::Pcc0dParameters parameters; elecstate::ChargeMoments moments; - std::string error; }; } // namespace @@ -77,11 +76,8 @@ TEST_F(Pcc0dTest, SeparatesCubicRestrictionFromOrthogonalGeometry) const ModuleBase::Matrix3 rectangular(1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0); - ASSERT_TRUE(unitcell::make_orthogonal_cell(rectangular, 10.0, 1.0e-10, cell, error)); - EXPECT_FALSE(elecstate::make_pcc_0d_parameters(cell, 1.0e-10, parameters, error)); - EXPECT_DOUBLE_EQ(parameters.length, 10.0); - EXPECT_FALSE(error.empty()); - EXPECT_FALSE(elecstate::make_pcc_0d_parameters(cell, 0.0, parameters, error)); + ASSERT_TRUE(unitcell::make_orthogonal_cell(rectangular, 10.0, 1.0e-10, cell)); + EXPECT_FALSE(elecstate::make_pcc_0d_parameters(cell, 1.0e-10, parameters)); } TEST_F(Pcc0dTest, MatchesAnalyticalMonopoleAndNeutralDipoleEnergy) @@ -143,7 +139,7 @@ TEST_F(Pcc0dTest, IonicForceMatchesTotalEnergyFiniteDifferences) ModuleBase::Vector3 positions[2] = { ModuleBase::Vector3(0.2, -0.3, 0.4), ModuleBase::Vector3(-0.5, 0.6, -0.7)}; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const ModuleBase::Vector3 force = elecstate::pcc_0d_force(moments, charges[0], positions[0], parameters); const double expected[3] = {force.x, force.y, force.z}; const double step = 1.0e-5; @@ -152,10 +148,10 @@ TEST_F(Pcc0dTest, IonicForceMatchesTotalEnergyFiniteDifferences) double* component[3] = {&positions[0].x, &positions[0].y, &positions[0].z}; const double original = *component[axis]; *component[axis] = original + step; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double plus_energy = elecstate::pcc_0d_energy(moments, parameters); *component[axis] = original - step; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double minus_energy = elecstate::pcc_0d_energy(moments, parameters); *component[axis] = original; const double numerical_force = -(plus_energy - minus_energy) / (2.0 * step); @@ -189,12 +185,12 @@ TEST_F(Pcc0dTest, EnergyIsIndependentOfMultipoleOrigin) ModuleBase::Vector3 positions[2] = { ModuleBase::Vector3(0.3, -0.2, 0.1), ModuleBase::Vector3(-0.5, 0.6, -0.7)}; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double original_energy = elecstate::pcc_0d_energy(moments, parameters); const ModuleBase::Vector3 shift(0.4, -0.3, 0.2); positions[0] -= shift; positions[1] -= shift; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double shifted_energy = elecstate::pcc_0d_energy(moments, parameters); EXPECT_NEAR(original_energy, shifted_energy, 1.0e-14); } diff --git a/source/source_estate/test/test_pcc_2d.cpp b/source/source_estate/test/test_pcc_2d.cpp index 5487a4842c4..1314bb7440b 100644 --- a/source/source_estate/test/test_pcc_2d.cpp +++ b/source/source_estate/test/test_pcc_2d.cpp @@ -42,9 +42,7 @@ TEST(Pcc2d, MatchesChargedSheetGaugeAndNeutralDipoleEnergy) unitcell::SlabCell cell; cell.area = 6.0; cell.length = 8.0; - elecstate::Pcc2dParameters parameters; - std::string error; - ASSERT_TRUE(elecstate::make_pcc_2d_parameters(cell, parameters, error)); + const elecstate::Pcc2dParameters parameters = elecstate::make_pcc_2d_parameters(cell); elecstate::ChargeMoments moments; moments.charge = 2.0; const double charged = elecstate::pcc_2d_energy(moments, parameters); @@ -55,9 +53,6 @@ TEST(Pcc2d, MatchesChargedSheetGaugeAndNeutralDipoleEnergy) const double neutral = elecstate::pcc_2d_energy(moments, parameters); const double expected_neutral = 2.0 * ModuleBase::PI * 4.0 / 48.0; EXPECT_NEAR(neutral, expected_neutral, 1.0e-14); - cell.area = 0.0; - EXPECT_FALSE(elecstate::make_pcc_2d_parameters(cell, parameters, error)); - EXPECT_DOUBLE_EQ(parameters.area, 6.0); } TEST(Pcc2d, NormalForceMatchesEnergyDerivativeAndHasNoTangentialComponent) @@ -71,18 +66,17 @@ TEST(Pcc2d, NormalForceMatchesEnergyDerivativeAndHasNoTangentialComponent) ModuleBase::Vector3(0.0, 0.2, 0.0), ModuleBase::Vector3(0.0, -0.5, 0.0)}; elecstate::ChargeMoments moments; - std::string error; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double coordinate = positions[0].y; const ModuleBase::Vector3 force = elecstate::pcc_2d_force(moments, charges[0], coordinate, normal, parameters); EXPECT_DOUBLE_EQ(force.x, 0.0); EXPECT_DOUBLE_EQ(force.z, 0.0); const double step = 1.0e-5; positions[0].y = coordinate + step; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double plus = elecstate::pcc_2d_energy(moments, parameters); positions[0].y = coordinate - step; - ASSERT_TRUE(elecstate::charge_moments(charges, positions, 2, 1.0, moments, error)); + moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double minus = elecstate::pcc_2d_energy(moments, parameters); const double numerical_force = -(plus - minus) / (2.0 * step); EXPECT_NEAR(force.y, numerical_force, 1.0e-10); diff --git a/source/source_estate/test/test_pot_pcc.cpp b/source/source_estate/test/test_pot_pcc.cpp index a2a10f74978..5ba6dba5557 100644 --- a/source/source_estate/test/test_pot_pcc.cpp +++ b/source/source_estate/test/test_pot_pcc.cpp @@ -135,22 +135,6 @@ TEST_F(PotPccTest, ForceMatchesFixedDensityEnergyDerivativeAfterMovingIons) EXPECT_NEAR(force(0, 0), numerical_force, 1.0e-10); } -TEST_F(PotPccTest, RejectsEnergyBeforeDensityUpdate) -{ - elecstate::PotPcc correction(&basis); - EXPECT_EXIT(correction.get_energy(), testing::ExitedWithCode(1), ""); -} - -TEST_F(PotPccTest, RejectsForceFromAnOlderGeometry) -{ - elecstate::PotPcc correction(&basis); - ModuleBase::matrix potential(1, 8); - correction.cal_v_eff(&charge, &cell, potential); - atom.tau[0].x += 0.01; - ModuleBase::matrix force(1, 3); - EXPECT_EXIT(correction.add_force(cell, force), testing::ExitedWithCode(1), ""); -} - TEST_F(PotPccTest, SlabAxisControlsPotentialEnergyForceAndIgnoresInPlaneCoordinates) { const double expected_energy = 4.0 * ModuleBase::PI * 0.25 / 1000.0; @@ -191,13 +175,14 @@ TEST_F(PotPccTest, SlabRejectsOpenDirectionKpointsAndSymmetryButAllowsInPlaneTra EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); } -TEST_F(PotPccTest, RejectsMissingDensityThroughStandardErrorHandler) +TEST_F(PotPccTest, MoleculeRejectsNonCubicAndSlabRejectsTiltedCells) { - elecstate::PotPcc correction(&basis); ModuleBase::matrix potential(1, 8); - charge.rho = nullptr; - EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); - charge.rho = channels; - channels[0] = nullptr; - EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); + cell.latvec.e22 = 2.0; + elecstate::PotPcc molecule(&basis); + EXPECT_EXIT(molecule.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); + cell.latvec.e22 = 1.0; + cell.latvec.e31 = 0.1; + elecstate::PotPcc slab(&basis, elecstate::PotPcc::Dimension::slab, 2); + EXPECT_EXIT(slab.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); } diff --git a/source/source_estate/write_elecstat_pot.cpp b/source/source_estate/write_elecstat_pot.cpp index f3076de2dbf..a46e12bd512 100644 --- a/source/source_estate/write_elecstat_pot.cpp +++ b/source/source_estate/write_elecstat_pot.cpp @@ -29,10 +29,6 @@ void write_elecstat_pot( ModuleBase::timer::start("ModuleIO", "write_elecstat_pot"); std::vector v_elecstat(rho_basis->nrxx, 0.0); - if (pcc_potential != nullptr && pcc_potential->size() != static_cast(rho_basis->nrxx)) - { - ModuleBase::WARNING_QUIT("write_elecstat_pot", "PCC potential does not match the output grid"); - } const int nspin = PARAM.inp.nspin; const int efield = PARAM.inp.efield_flag; From 3e860b1ddf8cd9d417ab559ee77c31d057b96ba1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:58:37 +0000 Subject: [PATCH 29/41] refactor(sccs): drop unreachable checks in the basic SCCS layer - The Gaussian ionic density is built in reciprocal space, so its grid integral equals the valence charge to rounding; drop the normalization check that could never fire. - A non-finite residual already fails convergence and stops at the next sqrt-CG step; drop the separate residual, net-charge and functional energy finiteness checks. - validate_sccs_input reports which SCCS parameter is out of range and no longer checks finiteness, matching other INPUT items. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BKqk2SNWXoZn2PRRGRzGca --- source/source_estate/module_pot/pot_sccs.cpp | 15 ++----------- .../module_sccs/sccs_functional.cpp | 7 ------- .../module_sccs/sccs_response.cpp | 7 ++----- .../module_parameter/read_inp_sccs.cpp | 21 +++++++------------ 4 files changed, 12 insertions(+), 38 deletions(-) diff --git a/source/source_estate/module_pot/pot_sccs.cpp b/source/source_estate/module_pot/pot_sccs.cpp index 7eeff309d31..2168383e0d3 100644 --- a/source/source_estate/module_pot/pot_sccs.cpp +++ b/source/source_estate/module_pot/pot_sccs.cpp @@ -57,29 +57,18 @@ void PotSccs::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase:: ModuleSccs::gaussian_ionic_density(atoms, basis, cell->tpiba, ModuleSccs::gaussian_ion_spread, ions); std::vector density(basis.nrxx, 0.0); std::vector solute_charge(basis.nrxx); - double ionic_sum = 0.0; double net_charge = 0.0; for (int ir = 0; ir < basis.nrxx; ++ir) { for (int spin = 0; spin < charge->nspin; ++spin) { density[ir] += charge->rho[spin][ir]; } solute_charge[ir] = ions[ir] - density[ir]; - ionic_sum += ions[ir]; net_charge += solute_charge[ir]; } - Parallel_Reduce::reduce_pool(ionic_sum); + // Pool-reduced, so every rank takes the same decision. Parallel_Reduce::reduce_pool(net_charge); const double dv = basis.omega / basis.nxyz; - ionic_sum *= dv; net_charge *= dv; - double expected_ionic_charge = 0.0; - for (const unitcell::AtomData& atom : atoms) { expected_ionic_charge += atom.valence_charge; } - // Pool-reduced sums: every rank takes the same decision. - const double normalization_error = ionic_sum - expected_ionic_charge; - if (!std::isfinite(normalization_error) || std::abs(normalization_error) >= 1e-6) - { - ModuleBase::WARNING_QUIT("PotSccs::cal_v_eff", "SCCS Gaussian ionic charge normalization failed"); - } - const bool neutral = std::isfinite(net_charge) && std::abs(net_charge) < 1e-6; + const bool neutral = std::abs(net_charge) < 1e-6; if (!neutral) { ModuleBase::WARNING_QUIT("PotSccs::cal_v_eff", "Periodic SCCS currently requires a neutral cell"); diff --git a/source/source_hamilt/module_sccs/sccs_functional.cpp b/source/source_hamilt/module_sccs/sccs_functional.cpp index 294995e89c5..7547d7dae7b 100644 --- a/source/source_hamilt/module_sccs/sccs_functional.cpp +++ b/source/source_hamilt/module_sccs/sccs_functional.cpp @@ -4,7 +4,6 @@ #include "sccs_pw_coulomb.h" #include "source_base/parallel_reduce.h" -#include "source_base/tool_quit.h" #include "source_basis/module_pw/pw_basis.h" #include "source_hamilt/module_xc/xc_functional.h" @@ -64,12 +63,6 @@ void evaluate_functional(const std::vector& charge, * response.dsolute_drho[i]; candidate.electron_potential[i] += nonel; } - // The energies are pool-reduced, so every rank takes the same decision. - if (!std::isfinite(candidate.reaction_energy) || !std::isfinite(candidate.surface_energy) - || !std::isfinite(candidate.volume_energy)) - { - ModuleBase::WARNING_QUIT("ModuleSccs::evaluate_functional", "SCCS functional energy is not finite"); - } result = std::move(candidate); } } // namespace ModuleSccs diff --git a/source/source_hamilt/module_sccs/sccs_response.cpp b/source/source_hamilt/module_sccs/sccs_response.cpp index 1baa0fcc2af..f625be425eb 100644 --- a/source/source_hamilt/module_sccs/sccs_response.cpp +++ b/source/source_hamilt/module_sccs/sccs_response.cpp @@ -72,7 +72,8 @@ void prepare_cavity(const std::vector& density, } } -// Pool-reduced norms, so every rank takes the same convergence decision. +// Pool-reduced norms, so every rank takes the same convergence decision. A +// non-finite residual fails convergence and stops at the next CG step. void residual_norms(const std::vector& values, const ModulePW::PW_Basis& basis, double& rms, @@ -88,10 +89,6 @@ void residual_norms(const std::vector& values, } Parallel_Reduce::reduce_pool(square); Parallel_Reduce::reduce_max_pool(basis.poolnproc, maximum); - if (!std::isfinite(square) || !std::isfinite(maximum)) - { - ModuleBase::WARNING_QUIT("ModuleSccs::solve_sccs_response", "SCCS sqrt-CG residual is not finite"); - } const double mean_square = square / basis.nxyz; rms = std::sqrt(mean_square); } diff --git a/source/source_io/module_parameter/read_inp_sccs.cpp b/source/source_io/module_parameter/read_inp_sccs.cpp index badc3b000e8..d45e89036a5 100644 --- a/source/source_io/module_parameter/read_inp_sccs.cpp +++ b/source/source_io/module_parameter/read_inp_sccs.cpp @@ -5,7 +5,6 @@ #include #include -#include namespace ModuleIO { @@ -60,18 +59,14 @@ bool validate_sccs_input(const Input_para& input, std::string& error) error = "Unknown sccs_preset"; return false; } - if (input.sccs_maxiter <= 0 || !std::isfinite(input.sccs_epsilon) || input.sccs_epsilon < 1.0 - || !std::isfinite(input.sccs_rho_min) || !std::isfinite(input.sccs_rho_max) - || input.sccs_rho_min <= 0.0 || input.sccs_rho_max <= input.sccs_rho_min - || !std::isfinite(input.sccs_gamma) || !std::isfinite(input.sccs_pressure) - || !std::isfinite(input.sccs_tol_rms) || input.sccs_tol_rms <= 0.0 - || !std::isfinite(input.sccs_tol_max) || input.sccs_tol_max <= 0.0 - || !std::isfinite(input.sccs_surface_eta) || input.sccs_surface_eta <= 0.0) - { - error = "Invalid SCCS numerical parameters"; - return false; - } - return true; + if (input.sccs_epsilon < 1.0) { error = "sccs_epsilon must be at least 1"; } + else if (input.sccs_rho_min <= 0.0 || input.sccs_rho_max <= input.sccs_rho_min) + { error = "sccs_rho_min and sccs_rho_max must satisfy 0 < sccs_rho_min < sccs_rho_max"; } + else if (input.sccs_maxiter <= 0) { error = "sccs_maxiter must be positive"; } + else if (input.sccs_tol_rms <= 0.0) { error = "sccs_tol_rms must be positive"; } + else if (input.sccs_tol_max <= 0.0) { error = "sccs_tol_max must be positive"; } + else if (input.sccs_surface_eta <= 0.0) { error = "sccs_surface_eta must be positive"; } + return error.empty(); } void ReadInput::item_sccs() From 823e1d513caa3c46efcd6f02a45751bfe7ef8751 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Wed, 7 Oct 2026 10:05:17 -0600 Subject: [PATCH 30/41] feat(pcc): report a grid charge that differs from the expected count PotPcc takes the expected electron count (INPUT nelec) and writes a warning when the PCC net charge differs from it by more than 1e-6 e, e.g. after a very tight diagonalization. The correction keeps using the grid charge, as before. --- source/source_estate/module_pot/pot_pcc.cpp | 26 ++++++++-- source/source_estate/module_pot/pot_pcc.h | 8 ++- .../module_pot/potential_types.cpp | 16 +++--- source/source_estate/test/test_pot_pcc.cpp | 49 +++++++++++++++---- 4 files changed, 76 insertions(+), 23 deletions(-) diff --git a/source/source_estate/module_pot/pot_pcc.cpp b/source/source_estate/module_pot/pot_pcc.cpp index fc8be2a1292..271547e4fe3 100644 --- a/source/source_estate/module_pot/pot_pcc.cpp +++ b/source/source_estate/module_pot/pot_pcc.cpp @@ -6,8 +6,9 @@ #include "source_basis/module_pw/pw_grid_geometry.h" #include "source_cell/cell_tools.h" -#include #include +#include +#include namespace { @@ -108,15 +109,18 @@ bool slab_symmetry_valid(const ModuleSymmetry::Symmetry& symmetry, const int axi namespace elecstate { -PotPcc::PotPcc(const ModulePW::PW_Basis* basis) - : dimension_(Dimension::molecule), open_axis_(2) +PotPcc::PotPcc(const ModulePW::PW_Basis* basis, const double electron_count) + : dimension_(Dimension::molecule), open_axis_(2), electron_count_(electron_count) { this->rho_basis_ = basis; this->dynamic_mode = true; } -PotPcc::PotPcc(const ModulePW::PW_Basis* basis, const Dimension dimension, const int open_axis) - : dimension_(dimension), open_axis_(open_axis) +PotPcc::PotPcc(const ModulePW::PW_Basis* basis, + const Dimension dimension, + const int open_axis, + const double electron_count) + : dimension_(dimension), open_axis_(open_axis), electron_count_(electron_count) { this->rho_basis_ = basis; this->dynamic_mode = true; @@ -283,6 +287,18 @@ void PotPcc::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::m const ChargeMoments electronic_moments = this->collect_electrons(*charge, *cell, positions); // Ions are replicated on every rank and must be added after the reduction. moments_ = add_charge_moments(ionic_moments, electronic_moments); + // The grid density need not integrate to nelec exactly, e.g. after a very + // tight diagonalization; the correction uses the grid charge and reports + // the mismatch. The moments are pool-reduced, so every rank agrees. + const double expected_charge = ionic_moments.charge - electron_count_; + const double charge_error = moments_.charge - expected_charge; + if (std::abs(charge_error) > 1.0e-6) + { + std::ostringstream message; + message << "PCC net charge differs from the expected value by " << charge_error + << " e; the correction uses the grid charge"; + ModuleBase::WARNING("PotPcc::cal_v_eff", message.str()); + } energy_rydberg_ = 2.0 * this->correction_energy(); electron_potential_.resize(basis.nrxx); for (int ir = 0; ir < basis.nrxx; ++ir) diff --git a/source/source_estate/module_pot/pot_pcc.h b/source/source_estate/module_pot/pot_pcc.h index a69a14d1587..c567cfa5400 100644 --- a/source/source_estate/module_pot/pot_pcc.h +++ b/source/source_estate/module_pot/pot_pcc.h @@ -49,9 +49,12 @@ class PotPcc : public PotBase public: enum class Dimension { molecule, slab }; - explicit PotPcc(const ModulePW::PW_Basis* basis); + /// electron_count is the expected number of electrons (INPUT nelec); a + /// grid charge that differs from it by more than 1e-6 e is reported in the + /// warning log, and the correction uses the grid charge. + PotPcc(const ModulePW::PW_Basis* basis, double electron_count); - PotPcc(const ModulePW::PW_Basis* basis, Dimension dimension, int open_axis); + PotPcc(const ModulePW::PW_Basis* basis, Dimension dimension, int open_axis, double electron_count); static void validate_kpoints(const std::vector>& points, int count, int open_axis); void cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) override; @@ -72,6 +75,7 @@ class PotPcc : public PotBase const Dimension dimension_; const int open_axis_; + const double electron_count_; unitcell::SlabCell slab_; Pcc2dParameters slab_parameters_; unitcell::OrthogonalCell geometry_; diff --git a/source/source_estate/module_pot/potential_types.cpp b/source/source_estate/module_pot/potential_types.cpp index 84c6ee03182..4280bb8210f 100644 --- a/source/source_estate/module_pot/potential_types.cpp +++ b/source/source_estate/module_pot/potential_types.cpp @@ -36,14 +36,16 @@ PotBase* Potential::get_pot_type(const std::string& pot_type) { return new PotXC(PARAM.globalv.domag, PARAM.globalv.domag_z, PARAM.inp.gga_grad, this->rho_basis_, this->etxc_, this->vtxc_, &(this->vofk_eff)); } - else if (pot_type == "pcc") + else if (pot_type == "pcc" || pot_type == "pcc_2d") { - return new PotPcc(this->rho_basis_); - } - else if (pot_type == "pcc_2d") - { - const int open_axis = PARAM.inp.pcc_2d_axis; - return new PotPcc(this->rho_basis_, PotPcc::Dimension::slab, open_axis); + const Input_para& input = PARAM.inp; + const double electron_count = input.nelec; + if (pot_type == "pcc") + { + return new PotPcc(this->rho_basis_, electron_count); + } + const int open_axis = input.pcc_2d_axis; + return new PotPcc(this->rho_basis_, PotPcc::Dimension::slab, open_axis, electron_count); } else if (pot_type == "surchem") { diff --git a/source/source_estate/test/test_pot_pcc.cpp b/source/source_estate/test/test_pot_pcc.cpp index 5ba6dba5557..b4d5d0e49ae 100644 --- a/source/source_estate/test/test_pot_pcc.cpp +++ b/source/source_estate/test/test_pot_pcc.cpp @@ -1,8 +1,13 @@ #include "source_estate/module_pot/pot_pcc.h" #include "source_base/constants.h" +#include "source_base/global_variable.h" #include +#include +#include +#include + // These fixtures use public cell/charge storage without running INPUT setup. UnitCell::UnitCell() {} UnitCell::~UnitCell() {} @@ -59,7 +64,7 @@ class PotPccTest : public testing::Test TEST_F(PotPccTest, NeutralDipoleProducesConsistentEnergyPotentialAndForce) { - elecstate::PotPcc correction(&basis); + elecstate::PotPcc correction(&basis, 1.0); ModuleBase::matrix potential(1, 8); correction.cal_v_eff(&charge, &cell, potential); const double expected_energy = 4.0 * ModuleBase::PI * 0.75 / 3000.0; @@ -83,7 +88,7 @@ TEST_F(PotPccTest, TwoSpinChannelsShareCorrectionAndAddToExistingPotential) down[7] = 0.7 / 125.0; channels[1] = down.data(); charge.nspin = 2; - elecstate::PotPcc correction(&basis); + elecstate::PotPcc correction(&basis, 1.0); ModuleBase::matrix potential(2, 8); potential(0, 7) = 2.0; potential(1, 7) = 3.0; @@ -97,8 +102,8 @@ TEST_F(PotPccTest, TwoSpinChannelsShareCorrectionAndAddToExistingPotential) TEST_F(PotPccTest, RecomputesDensityAndKeepsInstancesIndependent) { - elecstate::PotPcc first(&basis); - elecstate::PotPcc second(&basis); + elecstate::PotPcc first(&basis, 1.0); + elecstate::PotPcc second(&basis, 1.0); ModuleBase::matrix potential(1, 8); first.cal_v_eff(&charge, &cell, potential); const double neutral_energy = first.get_energy(); @@ -116,7 +121,7 @@ TEST_F(PotPccTest, RecomputesDensityAndKeepsInstancesIndependent) TEST_F(PotPccTest, ForceMatchesFixedDensityEnergyDerivativeAfterMovingIons) { - elecstate::PotPcc correction(&basis); + elecstate::PotPcc correction(&basis, 1.0); ModuleBase::matrix potential(1, 8); correction.cal_v_eff(&charge, &cell, potential); ModuleBase::matrix force(1, 3); @@ -141,7 +146,7 @@ TEST_F(PotPccTest, SlabAxisControlsPotentialEnergyForceAndIgnoresInPlaneCoordina const double expected_force = 8.0 * ModuleBase::PI * 0.5 / 1000.0; for (int axis = 0; axis < 3; ++axis) { - elecstate::PotPcc correction(&basis, elecstate::PotPcc::Dimension::slab, axis); + elecstate::PotPcc correction(&basis, elecstate::PotPcc::Dimension::slab, axis, 1.0); ModuleBase::matrix potential(1, 8); correction.cal_v_eff(&charge, &cell, potential); EXPECT_NEAR(correction.get_energy(), expected_energy, 1.0e-14); @@ -161,7 +166,7 @@ TEST_F(PotPccTest, SlabRejectsOpenDirectionKpointsAndSymmetryButAllowsInPlaneTra const std::vector> points = {ModuleBase::Vector3(0.25, 0.0, 0.0)}; elecstate::PotPcc::validate_kpoints(points, 1, 2); EXPECT_EXIT(elecstate::PotPcc::validate_kpoints(points, 1, 0), testing::ExitedWithCode(1), ""); - elecstate::PotPcc correction(&basis, elecstate::PotPcc::Dimension::slab, 2); + elecstate::PotPcc correction(&basis, elecstate::PotPcc::Dimension::slab, 2, 1.0); cell.symm.ptrans = {ModuleBase::Vector3(0.5, 0.0, 0.0)}; ModuleBase::matrix potential(1, 8); correction.cal_v_eff(&charge, &cell, potential); @@ -179,10 +184,36 @@ TEST_F(PotPccTest, MoleculeRejectsNonCubicAndSlabRejectsTiltedCells) { ModuleBase::matrix potential(1, 8); cell.latvec.e22 = 2.0; - elecstate::PotPcc molecule(&basis); + elecstate::PotPcc molecule(&basis, 1.0); EXPECT_EXIT(molecule.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); cell.latvec.e22 = 1.0; cell.latvec.e31 = 0.1; - elecstate::PotPcc slab(&basis, elecstate::PotPcc::Dimension::slab, 2); + elecstate::PotPcc slab(&basis, elecstate::PotPcc::Dimension::slab, 2, 1.0); EXPECT_EXIT(slab.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); } + +TEST_F(PotPccTest, ReportsGridChargeMismatchAndUsesGridCharge) +{ + const std::string log_name = "pot_pcc_charge_warning.log"; + std::ofstream& warning_log = GlobalV::ofs_warning; + warning_log.open(log_name.c_str()); + ModuleBase::matrix matched_potential(1, 8); + elecstate::PotPcc matched(&basis, 1.0); + matched.cal_v_eff(&charge, &cell, matched_potential); + warning_log.flush(); + std::ifstream matched_log(log_name.c_str()); + std::stringstream matched_text; + matched_text << matched_log.rdbuf(); + EXPECT_EQ(matched_text.str().find("PCC net charge"), std::string::npos); + + ModuleBase::matrix mismatched_potential(1, 8); + elecstate::PotPcc mismatched(&basis, 2.0); + mismatched.cal_v_eff(&charge, &cell, mismatched_potential); + warning_log.close(); + std::ifstream mismatched_log(log_name.c_str()); + std::stringstream mismatched_text; + mismatched_text << mismatched_log.rdbuf(); + EXPECT_NE(mismatched_text.str().find("PCC net charge differs from the expected value"), std::string::npos); + EXPECT_DOUBLE_EQ(mismatched.get_energy(), matched.get_energy()); + std::remove(log_name.c_str()); +} From ea77c8e18d68ab1d21157fc39493995c3247b4c3 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Wed, 7 Oct 2026 10:06:18 -0600 Subject: [PATCH 31/41] feat(sccs): warn instead of stopping for a charged periodic cell The periodic Poisson solver drops the G = 0 component of the net charge. In a dielectric this leaves a cell-size error with no closed-form correction, so a charged cell with epsilon above 1 now runs with a warning instead of stopping; vacuum (epsilon 1) runs silently. PotSccs takes the expected electron count (INPUT nelec). It stops if the Gaussian ionic density does not integrate to the valence charge, and warns when the grid electron count differs from nelec by more than 1e-6 e; the solute charge keeps using the grid density. --- source/source_estate/module_pot/pot_sccs.cpp | 43 +++++++++--- source/source_estate/module_pot/pot_sccs.h | 14 +++- .../module_pot/potential_types.cpp | 7 +- source/source_estate/test/test_pot_sccs.cpp | 67 ++++++++++++++++++- .../module_parameter/read_inp_sccs.cpp | 2 +- 5 files changed, 118 insertions(+), 15 deletions(-) diff --git a/source/source_estate/module_pot/pot_sccs.cpp b/source/source_estate/module_pot/pot_sccs.cpp index 2168383e0d3..7d5dd323b19 100644 --- a/source/source_estate/module_pot/pot_sccs.cpp +++ b/source/source_estate/module_pot/pot_sccs.cpp @@ -11,6 +11,7 @@ #include "source_io/module_parameter/input_parameter.h" #include +#include #include namespace elecstate @@ -39,10 +40,24 @@ void make_sccs_config_from_input(const Input_para& input, solver.tolerance_max = input.sccs_tol_max; } +std::string check_sccs_charge(const ModuleSccs::SccsConfig& config, const UnitCell& cell, double electron_count) +{ + double ionic_charge = 0.0; + for (int it = 0; it < cell.ntype; ++it) + { + ionic_charge += cell.atoms[it].ncpp.zv * cell.atoms[it].na; + } + const double net_charge = ionic_charge - electron_count; + if (config.cavity.epsilon_bulk <= 1.0 || std::abs(net_charge) <= 1e-6) { return std::string(); } + return "charged SCCS with periodic boundaries: the periodic Poisson solver drops the G = 0 component " + "of the net charge, so the energy depends on the cell size."; +} + PotSccs::PotSccs(const ModulePW::PW_Basis* basis, const ModuleSccs::SccsConfig& config, - const ModuleSccs::PolarizationSolverParameters& solver) - : config_(config), solver_(solver) + const ModuleSccs::PolarizationSolverParameters& solver, + double electron_count) + : config_(config), solver_(solver), electron_count_(electron_count) { this->rho_basis_ = basis; this->dynamic_mode = true; @@ -57,21 +72,31 @@ void PotSccs::cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase:: ModuleSccs::gaussian_ionic_density(atoms, basis, cell->tpiba, ModuleSccs::gaussian_ion_spread, ions); std::vector density(basis.nrxx, 0.0); std::vector solute_charge(basis.nrxx); - double net_charge = 0.0; + double grid_charges[2] = {0.0, 0.0}; // electrons, ions for (int ir = 0; ir < basis.nrxx; ++ir) { for (int spin = 0; spin < charge->nspin; ++spin) { density[ir] += charge->rho[spin][ir]; } solute_charge[ir] = ions[ir] - density[ir]; - net_charge += solute_charge[ir]; + grid_charges[0] += density[ir]; + grid_charges[1] += ions[ir]; } // Pool-reduced, so every rank takes the same decision. - Parallel_Reduce::reduce_pool(net_charge); + Parallel_Reduce::reduce_pool(grid_charges, 2); const double dv = basis.omega / basis.nxyz; - net_charge *= dv; - const bool neutral = std::abs(net_charge) < 1e-6; - if (!neutral) + const double electron_error = grid_charges[0] * dv - electron_count_; + double valence_charge = 0.0; + for (const unitcell::AtomData& atom : atoms) { valence_charge += atom.valence_charge; } + const double ionic_error = grid_charges[1] * dv - valence_charge; + if (std::abs(ionic_error) > 1e-6) + { + ModuleBase::WARNING_QUIT("PotSccs::cal_v_eff", "SCCS ionic density normalization does not match the valence charge"); + } + if (std::abs(electron_error) > 1e-6) { - ModuleBase::WARNING_QUIT("PotSccs::cal_v_eff", "Periodic SCCS currently requires a neutral cell"); + std::ostringstream message; + message << "SCCS grid electron count differs from the expected value by " << electron_error + << " e; the solute charge uses the grid density"; + ModuleBase::WARNING("PotSccs::cal_v_eff", message.str()); } ModuleSccs::SccsResponse response; ModuleSccs::solve_sccs_response(density, solute_charge, config_.cavity, solver_, restart_potential_, basis, diff --git a/source/source_estate/module_pot/pot_sccs.h b/source/source_estate/module_pot/pot_sccs.h index c1aa7a770a2..2a2d02e3dcf 100644 --- a/source/source_estate/module_pot/pot_sccs.h +++ b/source/source_estate/module_pot/pot_sccs.h @@ -4,6 +4,8 @@ #include "pot_base.h" #include "source_hamilt/module_sccs/sccs_parameters.h" +#include + struct Input_para; namespace elecstate { @@ -12,12 +14,21 @@ void make_sccs_config_from_input(const Input_para& input, ModuleSccs::SccsConfig& config, ModuleSccs::PolarizationSolverParameters& solver); +// Returns a warning (empty if none) for a charged cell in a dielectric: the +// periodic Poisson solver drops the G = 0 component of the net charge, so the +// energy depends on the cell size. electron_count is INPUT nelec. +std::string check_sccs_charge(const ModuleSccs::SccsConfig& config, const UnitCell& cell, double electron_count); + class PotSccs : public PotBase { public: + // electron_count is the expected number of electrons (INPUT nelec); a grid + // density that differs from it by more than 1e-6 e is reported in the + // warning log, and the solute charge uses the grid density. PotSccs(const ModulePW::PW_Basis* basis, const ModuleSccs::SccsConfig& config, - const ModuleSccs::PolarizationSolverParameters& solver); + const ModuleSccs::PolarizationSolverParameters& solver, + double electron_count); void cal_v_eff(const Charge* charge, const UnitCell* cell, ModuleBase::matrix& potential) override; double get_energy() const override; void add_solvation_force(const UnitCell& cell, ModuleBase::matrix& force) const override; @@ -27,6 +38,7 @@ class PotSccs : public PotBase private: const ModuleSccs::SccsConfig config_; const ModuleSccs::PolarizationSolverParameters solver_; + const double electron_count_; double electrostatic_rydberg_ = 0.0; double non_electrostatic_rydberg_ = 0.0; std::vector electrostatic_potential_; diff --git a/source/source_estate/module_pot/potential_types.cpp b/source/source_estate/module_pot/potential_types.cpp index f7841ac64da..27694902916 100644 --- a/source/source_estate/module_pot/potential_types.cpp +++ b/source/source_estate/module_pot/potential_types.cpp @@ -27,7 +27,12 @@ PotBase* Potential::make_sccs_potential(const Input_para& input) ModuleSccs::SccsConfig config; ModuleSccs::PolarizationSolverParameters solver; make_sccs_config_from_input(input, config, solver); - return new PotSccs(this->rho_basis_, config, solver); + if (this->ucell_ != nullptr) + { + const std::string warning = check_sccs_charge(config, *this->ucell_, input.nelec); + if (!warning.empty()) { ModuleBase::WARNING("Potential::make_sccs_potential", warning); } + } + return new PotSccs(this->rho_basis_, config, solver, input.nelec); } PotBase* Potential::get_pot_type(const std::string& pot_type, const Input_para& input) diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp index e47be13ec70..00a96fbbaed 100644 --- a/source/source_estate/test/test_pot_sccs.cpp +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -1,6 +1,12 @@ #include "source_estate/module_pot/pot_sccs.h" #include "source_io/module_parameter/input_parameter.h" #include "source_hamilt/module_sccs/test/sccs_test.h" +#include "source_base/global_variable.h" + +#include +#include +#include +#include // Public storage fixtures: no INPUT initialization or privately owned atom maps. UnitCell::UnitCell() {} @@ -40,9 +46,9 @@ TEST_F(PotSccsTest, IndependentInstancesAndSpinChannels) ModuleSccs::PolarizationSolverParameters solver; elecstate::make_sccs_config_from_input(input, config, solver); config.cavity.epsilon_bulk = 5.0; - elecstate::PotSccs first(&basis, config, solver); + elecstate::PotSccs first(&basis, config, solver, 1.0); config.cavity.epsilon_bulk = 1.0; - elecstate::PotSccs vacuum(&basis, config, solver); + elecstate::PotSccs vacuum(&basis, config, solver, 1.0); EXPECT_DOUBLE_EQ(first.get_energy(), 0.0); ModuleBase::matrix potential(1, basis.nrxx); first.cal_v_eff(&charge, &cell, potential); @@ -114,7 +120,7 @@ TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) ModuleSccs::SccsConfig config; ModuleSccs::PolarizationSolverParameters solver; elecstate::make_sccs_config_from_input(input, config, solver); - elecstate::PotSccs component(&basis, config, solver); + elecstate::PotSccs component(&basis, config, solver, 2.0); ModuleBase::matrix potential(1, basis.nrxx); component.cal_v_eff(&charge, &cell, potential); ModuleBase::matrix force(2, 3); @@ -155,3 +161,58 @@ TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) } } } + +TEST_F(PotSccsTest, ChargedDielectricCellWarnsAndRuns) +{ + UnitCell cell; + Atom atom; + cell.lat0 = length; + cell.tpiba = tpiba; + cell.omega = basis.omega; + cell.ntype = 1; + cell.nat = 1; + cell.atoms = &atom; + atom.na = 1; + atom.ncpp.zv = 1.0; + atom.tau = {ModuleBase::Vector3(0.25, 0.25, 0.25)}; + Input_para input; + ModuleSccs::SccsConfig config; + ModuleSccs::PolarizationSolverParameters solver; + elecstate::make_sccs_config_from_input(input, config, solver); + config.cavity.epsilon_bulk = 5.0; + EXPECT_TRUE(elecstate::check_sccs_charge(config, cell, 1.0).empty()); + EXPECT_NE(elecstate::check_sccs_charge(config, cell, 0.5).find("G = 0"), std::string::npos); + ModuleSccs::SccsConfig vacuum_config = config; + vacuum_config.cavity.epsilon_bulk = 1.0; + EXPECT_TRUE(elecstate::check_sccs_charge(vacuum_config, cell, 0.5).empty()); + + const double density_value = 0.5 / basis.omega; + std::vector density(basis.nrxx, density_value); + double* channels[] = {density.data()}; + Charge charge; + charge.nspin = 1; + charge.rho = channels; + const std::string log_name = "pot_sccs_charge_warning.log"; + std::ofstream& warning_log = GlobalV::ofs_warning; + warning_log.open(log_name.c_str()); + elecstate::PotSccs matched(&basis, config, solver, 0.5); + ModuleBase::matrix matched_potential(1, basis.nrxx); + matched.cal_v_eff(&charge, &cell, matched_potential); + warning_log.flush(); + std::ifstream matched_log(log_name.c_str()); + std::stringstream matched_text; + matched_text << matched_log.rdbuf(); + EXPECT_EQ(matched_text.str().find("SCCS grid electron count"), std::string::npos); + EXPECT_TRUE(std::isfinite(matched.get_energy())); + + elecstate::PotSccs mismatched(&basis, config, solver, 1.0); + ModuleBase::matrix mismatched_potential(1, basis.nrxx); + mismatched.cal_v_eff(&charge, &cell, mismatched_potential); + warning_log.close(); + std::ifstream mismatched_log(log_name.c_str()); + std::stringstream mismatched_text; + mismatched_text << mismatched_log.rdbuf(); + EXPECT_NE(mismatched_text.str().find("SCCS grid electron count differs"), std::string::npos); + EXPECT_DOUBLE_EQ(mismatched.get_energy(), matched.get_energy()); + std::remove(log_name.c_str()); +} diff --git a/source/source_io/module_parameter/read_inp_sccs.cpp b/source/source_io/module_parameter/read_inp_sccs.cpp index d45e89036a5..5129a2b7e93 100644 --- a/source/source_io/module_parameter/read_inp_sccs.cpp +++ b/source/source_io/module_parameter/read_inp_sccs.cpp @@ -76,7 +76,7 @@ void ReadInput::item_sccs() item.annotation = "implicit solvent model"; item.category = "Implicit solvation model"; item.type = "Integer"; - item.description = "Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models."; + item.description = "Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. A charged cell in a dielectric solvent (any water preset, or sccs_preset custom with sccs_epsilon above 1) runs with a warning: the periodic Poisson solver drops the G = 0 component of the net charge, so the energy depends on the cell size."; item.default_value = "0"; item.read_value = [](const Input_Item& item, Parameter& para) { std::string error; From 0ebabd707b8c718ca20754711dfc11c6e1a36dd1 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Wed, 7 Oct 2026 10:09:01 -0600 Subject: [PATCH 32/41] docs(sccs): regenerate INPUT docs for the charged periodic warning --- docs/advanced/input_files/input-main.md | 2 +- docs/parameters.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 44084982e55..26db2fc0b1a 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -4938,7 +4938,7 @@ ### imp_sol - **Type**: Integer -- **Description**: Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. +- **Description**: Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. A charged cell in a dielectric solvent (any water preset, or sccs_preset custom with sccs_epsilon above 1) runs with a warning: the periodic Poisson solver drops the G = 0 component of the net charge, so the energy depends on the cell size. - **Default**: 0 ### sccs_preset diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 4a8407f48b4..50936f9f5dd 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -4355,7 +4355,7 @@ parameters: category: Implicit solvation model type: Integer description: | - Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports neutral periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. + Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. A charged cell in a dielectric solvent (any water preset, or sccs_preset custom with sccs_epsilon above 1) runs with a warning: the periodic Poisson solver drops the G = 0 component of the net charge, so the energy depends on the cell size. default_value: "0" unit: "" availability: "" From afef86de06576e7ac4809863e3f5a2f17ec6b05d Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Wed, 7 Oct 2026 10:12:33 -0600 Subject: [PATCH 33/41] test(sccs): write the charge-warning log per rank --- source/source_estate/test/test_pot_sccs.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp index 00a96fbbaed..ea42d0e69ec 100644 --- a/source/source_estate/test/test_pot_sccs.cpp +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // Public storage fixtures: no INPUT initialization or privately owned atom maps. UnitCell::UnitCell() {} @@ -192,7 +193,8 @@ TEST_F(PotSccsTest, ChargedDielectricCellWarnsAndRuns) Charge charge; charge.nspin = 1; charge.rho = channels; - const std::string log_name = "pot_sccs_charge_warning.log"; + // One log per rank: the MPI variant runs this test on several ranks at once. + const std::string log_name = "pot_sccs_charge_warning_" + std::to_string(basis.poolrank) + ".log"; std::ofstream& warning_log = GlobalV::ofs_warning; warning_log.open(log_name.c_str()); elecstate::PotSccs matched(&basis, config, solver, 0.5); From 9e05afe1583cfc9ca9c6fb90ac4d56149ef5ca9f Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Wed, 7 Oct 2026 11:37:18 -0600 Subject: [PATCH 34/41] feat(sccs): default sccs_epsilon to 1 as Environ environ_type input The custom preset is the counterpart of Environ environ_type input, whose env_static_permittivity defaults to 1 (no dielectric). sccs_epsilon defaulted to 78.3 instead. The vacuum and water presets already match Environ environ_type_setup and are unchanged. --- docs/advanced/input_files/input-main.md | 4 ++-- docs/parameters.yaml | 4 ++-- source/source_estate/test/test_pot_sccs.cpp | 17 +++++++++++++++++ .../module_parameter/input_parameter.h | 2 +- .../module_parameter/read_inp_sccs.cpp | 4 ++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 26db2fc0b1a..7cf69ff2cc6 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -4952,8 +4952,8 @@ - **Type**: Real - **Availability**: *[`imp_sol`](#imp_sol)==2* -- **Description**: Bulk dielectric constant >= 1 for the custom preset. Water presets use 78.3 and vacuum uses 1. -- **Default**: 78.3 +- **Description**: Bulk dielectric constant >= 1 for the custom preset. The default 1 (no dielectric) follows the Environ env_static_permittivity default for environ_type input. Water presets use 78.3 and vacuum uses 1. +- **Default**: 1.0 ### sccs_rho_min diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 50936f9f5dd..a1c0d9396d1 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -4371,8 +4371,8 @@ parameters: category: Implicit solvation model type: Real description: | - Bulk dielectric constant >= 1 for the custom preset. Water presets use 78.3 and vacuum uses 1. - default_value: "78.3" + Bulk dielectric constant >= 1 for the custom preset. The default 1 (no dielectric) follows the Environ env_static_permittivity default for environ_type input. Water presets use 78.3 and vacuum uses 1. + default_value: "1.0" unit: "" availability: imp_sol==2 - name: sccs_rho_min diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp index ea42d0e69ec..eb23917bd18 100644 --- a/source/source_estate/test/test_pot_sccs.cpp +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -94,6 +94,23 @@ TEST_F(PotSccsTest, InputMapsOntoSccsConfig) EXPECT_EQ(solver.max_iterations, 42); } +TEST_F(PotSccsTest, CustomDefaultsFollowEnvironInput) +{ + // Environ environ_type input: env_static_permittivity 1, rhomin 1e-4, + // rhomax 5e-3, no surface tension or pressure. + Input_para input; + ModuleSccs::SccsConfig config; + ModuleSccs::PolarizationSolverParameters solver; + elecstate::make_sccs_config_from_input(input, config, solver); + EXPECT_EQ(input.sccs_preset, "custom"); + EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 1.0); + EXPECT_DOUBLE_EQ(config.cavity.density_min, 1e-4); + EXPECT_DOUBLE_EQ(config.cavity.density_max, 5e-3); + EXPECT_DOUBLE_EQ(config.surface_tension, 0.0); + EXPECT_DOUBLE_EQ(config.pressure, 0.0); + EXPECT_EQ(solver.max_iterations, 200); +} + TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) { UnitCell cell; diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index 693c8f25bc6..54e408c593b 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -578,7 +578,7 @@ struct Input_para double sigma_k = 0.6; ///< the width of the diffuse cavity double nc_k = 0.00037; ///< the cut-off charge density std::string sccs_preset = "custom"; - double sccs_epsilon = 78.3; + double sccs_epsilon = 1.0; // Environ env_static_permittivity default double sccs_rho_min = 1e-4; double sccs_rho_max = 5e-3; double sccs_gamma = 0.0; // dyn/cm diff --git a/source/source_io/module_parameter/read_inp_sccs.cpp b/source/source_io/module_parameter/read_inp_sccs.cpp index 5129a2b7e93..ef74d61de44 100644 --- a/source/source_io/module_parameter/read_inp_sccs.cpp +++ b/source/source_io/module_parameter/read_inp_sccs.cpp @@ -108,8 +108,8 @@ void ReadInput::item_sccs() item.annotation = "Bulk dielectric constant >= 1 for the custom preset"; item.category = "Implicit solvation model"; item.type = "Real"; - item.description = "Bulk dielectric constant >= 1 for the custom preset. Water presets use 78.3 and vacuum uses 1."; - item.default_value = "78.3"; + item.description = "Bulk dielectric constant >= 1 for the custom preset. The default 1 (no dielectric) follows the Environ env_static_permittivity default for environ_type input. Water presets use 78.3 and vacuum uses 1."; + item.default_value = "1.0"; item.unit = ""; item.set_availability("imp_sol==2"); read_sync_double(input.sccs_epsilon); From 54041663f3db815b0548e341d9e8c720edd89f5e Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Wed, 7 Oct 2026 22:57:36 -0600 Subject: [PATCH 35/41] refactor(sccs): split the sqrt-CG solve into its stages solve_sccs_response now prepares the cavity and calls try_warm_start, iterate_sqrt_cg and finish_response, which share the operator data in SqrtCgOperator and the potential, residual and scratch in SqrtCgState. The periodic water case is bit-identical. --- .../module_sccs/sccs_response.cpp | 168 ++++++++++++------ 1 file changed, 113 insertions(+), 55 deletions(-) diff --git a/source/source_hamilt/module_sccs/sccs_response.cpp b/source/source_hamilt/module_sccs/sccs_response.cpp index f625be425eb..ea0a81c414f 100644 --- a/source/source_hamilt/module_sccs/sccs_response.cpp +++ b/source/source_hamilt/module_sccs/sccs_response.cpp @@ -130,69 +130,78 @@ void apply_preconditioner(const std::vector& rhs, value[i] *= invsqrt[i]; } } -} // namespace -void solve_sccs_response(const std::vector& density, - const std::vector& charge, - const CavityParameters& cavity, - const PolarizationSolverParameters& solver, - const std::vector& initial_potential, - const ModulePW::PW_Basis& basis, - double tpiba, - SccsResponse& result) +// The preconditioned sqrt-CG system of one response. +struct SqrtCgOperator { - // Determine start mode collectively, including ranks with no real-space points. - double initial_count = initial_potential.size(); - Parallel_Reduce::reduce_pool(initial_count); - const bool warm_start = initial_count != 0.0; + const std::vector& coefficient; + const std::vector& invsqrt; + const ModulePW::PW_Basis& basis; + PeriodicCoulombOperator& coulomb; +}; - SccsResponse candidate; - std::vector coefficient; - prepare_cavity(density, cavity, basis, tpiba, candidate, coefficient); - const std::size_t size = density.size(); - std::vector invsqrt(size); +// Potential and charge residual of the sqrt-CG, with preconditioner scratch. +struct SqrtCgState +{ + std::vector potential; + std::vector residual; + std::vector weighted; + std::vector preconditioned; +}; + +// Replace the cold start by the old fixed point when that lowers the RMS +// charge residual. +void try_warm_start(const SqrtCgOperator& system, + const std::vector& charge, + const std::vector& initial_potential, + SqrtCgState& state, + PolarizationResult& polarization) +{ + const std::size_t size = charge.size(); + const std::vector& coefficient = system.coefficient; + std::vector& z = state.preconditioned; + std::vector guess_residual(size); for (std::size_t i = 0; i < size; ++i) { - invsqrt[i] = 1.0 / std::sqrt(candidate.epsilon[i]); + guess_residual[i] = charge[i] - coefficient[i] * initial_potential[i]; } - PeriodicCoulombOperator coulomb(basis, tpiba); - std::vector residual = charge; - std::vector potential(size, 0.0); - std::vector direction(size, 0.0); - std::vector image(size, 0.0); - std::vector weighted; - std::vector z; - PolarizationResult& polarization = candidate.polarization; - residual_norms(residual, basis, polarization.residual_rms, polarization.residual_max); - // Retain the old fixed-point warm start only when it reduces the charge residual. - if (!converged(polarization, solver) && warm_start) + apply_preconditioner(guess_residual, system.invsqrt, system.coulomb, state.weighted, z); + for (std::size_t i = 0; i < size; ++i) { - std::vector guess_residual(size); - for (std::size_t i = 0; i < size; ++i) - { - guess_residual[i] = charge[i] - coefficient[i] * initial_potential[i]; - } - apply_preconditioner(guess_residual, invsqrt, coulomb, weighted, z); - for (std::size_t i = 0; i < size; ++i) - { - guess_residual[i] = coefficient[i] * (initial_potential[i] - z[i]); - } - double guess_rms = 0.0; - double guess_max = 0.0; - residual_norms(guess_residual, basis, guess_rms, guess_max); - if (guess_rms < polarization.residual_rms) - { - potential.swap(z); - residual.swap(guess_residual); - polarization.warm_started = true; - polarization.residual_rms = guess_rms; - polarization.residual_max = guess_max; - } + guess_residual[i] = coefficient[i] * (initial_potential[i] - z[i]); + } + double guess_rms = 0.0; + double guess_max = 0.0; + residual_norms(guess_residual, system.basis, guess_rms, guess_max); + if (guess_rms < polarization.residual_rms) + { + state.potential.swap(z); + state.residual.swap(guess_residual); + polarization.warm_started = true; + polarization.residual_rms = guess_rms; + polarization.residual_max = guess_max; } +} + +// Preconditioned CG until both residual tolerances hold; a breakdown or a +// missed tolerance stops the run with WARNING_QUIT. +void iterate_sqrt_cg(const SqrtCgOperator& system, + const PolarizationSolverParameters& solver, + SqrtCgState& state, + PolarizationResult& polarization) +{ + const ModulePW::PW_Basis& basis = system.basis; + const std::vector& coefficient = system.coefficient; + std::vector& potential = state.potential; + std::vector& residual = state.residual; + std::vector& z = state.preconditioned; + const std::size_t size = residual.size(); + std::vector direction(size, 0.0); + std::vector image(size, 0.0); double old_rz = 0.0; for (int iteration = 1; !converged(polarization, solver) && iteration <= solver.max_iterations; ++iteration) { - apply_preconditioner(residual, invsqrt, coulomb, weighted, z); + apply_preconditioner(residual, system.invsqrt, system.coulomb, state.weighted, z); const double rz = grid_dot(residual, z, basis); if (!std::isfinite(rz) || std::abs(rz) < 1e-30) { @@ -228,8 +237,18 @@ void solve_sccs_response(const std::vector& density, << polarization.residual_max << ")"; ModuleBase::WARNING_QUIT("ModuleSccs::solve_sccs_response", message.str()); } +} - candidate.restart_potential = potential; +// Keep the unshifted solution for warm starts, then store the zero-mean +// potential, its gradient and the cavity potential. +void finish_response(const ModulePW::PW_Basis& basis, + double tpiba, + std::vector& potential, + SccsResponse& response) +{ + PolarizationResult& polarization = response.polarization; + const std::size_t size = potential.size(); + response.restart_potential = potential; double mean = 0.0; for (double value : potential) { @@ -246,13 +265,52 @@ void solve_sccs_response(const std::vector& density, polarization.gradient.resize(size); XC_Functional::grad_rho(potential_g.data(), polarization.gradient.data(), &basis, tpiba); polarization.potential.swap(potential); - candidate.cavity_potential.resize(size); + response.cavity_potential.resize(size); for (std::size_t i = 0; i < size; ++i) { const ModuleBase::Vector3& gradient = polarization.gradient[i]; const double gradient_square = gradient * gradient; - candidate.cavity_potential[i] = -candidate.depsilon_drho[i] * gradient_square / (8.0 * ModuleBase::PI); + response.cavity_potential[i] = -response.depsilon_drho[i] * gradient_square / (8.0 * ModuleBase::PI); + } +} +} // namespace + +void solve_sccs_response(const std::vector& density, + const std::vector& charge, + const CavityParameters& cavity, + const PolarizationSolverParameters& solver, + const std::vector& initial_potential, + const ModulePW::PW_Basis& basis, + double tpiba, + SccsResponse& result) +{ + // Determine start mode collectively, including ranks with no real-space points. + double initial_count = initial_potential.size(); + Parallel_Reduce::reduce_pool(initial_count); + const bool warm_start = initial_count != 0.0; + + SccsResponse candidate; + std::vector coefficient; + prepare_cavity(density, cavity, basis, tpiba, candidate, coefficient); + const std::size_t size = density.size(); + std::vector invsqrt(size); + for (std::size_t i = 0; i < size; ++i) + { + invsqrt[i] = 1.0 / std::sqrt(candidate.epsilon[i]); + } + PeriodicCoulombOperator coulomb(basis, tpiba); + const SqrtCgOperator system = {coefficient, invsqrt, basis, coulomb}; + SqrtCgState state; + state.residual = charge; + state.potential.assign(size, 0.0); + PolarizationResult& polarization = candidate.polarization; + residual_norms(state.residual, basis, polarization.residual_rms, polarization.residual_max); + if (!converged(polarization, solver) && warm_start) + { + try_warm_start(system, charge, initial_potential, state, polarization); } + iterate_sqrt_cg(system, solver, state, polarization); + finish_response(basis, tpiba, state.potential, candidate); result = std::move(candidate); } } // namespace ModuleSccs From 3d8143bc93dd0610cb6a279a9aa33058addb0336 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Wed, 7 Oct 2026 23:08:45 -0600 Subject: [PATCH 36/41] refactor(sccs): group the SCCS INPUT checks validate_sccs_input runs the calculation-context, model and solver checks in sccs_context_error, sccs_model_error and sccs_solver_error, so each later key joins a short list of related checks. --- .../module_parameter/read_inp_sccs.cpp | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/source/source_io/module_parameter/read_inp_sccs.cpp b/source/source_io/module_parameter/read_inp_sccs.cpp index ef74d61de44..1fb6e36f0d2 100644 --- a/source/source_io/module_parameter/read_inp_sccs.cpp +++ b/source/source_io/module_parameter/read_inp_sccs.cpp @@ -31,15 +31,12 @@ bool parse_solvation_model(const std::string& value, int& model, std::string& er return true; } -bool validate_sccs_input(const Input_para& input, std::string& error) +namespace { - error.clear(); - if (input.imp_sol < 0 || input.imp_sol > 2) - { - error = "imp_sol must be 0, 1 or 2"; - return false; - } - if (input.imp_sol != 2) { return true; } +// Calculation settings that SCCS supports; empty when all are supported. +std::string sccs_context_error(const Input_para& input) +{ + std::string error; if (input.assume_isolated != "none") { error = "SCCS currently requires assume_isolated none"; } else if (input.device != "cpu" || input.esolver_type != "ksdft") { error = "SCCS requires CPU KS-DFT"; } else if (input.basis_type != "pw" && input.basis_type != "lcao") { error = "SCCS requires basis_type pw or lcao"; } @@ -52,20 +49,48 @@ bool validate_sccs_input(const Input_para& input, std::string& error) { error = "SCCS does not support electric/gate fields, DFT-1/2, DeePKS or dm_to_rho"; } else if (!input.vl_in_h || !input.vion_in_h || !input.vh_in_h) { error = "SCCS requires the local ionic and Hartree potentials in the Hamiltonian"; } - if (!error.empty()) { return false; } + return error; +} + +// Cavity, surface and boundary model parameters, starting with the preset. +std::string sccs_model_error(const Input_para& input) +{ const std::vector presets = {"custom", "vacuum", "water-neutral", "water-cation", "water-anion"}; if (std::find(presets.begin(), presets.end(), input.sccs_preset) == presets.end()) { - error = "Unknown sccs_preset"; - return false; + return "Unknown sccs_preset"; } + std::string error; if (input.sccs_epsilon < 1.0) { error = "sccs_epsilon must be at least 1"; } else if (input.sccs_rho_min <= 0.0 || input.sccs_rho_max <= input.sccs_rho_min) { error = "sccs_rho_min and sccs_rho_max must satisfy 0 < sccs_rho_min < sccs_rho_max"; } - else if (input.sccs_maxiter <= 0) { error = "sccs_maxiter must be positive"; } + else if (input.sccs_surface_eta <= 0.0) { error = "sccs_surface_eta must be positive"; } + return error; +} + +// sqrt-CG solver controls. +std::string sccs_solver_error(const Input_para& input) +{ + std::string error; + if (input.sccs_maxiter <= 0) { error = "sccs_maxiter must be positive"; } else if (input.sccs_tol_rms <= 0.0) { error = "sccs_tol_rms must be positive"; } else if (input.sccs_tol_max <= 0.0) { error = "sccs_tol_max must be positive"; } - else if (input.sccs_surface_eta <= 0.0) { error = "sccs_surface_eta must be positive"; } + return error; +} +} // namespace + +bool validate_sccs_input(const Input_para& input, std::string& error) +{ + error.clear(); + if (input.imp_sol < 0 || input.imp_sol > 2) + { + error = "imp_sol must be 0, 1 or 2"; + return false; + } + if (input.imp_sol != 2) { return true; } + error = sccs_context_error(input); + if (error.empty()) { error = sccs_model_error(input); } + if (error.empty()) { error = sccs_solver_error(input); } return error.empty(); } From 9667b09df17274df467d652ea9cde5767df975d7 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Thu, 8 Oct 2026 00:26:18 -0600 Subject: [PATCH 37/41] docs(pcc): shorten the PCC INPUT descriptions Drop the symmetry and origin details from assume_isolated pcc_0d and pcc_2d and the LCAO load-balance note from pcc_2d_axis; the run still stops with a clear message when the cell or symmetry is unsupported. --- docs/advanced/input_files/input-main.md | 6 +++--- docs/parameters.yaml | 6 +++--- source/source_io/module_parameter/read_inp_sys.cpp | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 67ddc5f2be5..6453589ec9d 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -770,8 +770,8 @@ Available options are: - none: regular periodic calculation without isolated-system correction. - - pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. The cell must be primitive: with symmetry 1, an analyzed fractional translation stops the run. - - pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The multipoles are taken about the mass-weighted ionic center along the normal, and the correction is cut half a cell length from that center, so this plane must lie in the vacuum. With symmetry 1, every analyzed operation, including the primitive-cell translations used to symmetrize the density, must map the open vector onto plus or minus itself; an operation that keeps its direction must not translate by a fraction of it, which would copy the slab within the cell, whereas a mirror normal to it may. Otherwise the run stops; use a primitive cell with vacuum only along the open vector, or symmetry 0 or -1. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. + - pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. + - pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. - makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). pcc_0d and pcc_2d contribute to the energy, the potential and the fixed-cell forces; the correction energy is printed as E_pcc. They require CPU KS-DFT (esolver_type ksdft) with basis_type pw or lcao, calculation scf or relax, and nspin 1 or 2, without efield_flag, gate_flag, cal_stress, DFT-1/2, deepks output or dm_to_rho. @@ -783,7 +783,7 @@ - **Type**: Integer - **Availability**: *[`assume_isolated`](#assume_isolated)==pcc_2d* -- **Description**: Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only, and symmetry operations must keep it (see assume_isolated). For LCAO the real-space grid integration is distributed over planes along the third lattice vector, so an open axis of 0 or 1 keeps the vacuum out of that distribution and balances the load better. +- **Description**: Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only. - **Default**: 2 ### init_wfc diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 1c53c5f0dc3..27dc078eb0f 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -178,8 +178,8 @@ parameters: Available options are: * none: regular periodic calculation without isolated-system correction. - * pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. The cell must be primitive: with symmetry 1, an analyzed fractional translation stops the run. - * pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The multipoles are taken about the mass-weighted ionic center along the normal, and the correction is cut half a cell length from that center, so this plane must lie in the vacuum. With symmetry 1, every analyzed operation, including the primitive-cell translations used to symmetrize the density, must map the open vector onto plus or minus itself; an operation that keeps its direction must not translate by a fraction of it, which would copy the slab within the cell, whereas a mirror normal to it may. Otherwise the run stops; use a primitive cell with vacuum only along the open vector, or symmetry 0 or -1. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. + * pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. + * pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). pcc_0d and pcc_2d contribute to the energy, the potential and the fixed-cell forces; the correction energy is printed as E_pcc. They require CPU KS-DFT (esolver_type ksdft) with basis_type pw or lcao, calculation scf or relax, and nspin 1 or 2, without efield_flag, gate_flag, cal_stress, DFT-1/2, deepks output or dm_to_rho. @@ -192,7 +192,7 @@ parameters: category: System variables type: Integer description: | - Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only, and symmetry operations must keep it (see assume_isolated). For LCAO the real-space grid integration is distributed over planes along the third lattice vector, so an open axis of 0 or 1 keeps the vacuum out of that distribution and balances the load better. + Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only. default_value: "2" unit: "" availability: assume_isolated==pcc_2d diff --git a/source/source_io/module_parameter/read_inp_sys.cpp b/source/source_io/module_parameter/read_inp_sys.cpp index 856eaecd7ff..a0ba7ff398a 100644 --- a/source/source_io/module_parameter/read_inp_sys.cpp +++ b/source/source_io/module_parameter/read_inp_sys.cpp @@ -506,8 +506,8 @@ Available options are: Available options are: * none: regular periodic calculation without isolated-system correction. -* pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. The cell must be primitive: with symmetry 1, an analyzed fractional translation stops the run. -* pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The multipoles are taken about the mass-weighted ionic center along the normal, and the correction is cut half a cell length from that center, so this plane must lie in the vacuum. With symmetry 1, every analyzed operation, including the primitive-cell translations used to symmetrize the density, must map the open vector onto plus or minus itself; an operation that keeps its direction must not translate by a fraction of it, which would copy the slab within the cell, whereas a mirror normal to it may. Otherwise the run stops; use a primitive cell with vacuum only along the open vector, or symmetry 0 or -1. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. +* pcc_0d: self-consistent point-counter-charge (PCC) correction for a molecule in a cubic cell. The multipoles are taken about the mass-weighted ionic center. +* pcc_2d: self-consistent PCC correction for a slab, open along the lattice vector selected by pcc_2d_axis (default: the third) and periodic along the other two. The open vector must be perpendicular to the periodic plane (rewrite a tilted cell as the equivalent perpendicular one), and the k-point sampling along it must be Gamma only. The monopole term uses the open planar kernel that vanishes on the plane of the charge, -pi*Q*L/(3*A) with L the cell length along the open vector and A the periodic area, so the energy of a charged slab converges with the vacuum size; it is referenced to zero potential on the plane of the charge. ENVIRON uses -pi*Q/(3*L), so charged-slab energies agree with ENVIRON only for A = L^2. * makov-payne, m-p, mp: compute the Makov-Payne correction to the total energy and estimate a corrected vacuum level for eigenvalue alignment. This option is available only for cubic lattices (latname = sc, fcc, or bcc). pcc_0d and pcc_2d contribute to the energy, the potential and the fixed-cell forces; the correction energy is printed as E_pcc. They require CPU KS-DFT (esolver_type ksdft) with basis_type pw or lcao, calculation scf or relax, and nspin 1 or 2, without efield_flag, gate_flag, cal_stress, DFT-1/2, deepks output or dm_to_rho. @@ -567,7 +567,7 @@ Theory: G. Makov and M. C. Payne, Phys. Rev. B 51, 4014 (1995); PCC: O. Andreuss item.annotation = "open lattice vector of assume_isolated pcc_2d"; item.category = "System variables"; item.type = "Integer"; - item.description = "Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only, and symmetry operations must keep it (see assume_isolated). For LCAO the real-space grid integration is distributed over planes along the third lattice vector, so an open axis of 0 or 1 keeps the vacuum out of that distribution and balances the load better."; + item.description = "Index of the lattice vector along which assume_isolated=pcc_2d is open: 0, 1 or 2 for the first, second or third vector of LATTICE_VECTORS. The selected vector must be perpendicular to the other two, which span the periodic plane; the k-point sampling along it must be Gamma only."; item.default_value = "2"; item.set_availability("assume_isolated==pcc_2d"); read_sync_int(input.pcc_2d_axis); From 04cb8630685908037f0667385fd01af6794b3b07 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Thu, 8 Oct 2026 00:27:18 -0600 Subject: [PATCH 38/41] docs(sccs): shorten imp_sol and list the SCCS keys after the original model imp_sol keeps only the model choice and the unsupported features. It is registered again where the original solvation keys are, and the SCCS keys follow eb_k, tau, sigma_k and nc_k in INPUT and the documentation. --- docs/advanced/input_files/input-main.md | 60 ++++++++--------- docs/parameters.yaml | 66 +++++++++---------- .../module_parameter/read_inp_model.cpp | 25 ++++++- .../module_parameter/read_inp_sccs.cpp | 20 ------ 4 files changed, 86 insertions(+), 85 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 071c8920818..535691df663 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -533,6 +533,10 @@ - [cond\_nonlocal](#cond_nonlocal) - [Implicit solvation model](#implicit-solvation-model) - [imp\_sol](#imp_sol) + - [eb\_k](#eb_k) + - [tau](#tau) + - [sigma\_k](#sigma_k) + - [nc\_k](#nc_k) - [sccs\_preset](#sccs_preset) - [sccs\_epsilon](#sccs_epsilon) - [sccs\_rho\_min](#sccs_rho_min) @@ -543,10 +547,6 @@ - [sccs\_tol\_rms](#sccs_tol_rms) - [sccs\_tol\_max](#sccs_tol_max) - [sccs\_surface\_eta](#sccs_surface_eta) - - [eb\_k](#eb_k) - - [tau](#tau) - - [sigma\_k](#sigma_k) - - [nc\_k](#nc_k) - [Quasiatomic Orbital (QO) analysis](#quasiatomic-orbital-qo-analysis) - [qo\_switch](#qo_switch) - [qo\_basis](#qo_basis) @@ -4938,9 +4938,34 @@ ### imp_sol - **Type**: Integer -- **Description**: Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. A charged cell in a dielectric solvent (any water preset, or sccs_preset custom with sccs_epsilon above 1) runs with a warning: the periodic Poisson solver drops the G = 0 component of the net charge, so the energy depends on the cell size. +- **Description**: Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Stress, external fields and other correction models are not supported. - **Default**: 0 +### eb_k + +- **Type**: Real +- **Availability**: *[`imp_sol`](#imp_sol)==1* +- **Description**: The relative permittivity of the bulk solvent, 80 for water +- **Default**: 80 + +### tau + +- **Type**: Real +- **Description**: The effective surface tension parameter that describes the cavitation, the dispersion, and the repulsion interaction between the solute and the solvent which are not captured by the electrostatic terms +- **Default**: 1.0798e-05 + +### sigma_k + +- **Type**: Real +- **Description**: The width of the diffuse cavity that is implicitly determined by the electronic structure of the solute +- **Default**: 0.6 + +### nc_k + +- **Type**: Real +- **Description**: The value of the electron density at which the dielectric cavity forms +- **Default**: 0.00037 + ### sccs_preset - **Type**: String @@ -5018,31 +5043,6 @@ - **Default**: 1e-8 - **Unit**: bohr^-1 -### eb_k - -- **Type**: Real -- **Availability**: *[`imp_sol`](#imp_sol)==1* -- **Description**: The relative permittivity of the bulk solvent, 80 for water -- **Default**: 80 - -### tau - -- **Type**: Real -- **Description**: The effective surface tension parameter that describes the cavitation, the dispersion, and the repulsion interaction between the solute and the solvent which are not captured by the electrostatic terms -- **Default**: 1.0798e-05 - -### sigma_k - -- **Type**: Real -- **Description**: The width of the diffuse cavity that is implicitly determined by the electronic structure of the solute -- **Default**: 0.6 - -### nc_k - -- **Type**: Real -- **Description**: The value of the electron density at which the dielectric cavity forms -- **Default**: 0.00037 - [back to top](#full-list-of-input-keywords) ## Quasiatomic Orbital (QO) analysis diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 7e2537d4884..dfc30eddd93 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -4355,10 +4355,42 @@ parameters: category: Implicit solvation model type: Integer description: | - Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. A charged cell in a dielectric solvent (any water preset, or sccs_preset custom with sccs_epsilon above 1) runs with a warning: the periodic Poisson solver drops the G = 0 component of the net charge, so the energy depends on the cell size. + Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Stress, external fields and other correction models are not supported. default_value: "0" unit: "" availability: "" + - name: eb_k + category: Implicit solvation model + type: Real + description: | + The relative permittivity of the bulk solvent, 80 for water + default_value: "80" + unit: "" + availability: imp_sol==1 + - name: tau + category: Implicit solvation model + type: Real + description: | + The effective surface tension parameter that describes the cavitation, the dispersion, and the repulsion interaction between the solute and the solvent which are not captured by the electrostatic terms + default_value: "1.0798e-05" + unit: "" + availability: "" + - name: sigma_k + category: Implicit solvation model + type: Real + description: | + The width of the diffuse cavity that is implicitly determined by the electronic structure of the solute + default_value: "0.6" + unit: "" + availability: "" + - name: nc_k + category: Implicit solvation model + type: Real + description: | + The value of the electron density at which the dielectric cavity forms + default_value: "0.00037" + unit: "" + availability: "" - name: sccs_preset category: Implicit solvation model type: String @@ -4439,38 +4471,6 @@ parameters: default_value: "1e-8" unit: bohr^-1 availability: imp_sol==2 - - name: eb_k - category: Implicit solvation model - type: Real - description: | - The relative permittivity of the bulk solvent, 80 for water - default_value: "80" - unit: "" - availability: imp_sol==1 - - name: tau - category: Implicit solvation model - type: Real - description: | - The effective surface tension parameter that describes the cavitation, the dispersion, and the repulsion interaction between the solute and the solvent which are not captured by the electrostatic terms - default_value: "1.0798e-05" - unit: "" - availability: "" - - name: sigma_k - category: Implicit solvation model - type: Real - description: | - The width of the diffuse cavity that is implicitly determined by the electronic structure of the solute - default_value: "0.6" - unit: "" - availability: "" - - name: nc_k - category: Implicit solvation model - type: Real - description: | - The value of the electron density at which the dielectric cavity forms - default_value: "0.00037" - unit: "" - availability: "" - name: vdw_method category: vdW correction type: String diff --git a/source/source_io/module_parameter/read_inp_model.cpp b/source/source_io/module_parameter/read_inp_model.cpp index 4a60a73ce91..b9976ecacf6 100644 --- a/source/source_io/module_parameter/read_inp_model.cpp +++ b/source/source_io/module_parameter/read_inp_model.cpp @@ -2,6 +2,7 @@ #include "source_base/tool_quit.h" #include "read_input.h" #include "read_input_tool.h" +#include "read_inp_sccs.h" namespace ModuleIO { void ReadInput::item_model() @@ -181,8 +182,27 @@ void ReadInput::item_model() this->add_item(item); } - // Implicit solvation: preserve the existing model parameter ordering. - this->item_sccs(); + // Implicit solvation: imp_sol, the original model, then SCCS. + { + Input_Item item("imp_sol"); + item.annotation = "implicit solvent model"; + item.category = "Implicit solvation model"; + item.type = "Integer"; + item.description = "Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Stress, external fields and other correction models are not supported."; + item.default_value = "0"; + item.read_value = [](const Input_Item& item, Parameter& para) { + std::string error; + const bool valid = parse_solvation_model(item.str_values[0], para.input.imp_sol, error); + if (!valid) { ModuleBase::WARNING_QUIT("ReadInput", error); } + }; + sync_int(input.imp_sol); + item.check_value = [](const Input_Item&, const Parameter& para) { + std::string error; + const bool valid = validate_sccs_input(para.input, error); + if (!valid) { ModuleBase::WARNING_QUIT("ReadInput", error); } + }; + this->add_item(item); + } { Input_Item item("eb_k"); item.annotation = "the relative permittivity of the bulk solvent"; @@ -228,6 +248,7 @@ void ReadInput::item_model() read_sync_double(input.nc_k); this->add_item(item); } + this->item_sccs(); // vdW Correction { diff --git a/source/source_io/module_parameter/read_inp_sccs.cpp b/source/source_io/module_parameter/read_inp_sccs.cpp index 1fb6e36f0d2..1d651c177fe 100644 --- a/source/source_io/module_parameter/read_inp_sccs.cpp +++ b/source/source_io/module_parameter/read_inp_sccs.cpp @@ -96,26 +96,6 @@ bool validate_sccs_input(const Input_para& input, std::string& error) void ReadInput::item_sccs() { - { - Input_Item item("imp_sol"); - item.annotation = "implicit solvent model"; - item.category = "Implicit solvation model"; - item.type = "Integer"; - item.description = "Select 0 for vacuum, 1 for the original ABACUS implicit solvation model, or 2 for SCCS. Legacy Boolean values remain accepted as 0 or 1. SCCS currently supports periodic CPU KS-DFT SCF calculations with basis_type pw or lcao and nspin 1 or 2, without stress, external fields or other correction models. A charged cell in a dielectric solvent (any water preset, or sccs_preset custom with sccs_epsilon above 1) runs with a warning: the periodic Poisson solver drops the G = 0 component of the net charge, so the energy depends on the cell size."; - item.default_value = "0"; - item.read_value = [](const Input_Item& item, Parameter& para) { - std::string error; - const bool valid = parse_solvation_model(item.str_values[0], para.input.imp_sol, error); - if (!valid) { ModuleBase::WARNING_QUIT("ReadInput", error); } - }; - sync_int(input.imp_sol); - item.check_value = [](const Input_Item&, const Parameter& para) { - std::string error; - const bool valid = validate_sccs_input(para.input, error); - if (!valid) { ModuleBase::WARNING_QUIT("ReadInput", error); } - }; - this->add_item(item); - } { Input_Item item("sccs_preset"); item.annotation = "SCCS parameter preset"; From 1f6cdad40d24e4cc060ab57853e6f204c4ab26d8 Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Thu, 8 Oct 2026 00:37:55 -0600 Subject: [PATCH 39/41] test(pcc): merge overlapping PCC unit tests The exact monopole and dipole values are implied by the isolated-energy tests, and the bilinear kernel is an internal step of pcc_0d_energy. The finite-difference checks of each kernel, the orthogonal and slab geometry checks, the PotPcc input rejections and the charge-moment checks now share one test each. Every property stays checked; 31 tests become 18. --- .../source_cell/test/test_cell_geometry.cpp | 57 +++++------ .../test/test_charge_moments.cpp | 6 +- source/source_estate/test/test_pcc_0d.cpp | 96 +++++-------------- source/source_estate/test/test_pcc_2d.cpp | 79 +++++---------- source/source_estate/test/test_pot_pcc.cpp | 50 +++++----- 5 files changed, 97 insertions(+), 191 deletions(-) diff --git a/source/source_cell/test/test_cell_geometry.cpp b/source/source_cell/test/test_cell_geometry.cpp index 07aca405f8d..652f8bd57f1 100644 --- a/source/source_cell/test/test_cell_geometry.cpp +++ b/source/source_cell/test/test_cell_geometry.cpp @@ -30,7 +30,7 @@ void expect_vector(const ModuleBase::Vector3& actual, } } // namespace -TEST_F(CellGeometryTest, BuildsRotatedRectangularCell) +TEST_F(CellGeometryTest, BuildsRotatedRectangularCellWrapsImagesAndRejectsSkew) { const ModuleBase::Matrix3 lattice(0.0, 1.0, 0.0, -2.0, 0.0, 0.0, @@ -47,30 +47,34 @@ TEST_F(CellGeometryTest, BuildsRotatedRectangularCell) const ModuleBase::Vector3 actual = unitcell::relative_position(position, cell); const ModuleBase::Vector3 expected(-9.0, -4.0, -14.0); expect_vector(actual, expected); -} -TEST_F(CellGeometryTest, WrapsMultipleImagesAndUsesHalfOpenInterval) -{ + // Several images away from the origin of a 10-Bohr cube, on the half-open interval. + const ModuleBase::Matrix3 cube(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + ASSERT_TRUE(unitcell::make_orthogonal_cell(cube, 10.0, 1.0e-10, cell)); cell.origin = ModuleBase::Vector3(0.0, 0.0, 0.0); - const ModuleBase::Vector3 position(25.0, -25.0, 31.0); - const ModuleBase::Vector3 actual = unitcell::relative_position(position, cell); - const ModuleBase::Vector3 expected(-5.0, -5.0, 1.0); - expect_vector(actual, expected); + const ModuleBase::Vector3 far_position(25.0, -25.0, 31.0); + const ModuleBase::Vector3 far_actual = unitcell::relative_position(far_position, cell); + const ModuleBase::Vector3 wrapped(-5.0, -5.0, 1.0); + expect_vector(far_actual, wrapped); + + const ModuleBase::Matrix3 skew(1.0, 0.0, 0.0, + 0.1, 1.0, 0.0, + 0.0, 0.0, 1.0); + EXPECT_FALSE(unitcell::make_orthogonal_cell(skew, 10.0, 1.0e-10, cell)); } -TEST_F(CellGeometryTest, UnwrapsWeightedCenterAcrossCellBoundary) +TEST_F(CellGeometryTest, WeightedCenterUnwrapsAcrossTheBoundaryAndFollowsTranslations) { - const std::vector> positions = { + const std::vector> boundary_positions = { ModuleBase::Vector3(9.8, 9.7, 9.6), ModuleBase::Vector3(0.2, 0.3, 0.4)}; - const std::vector weights = {1.0, 3.0}; - const ModuleBase::Vector3 center = unitcell::weighted_center(positions, weights, cell); - const ModuleBase::Vector3 expected(0.1, 0.15, 0.2); - expect_vector(center, expected); -} + const std::vector boundary_weights = {1.0, 3.0}; + const ModuleBase::Vector3 center = unitcell::weighted_center(boundary_positions, boundary_weights, cell); + const ModuleBase::Vector3 expected_center(0.1, 0.15, 0.2); + expect_vector(center, expected_center); -TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) -{ const std::vector> original = { ModuleBase::Vector3(9.8, 9.6, 0.1), ModuleBase::Vector3(0.3, 0.4, 9.7)}; @@ -98,15 +102,7 @@ TEST_F(CellGeometryTest, PreservesRelativePositionsUnderWrappedTranslation) } } -TEST_F(CellGeometryTest, RejectsSkewCell) -{ - const ModuleBase::Matrix3 skew(1.0, 0.0, 0.0, - 0.1, 1.0, 0.0, - 0.0, 0.0, 1.0); - EXPECT_FALSE(unitcell::make_orthogonal_cell(skew, 10.0, 1.0e-10, cell)); -} - -TEST(SlabGeometry, SupportsSkewPeriodicPlaneAndRotatedNormal) +TEST(SlabGeometry, SupportsSkewPlanesRotatedNormalsAndEveryAxisAndRejectsTilt) { const ModuleBase::Matrix3 lattice(0.0, 2.0, 0.0, 0.0, 1.0, 3.0, @@ -124,18 +120,14 @@ TEST(SlabGeometry, SupportsSkewPeriodicPlaneAndRotatedNormal) const std::vector weights = {1.0, 3.0}; const double center = unitcell::weighted_center(positions, weights, cell); EXPECT_NEAR(center, 0.1, 1.0e-14); -} -TEST(SlabGeometry, SelectsAllAxesAndRejectsTilt) -{ - const ModuleBase::Matrix3 lattice(2.0, 0.0, 0.0, + const ModuleBase::Matrix3 rectangular(2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 8.0); const double lengths[3] = {2.0, 3.0, 8.0}; - unitcell::SlabCell cell; for (int axis = 0; axis < 3; ++axis) { - ASSERT_TRUE(unitcell::make_slab_cell(lattice, 1.0, axis, 1.0e-6, cell)); + ASSERT_TRUE(unitcell::make_slab_cell(rectangular, 1.0, axis, 1.0e-6, cell)); EXPECT_DOUBLE_EQ(cell.length, lengths[axis]); const double expected_area = 48.0 / lengths[axis]; EXPECT_DOUBLE_EQ(cell.area, expected_area); @@ -145,3 +137,4 @@ TEST(SlabGeometry, SelectsAllAxesAndRejectsTilt) 0.1, 0.0, 8.0); EXPECT_FALSE(unitcell::make_slab_cell(tilted, 1.0, 2, 1.0e-6, cell)); } + diff --git a/source/source_estate/test/test_charge_moments.cpp b/source/source_estate/test/test_charge_moments.cpp index 6c4bb5ac8f0..77818118b93 100644 --- a/source/source_estate/test/test_charge_moments.cpp +++ b/source/source_estate/test/test_charge_moments.cpp @@ -2,7 +2,7 @@ #include -TEST(ChargeMoments, IntegratesSignedDensityWithVolumeElement) +TEST(ChargeMoments, IntegratesAddsAndAcceptsAnEmptyLocalGrid) { const double charges[2] = {2.0, -1.0}; const ModuleBase::Vector3 positions[2] = { @@ -14,10 +14,7 @@ TEST(ChargeMoments, IntegratesSignedDensityWithVolumeElement) EXPECT_DOUBLE_EQ(moments.dipole.y, -1.0); EXPECT_DOUBLE_EQ(moments.dipole.z, 0.0); EXPECT_DOUBLE_EQ(moments.second_moment, -1.0); -} -TEST(ChargeMoments, AddsIndependentContributionsAndAcceptsEmptyLocalGrid) -{ elecstate::ChargeMoments ions; ions.charge = 2.0; ions.dipole.x = 3.0; @@ -36,3 +33,4 @@ TEST(ChargeMoments, AddsIndependentContributionsAndAcceptsEmptyLocalGrid) EXPECT_DOUBLE_EQ(electrons.charge, 0.0); EXPECT_DOUBLE_EQ(electrons.second_moment, 0.0); } + diff --git a/source/source_estate/test/test_pcc_0d.cpp b/source/source_estate/test/test_pcc_0d.cpp index cd65bfcb653..ac73b764eb7 100644 --- a/source/source_estate/test/test_pcc_0d.cpp +++ b/source/source_estate/test/test_pcc_0d.cpp @@ -80,91 +80,39 @@ TEST_F(Pcc0dTest, SeparatesCubicRestrictionFromOrthogonalGeometry) EXPECT_FALSE(elecstate::make_pcc_0d_parameters(cell, 1.0e-10, parameters)); } -TEST_F(Pcc0dTest, MatchesAnalyticalMonopoleAndNeutralDipoleEnergy) -{ - elecstate::ChargeMoments monopole; - monopole.charge = 2.0; - const double actual_monopole = elecstate::pcc_0d_energy(monopole, parameters); - const double expected_monopole = 0.5 * parameters.madelung * 4.0 / parameters.length; - EXPECT_NEAR(actual_monopole, expected_monopole, 1.0e-14); - - elecstate::ChargeMoments neutral; - neutral.dipole = ModuleBase::Vector3(1.0, 2.0, 3.0); - const double actual_neutral = elecstate::pcc_0d_energy(neutral, parameters); - const double expected_neutral = 2.0 * ModuleBase::PI * 14.0 / 3000.0; - EXPECT_NEAR(actual_neutral, expected_neutral, 1.0e-14); -} - -TEST_F(Pcc0dTest, BilinearKernelIsSymmetricAndHasConsistentSelfEnergy) -{ - elecstate::ChargeMoments other; - other.charge = -0.4; - other.dipole = ModuleBase::Vector3(0.7, 0.5, -0.1); - other.second_moment = 1.3; - const double forward = elecstate::pcc_0d_bilinear_energy(moments, other, parameters); - const double reverse = elecstate::pcc_0d_bilinear_energy(other, moments, parameters); - EXPECT_NEAR(forward, reverse, 1.0e-14); - const elecstate::ChargeMoments sum = elecstate::add_charge_moments(moments, other); - const double sum_energy = elecstate::pcc_0d_energy(sum, parameters); - const double left_energy = elecstate::pcc_0d_energy(moments, parameters); - const double right_energy = elecstate::pcc_0d_energy(other, parameters); - const double expected = left_energy + right_energy + forward; - EXPECT_NEAR(sum_energy, expected, 1.0e-14); -} - -TEST_F(Pcc0dTest, GradientMatchesPotentialFiniteDifferences) +// The gradient, the ionic force and the electron potential are derivatives of +// the potential and of the energy with respect to a point charge. +TEST_F(Pcc0dTest, DerivativesMatchFiniteDifferences) { + const double step = 1.0e-5; const ModuleBase::Vector3 position(0.4, -0.5, 0.6); const ModuleBase::Vector3 gradient = elecstate::pcc_0d_gradient(moments, position, parameters); - const double expected[3] = {gradient.x, gradient.y, gradient.z}; - const double step = 1.0e-5; + const double charges[2] = {1.3, -0.4}; + ModuleBase::Vector3 positions[2] = {position, ModuleBase::Vector3(-0.5, 0.6, -0.7)}; + const elecstate::ChargeMoments ions = elecstate::charge_moments(charges, positions, 2, 1.0); + const ModuleBase::Vector3 force = elecstate::pcc_0d_force(ions, charges[0], position, parameters); for (int axis = 0; axis < 3; ++axis) { ModuleBase::Vector3 plus = position; ModuleBase::Vector3 minus = position; - double* plus_component[3] = {&plus.x, &plus.y, &plus.z}; - double* minus_component[3] = {&minus.x, &minus.y, &minus.z}; - *plus_component[axis] += step; - *minus_component[axis] -= step; + plus[axis] += step; + minus[axis] -= step; const double plus_potential = elecstate::pcc_0d_potential(moments, plus, parameters); const double minus_potential = elecstate::pcc_0d_potential(moments, minus, parameters); - const double derivative = (plus_potential - minus_potential) / (2.0 * step); - EXPECT_NEAR(derivative, expected[axis], 1.0e-10); - } -} - -TEST_F(Pcc0dTest, IonicForceMatchesTotalEnergyFiniteDifferences) -{ - const double charges[2] = {1.3, -0.4}; - ModuleBase::Vector3 positions[2] = { - ModuleBase::Vector3(0.2, -0.3, 0.4), - ModuleBase::Vector3(-0.5, 0.6, -0.7)}; - moments = elecstate::charge_moments(charges, positions, 2, 1.0); - const ModuleBase::Vector3 force = elecstate::pcc_0d_force(moments, charges[0], positions[0], parameters); - const double expected[3] = {force.x, force.y, force.z}; - const double step = 1.0e-5; - for (int axis = 0; axis < 3; ++axis) - { - double* component[3] = {&positions[0].x, &positions[0].y, &positions[0].z}; - const double original = *component[axis]; - *component[axis] = original + step; - moments = elecstate::charge_moments(charges, positions, 2, 1.0); - const double plus_energy = elecstate::pcc_0d_energy(moments, parameters); - *component[axis] = original - step; - moments = elecstate::charge_moments(charges, positions, 2, 1.0); - const double minus_energy = elecstate::pcc_0d_energy(moments, parameters); - *component[axis] = original; + const double potential_slope = (plus_potential - minus_potential) / (2.0 * step); + EXPECT_NEAR(potential_slope, gradient[axis], 1.0e-10); + positions[0] = plus; + const elecstate::ChargeMoments plus_ions = elecstate::charge_moments(charges, positions, 2, 1.0); + positions[0] = minus; + const elecstate::ChargeMoments minus_ions = elecstate::charge_moments(charges, positions, 2, 1.0); + const double plus_energy = elecstate::pcc_0d_energy(plus_ions, parameters); + const double minus_energy = elecstate::pcc_0d_energy(minus_ions, parameters); const double numerical_force = -(plus_energy - minus_energy) / (2.0 * step); - EXPECT_NEAR(numerical_force, expected[axis], 1.0e-10); + EXPECT_NEAR(numerical_force, force[axis], 1.0e-10); } -} - -TEST_F(Pcc0dTest, ElectronPotentialMatchesEnergyDerivative) -{ - const ModuleBase::Vector3 position(0.2, -0.3, 0.4); + // Adding electrons -step at position changes the energy by the electron potential (Ry). const double positive_potential = elecstate::pcc_0d_potential(moments, position, parameters); const double electron_potential_ry = -2.0 * positive_potential; - const double step = 1.0e-5; elecstate::ChargeMoments plus = moments; plus.charge -= step; plus.dipole -= position * step; @@ -175,8 +123,8 @@ TEST_F(Pcc0dTest, ElectronPotentialMatchesEnergyDerivative) minus.second_moment += step * position.norm2(); const double plus_energy = 2.0 * elecstate::pcc_0d_energy(plus, parameters); const double minus_energy = 2.0 * elecstate::pcc_0d_energy(minus, parameters); - const double derivative = (plus_energy - minus_energy) / (2.0 * step); - EXPECT_NEAR(derivative, electron_potential_ry, 1.0e-10); + const double energy_slope = (plus_energy - minus_energy) / (2.0 * step); + EXPECT_NEAR(energy_slope, electron_potential_ry, 1.0e-10); } TEST_F(Pcc0dTest, EnergyIsIndependentOfMultipoleOrigin) diff --git a/source/source_estate/test/test_pcc_2d.cpp b/source/source_estate/test/test_pcc_2d.cpp index 1314bb7440b..0065d5aefac 100644 --- a/source/source_estate/test/test_pcc_2d.cpp +++ b/source/source_estate/test/test_pcc_2d.cpp @@ -37,77 +37,50 @@ double periodic_sheet_energy(const double area, } } // namespace -TEST(Pcc2d, MatchesChargedSheetGaugeAndNeutralDipoleEnergy) -{ - unitcell::SlabCell cell; - cell.area = 6.0; - cell.length = 8.0; - const elecstate::Pcc2dParameters parameters = elecstate::make_pcc_2d_parameters(cell); - elecstate::ChargeMoments moments; - moments.charge = 2.0; - const double charged = elecstate::pcc_2d_energy(moments, parameters); - const double expected_charged = -ModuleBase::PI * 8.0 * 4.0 / 36.0; - EXPECT_NEAR(charged, expected_charged, 1.0e-14); - moments.charge = 0.0; - moments.dipole.x = 2.0; - const double neutral = elecstate::pcc_2d_energy(moments, parameters); - const double expected_neutral = 2.0 * ModuleBase::PI * 4.0 / 48.0; - EXPECT_NEAR(neutral, expected_neutral, 1.0e-14); -} - -TEST(Pcc2d, NormalForceMatchesEnergyDerivativeAndHasNoTangentialComponent) +// The ionic force is normal and the electron potential (Ry) is the energy +// derivative with respect to a point charge. +TEST(Pcc2d, ForceAndElectronPotentialMatchEnergyDerivatives) { elecstate::Pcc2dParameters parameters; parameters.area = 6.0; parameters.length = 8.0; + const double step = 1.0e-5; const ModuleBase::Vector3 normal(0.0, 1.0, 0.0); const double charges[2] = {1.3, -0.4}; ModuleBase::Vector3 positions[2] = { ModuleBase::Vector3(0.0, 0.2, 0.0), ModuleBase::Vector3(0.0, -0.5, 0.0)}; - elecstate::ChargeMoments moments; - moments = elecstate::charge_moments(charges, positions, 2, 1.0); + const elecstate::ChargeMoments moments = elecstate::charge_moments(charges, positions, 2, 1.0); const double coordinate = positions[0].y; - const ModuleBase::Vector3 force = elecstate::pcc_2d_force(moments, charges[0], coordinate, normal, parameters); + const ModuleBase::Vector3 force + = elecstate::pcc_2d_force(moments, charges[0], coordinate, normal, parameters); EXPECT_DOUBLE_EQ(force.x, 0.0); EXPECT_DOUBLE_EQ(force.z, 0.0); - const double step = 1.0e-5; positions[0].y = coordinate + step; - moments = elecstate::charge_moments(charges, positions, 2, 1.0); - const double plus = elecstate::pcc_2d_energy(moments, parameters); + const elecstate::ChargeMoments plus_moments = elecstate::charge_moments(charges, positions, 2, 1.0); positions[0].y = coordinate - step; - moments = elecstate::charge_moments(charges, positions, 2, 1.0); - const double minus = elecstate::pcc_2d_energy(moments, parameters); + const elecstate::ChargeMoments minus_moments = elecstate::charge_moments(charges, positions, 2, 1.0); + const double plus = elecstate::pcc_2d_energy(plus_moments, parameters); + const double minus = elecstate::pcc_2d_energy(minus_moments, parameters); const double numerical_force = -(plus - minus) / (2.0 * step); EXPECT_NEAR(force.y, numerical_force, 1.0e-10); -} -TEST(Pcc2d, ElectronicPotentialMatchesEnergyDerivative) -{ - elecstate::Pcc2dParameters parameters; - parameters.area = 6.0; - parameters.length = 8.0; - const ModuleBase::Vector3 normal(1.0, 0.0, 0.0); - const double coordinate = 0.3; - elecstate::ChargeMoments moments; - moments.charge = 1.2; - moments.dipole.x = 0.2; - moments.second_moment = 0.8; const double positive_potential = elecstate::pcc_2d_potential(moments, coordinate, normal, parameters); - const double expected = -2.0 * positive_potential; - const double step = 1.0e-5; - elecstate::ChargeMoments plus = moments; - plus.charge -= step; - plus.dipole.x -= coordinate * step; - plus.second_moment -= coordinate * coordinate * step; - elecstate::ChargeMoments minus = moments; - minus.charge += step; - minus.dipole.x += coordinate * step; - minus.second_moment += coordinate * coordinate * step; - const double plus_energy = 2.0 * elecstate::pcc_2d_energy(plus, parameters); - const double minus_energy = 2.0 * elecstate::pcc_2d_energy(minus, parameters); - const double derivative = (plus_energy - minus_energy) / (2.0 * step); - EXPECT_NEAR(derivative, expected, 1.0e-10); + const double potential_ry = -2.0 * positive_potential; + const ModuleBase::Vector3 dipole_step = normal * (coordinate * step); + const double second_step = coordinate * coordinate * step; + elecstate::ChargeMoments removed = moments; + removed.charge -= step; + removed.dipole -= dipole_step; + removed.second_moment -= second_step; + elecstate::ChargeMoments added = moments; + added.charge += step; + added.dipole += dipole_step; + added.second_moment += second_step; + const double removed_energy = 2.0 * elecstate::pcc_2d_energy(removed, parameters); + const double added_energy = 2.0 * elecstate::pcc_2d_energy(added, parameters); + const double energy_slope = (removed_energy - added_energy) / (2.0 * step); + EXPECT_NEAR(energy_slope, potential_ry, 1.0e-10); } // Physical check of the charged-slab gauge: a charged Gaussian layer in cells diff --git a/source/source_estate/test/test_pot_pcc.cpp b/source/source_estate/test/test_pot_pcc.cpp index b4d5d0e49ae..f4cadd7819c 100644 --- a/source/source_estate/test/test_pot_pcc.cpp +++ b/source/source_estate/test/test_pot_pcc.cpp @@ -81,26 +81,7 @@ TEST_F(PotPccTest, NeutralDipoleProducesConsistentEnergyPotentialAndForce) EXPECT_NEAR(force(0, 2), expected_force, 1.0e-14); } -TEST_F(PotPccTest, TwoSpinChannelsShareCorrectionAndAddToExistingPotential) -{ - std::vector down(8, 0.0); - density[7] = 0.3 / 125.0; - down[7] = 0.7 / 125.0; - channels[1] = down.data(); - charge.nspin = 2; - elecstate::PotPcc correction(&basis, 1.0); - ModuleBase::matrix potential(2, 8); - potential(0, 7) = 2.0; - potential(1, 7) = 3.0; - correction.cal_v_eff(&charge, &cell, potential); - const double energy = correction.get_energy(); - const double first_expected = 2.0 + energy; - const double second_expected = 3.0 + energy; - EXPECT_NEAR(potential(0, 7), first_expected, 1.0e-14); - EXPECT_NEAR(potential(1, 7), second_expected, 1.0e-14); -} - -TEST_F(PotPccTest, RecomputesDensityAndKeepsInstancesIndependent) +TEST_F(PotPccTest, RecomputesDensityKeepsInstancesIndependentAndSharesSpinChannels) { elecstate::PotPcc first(&basis, 1.0); elecstate::PotPcc second(&basis, 1.0); @@ -117,6 +98,23 @@ TEST_F(PotPccTest, RecomputesDensityAndKeepsInstancesIndependent) potential.zero_out(); first.cal_v_eff(&charge, &cell, potential); EXPECT_NEAR(first.get_energy(), charged_energy, 1.0e-14); + + // Both spin channels get the same correction on top of their potentials. + std::vector down(8, 0.0); + density[7] = 0.3 / 125.0; + down[7] = 0.7 / 125.0; + channels[1] = down.data(); + charge.nspin = 2; + elecstate::PotPcc correction(&basis, 1.0); + ModuleBase::matrix spin_potential(2, 8); + spin_potential(0, 7) = 2.0; + spin_potential(1, 7) = 3.0; + correction.cal_v_eff(&charge, &cell, spin_potential); + const double energy = correction.get_energy(); + const double first_expected = 2.0 + energy; + const double second_expected = 3.0 + energy; + EXPECT_NEAR(spin_potential(0, 7), first_expected, 1.0e-14); + EXPECT_NEAR(spin_potential(1, 7), second_expected, 1.0e-14); } TEST_F(PotPccTest, ForceMatchesFixedDensityEnergyDerivativeAfterMovingIons) @@ -161,7 +159,7 @@ TEST_F(PotPccTest, SlabAxisControlsPotentialEnergyForceAndIgnoresInPlaneCoordina } } -TEST_F(PotPccTest, SlabRejectsOpenDirectionKpointsAndSymmetryButAllowsInPlaneTranslations) +TEST_F(PotPccTest, RejectsUnsupportedCellsKpointsAndSymmetry) { const std::vector> points = {ModuleBase::Vector3(0.25, 0.0, 0.0)}; elecstate::PotPcc::validate_kpoints(points, 1, 2); @@ -178,18 +176,14 @@ TEST_F(PotPccTest, SlabRejectsOpenDirectionKpointsAndSymmetryButAllowsInPlaneTra 0.0, 1.0, 0.0, 1.0, 0.0, 0.0); EXPECT_EXIT(correction.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); -} - -TEST_F(PotPccTest, MoleculeRejectsNonCubicAndSlabRejectsTiltedCells) -{ - ModuleBase::matrix potential(1, 8); + cell.symm.nrotk = 0; cell.latvec.e22 = 2.0; elecstate::PotPcc molecule(&basis, 1.0); EXPECT_EXIT(molecule.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); cell.latvec.e22 = 1.0; cell.latvec.e31 = 0.1; - elecstate::PotPcc slab(&basis, elecstate::PotPcc::Dimension::slab, 2, 1.0); - EXPECT_EXIT(slab.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); + elecstate::PotPcc tilted_slab(&basis, elecstate::PotPcc::Dimension::slab, 2, 1.0); + EXPECT_EXIT(tilted_slab.cal_v_eff(&charge, &cell, potential), testing::ExitedWithCode(1), ""); } TEST_F(PotPccTest, ReportsGridChargeMismatchAndUsesGridCharge) From 4d6156261b8ac7594f05d83735de775e49e35bce Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Thu, 8 Oct 2026 00:39:11 -0600 Subject: [PATCH 40/41] test(sccs): merge overlapping SCCS core unit tests The cavity limits, thresholds and vacuum share one test, as do the midpoint values and the finite-difference derivatives; the preset names, the preset values (which fix the unit conversions) and the solver tolerance checks are merged likewise. The custom-preset defaults join the INPUT mapping test, and the solvation-force finite difference becomes a fixture method for every cavity model. --- source/source_estate/test/test_pot_sccs.cpp | 159 +++++++++--------- .../module_sccs/test/test_sccs_cavity.cpp | 75 ++++----- .../module_sccs/test/test_sccs_parameters.cpp | 29 +--- .../module_sccs/test/test_sccs_response.cpp | 21 +-- 4 files changed, 127 insertions(+), 157 deletions(-) diff --git a/source/source_estate/test/test_pot_sccs.cpp b/source/source_estate/test/test_pot_sccs.cpp index eb23917bd18..4bf0aa26977 100644 --- a/source/source_estate/test/test_pot_sccs.cpp +++ b/source/source_estate/test/test_pot_sccs.cpp @@ -21,7 +21,75 @@ Sep_Cell::~Sep_Cell() noexcept {} Charge::Charge() {} Charge::~Charge() {} -using PotSccsTest = SccsTest::PwTest; +class PotSccsTest : public SccsTest::PwTest +{ +protected: + // Two atoms in a uniform electron density: the solvation force is the + // finite difference of the Rydberg energy and is added once to an existing + // force. + void check_solvation_force(const ModuleSccs::SccsConfig& config, + const ModuleSccs::PolarizationSolverParameters& solver) + { + UnitCell cell; + Atom atom; + cell.lat0 = length; + cell.tpiba = tpiba; + cell.omega = basis.omega; + cell.ntype = 1; + cell.nat = 2; + cell.atoms = &atom; + atom.na = 2; + atom.ncpp.zv = 1.0; + atom.tau = {ModuleBase::Vector3(0.21, 0.32, 0.43), + ModuleBase::Vector3(0.64, 0.51, 0.27)}; + const double density_value = 2.0 / basis.omega; + std::vector density(basis.nrxx, density_value); + double* channels[] = {density.data()}; + Charge charge; + charge.nspin = 1; + charge.rho = channels; + elecstate::PotSccs component(&basis, config, solver, 2.0); + ModuleBase::matrix potential(1, basis.nrxx); + component.cal_v_eff(&charge, &cell, potential); + ModuleBase::matrix force(2, 3); + component.add_solvation_force(cell, force); + const double step = 1e-4; + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) + { + const double original = atom.tau[ia][axis]; + atom.tau[ia][axis] = original + step / length; + potential.zero_out(); + component.cal_v_eff(&charge, &cell, potential); + const double positive = component.get_energy(); + atom.tau[ia][axis] = original - step / length; + potential.zero_out(); + component.cal_v_eff(&charge, &cell, potential); + const double negative = component.get_energy(); + atom.tau[ia][axis] = original; + const double finite_difference = -(positive - negative) / (2.0 * step); + EXPECT_NEAR(force(ia, axis), finite_difference, 1e-8); + } + } + potential.zero_out(); + component.cal_v_eff(&charge, &cell, potential); + ModuleBase::matrix accumulated(2, 3); + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) { accumulated(ia, axis) = 7.0; } + } + component.add_solvation_force(cell, accumulated); + for (int ia = 0; ia < 2; ++ia) + { + for (int axis = 0; axis < 3; ++axis) + { + const double expected_force = 7.0 + force(ia, axis); + EXPECT_NEAR(accumulated(ia, axis), expected_force, 1e-10); + } + } + } +}; TEST_F(PotSccsTest, IndependentInstancesAndSpinChannels) { @@ -79,7 +147,20 @@ TEST_F(PotSccsTest, IndependentInstancesAndSpinChannels) TEST_F(PotSccsTest, InputMapsOntoSccsConfig) { + // The custom defaults follow Environ environ_type input: + // env_static_permittivity 1, rhomin 1e-4, rhomax 5e-3, no surface tension + // or pressure. Input_para input; + ModuleSccs::SccsConfig defaults; + ModuleSccs::PolarizationSolverParameters default_solver; + elecstate::make_sccs_config_from_input(input, defaults, default_solver); + EXPECT_EQ(input.sccs_preset, "custom"); + EXPECT_DOUBLE_EQ(defaults.cavity.epsilon_bulk, 1.0); + EXPECT_DOUBLE_EQ(defaults.cavity.density_min, 1e-4); + EXPECT_DOUBLE_EQ(defaults.cavity.density_max, 5e-3); + EXPECT_DOUBLE_EQ(defaults.surface_tension, 0.0); + EXPECT_DOUBLE_EQ(defaults.pressure, 0.0); + EXPECT_EQ(default_solver.max_iterations, 200); input.sccs_preset = "water-neutral"; input.sccs_epsilon = 2.0; // replaced by the preset input.sccs_maxiter = 42; @@ -94,43 +175,8 @@ TEST_F(PotSccsTest, InputMapsOntoSccsConfig) EXPECT_EQ(solver.max_iterations, 42); } -TEST_F(PotSccsTest, CustomDefaultsFollowEnvironInput) -{ - // Environ environ_type input: env_static_permittivity 1, rhomin 1e-4, - // rhomax 5e-3, no surface tension or pressure. - Input_para input; - ModuleSccs::SccsConfig config; - ModuleSccs::PolarizationSolverParameters solver; - elecstate::make_sccs_config_from_input(input, config, solver); - EXPECT_EQ(input.sccs_preset, "custom"); - EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 1.0); - EXPECT_DOUBLE_EQ(config.cavity.density_min, 1e-4); - EXPECT_DOUBLE_EQ(config.cavity.density_max, 5e-3); - EXPECT_DOUBLE_EQ(config.surface_tension, 0.0); - EXPECT_DOUBLE_EQ(config.pressure, 0.0); - EXPECT_EQ(solver.max_iterations, 200); -} - TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) { - UnitCell cell; - Atom atom; - cell.lat0 = length; - cell.tpiba = tpiba; - cell.omega = basis.omega; - cell.ntype = 1; - cell.nat = 2; - cell.atoms = &atom; - atom.na = 2; - atom.ncpp.zv = 1.0; - atom.tau = {ModuleBase::Vector3(0.21, 0.32, 0.43), - ModuleBase::Vector3(0.64, 0.51, 0.27)}; - const double density_value = 2.0 / basis.omega; - std::vector density(basis.nrxx, density_value); - double* channels[] = {density.data()}; - Charge charge; - charge.nspin = 1; - charge.rho = channels; Input_para input; input.sccs_epsilon = 5.0; input.sccs_tol_rms = 1e-13; @@ -138,46 +184,7 @@ TEST_F(PotSccsTest, IonicForceAddsRydbergDerivativeOnce) ModuleSccs::SccsConfig config; ModuleSccs::PolarizationSolverParameters solver; elecstate::make_sccs_config_from_input(input, config, solver); - elecstate::PotSccs component(&basis, config, solver, 2.0); - ModuleBase::matrix potential(1, basis.nrxx); - component.cal_v_eff(&charge, &cell, potential); - ModuleBase::matrix force(2, 3); - component.add_solvation_force(cell, force); - const double step = 1e-4; - for (int ia = 0; ia < 2; ++ia) - { - for (int axis = 0; axis < 3; ++axis) - { - const double original = atom.tau[ia][axis]; - atom.tau[ia][axis] = original + step / length; - potential.zero_out(); - component.cal_v_eff(&charge, &cell, potential); - const double positive = component.get_energy(); - atom.tau[ia][axis] = original - step / length; - potential.zero_out(); - component.cal_v_eff(&charge, &cell, potential); - const double negative = component.get_energy(); - atom.tau[ia][axis] = original; - const double finite_difference = -(positive - negative) / (2.0 * step); - EXPECT_NEAR(force(ia, axis), finite_difference, 1e-8); - } - } - potential.zero_out(); - component.cal_v_eff(&charge, &cell, potential); - ModuleBase::matrix accumulated(2, 3); - for (int ia = 0; ia < 2; ++ia) - { - for (int axis = 0; axis < 3; ++axis) { accumulated(ia, axis) = 7.0; } - } - component.add_solvation_force(cell, accumulated); - for (int ia = 0; ia < 2; ++ia) - { - for (int axis = 0; axis < 3; ++axis) - { - const double expected_force = 7.0 + force(ia, axis); - EXPECT_NEAR(accumulated(ia, axis), expected_force, 1e-10); - } - } + check_solvation_force(config, solver); } TEST_F(PotSccsTest, ChargedDielectricCellWarnsAndRuns) diff --git a/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp b/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp index 108296255f6..c4d8fc15c4d 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_cavity.cpp @@ -18,7 +18,9 @@ class SccsCavityTest : public ::testing::Test } }; -TEST_F(SccsCavityTest, BulkAndSoluteLimits) +// Bulk and solute limits, continuity at both thresholds, and no dielectric +// response in vacuum. +TEST_F(SccsCavityTest, LimitsThresholdsAndVacuum) { const double bulk_densities[] = {-1e-6, 0.0, 1e-4}; for (double density : bulk_densities) @@ -40,30 +42,47 @@ TEST_F(SccsCavityTest, BulkAndSoluteLimits) EXPECT_DOUBLE_EQ(point.dsolute_drho, 0.0); EXPECT_DOUBLE_EQ(point.depsilon_drho, 0.0); } + const double near_min = parameters.density_min * (1.0 + 1e-8); + const double near_max = parameters.density_max * (1.0 - 1e-8); + ModuleSccs::CavityPoint lower; + ModuleSccs::CavityPoint upper; + lower = ModuleSccs::evaluate_cavity(near_min, parameters); + upper = ModuleSccs::evaluate_cavity(near_max, parameters); + EXPECT_NEAR(lower.solute, 0.0, 1e-14); + EXPECT_NEAR(upper.solute, 1.0, 1e-14); + EXPECT_NEAR(lower.epsilon, 78.3, 1e-12); + EXPECT_NEAR(upper.epsilon, 1.0, 1e-12); + EXPECT_NEAR(lower.dsolute_drho, 0.0, 1e-10); + EXPECT_NEAR(upper.dsolute_drho, 0.0, 1e-10); + EXPECT_NEAR(lower.depsilon_drho, 0.0, 1e-8); + EXPECT_NEAR(upper.depsilon_drho, 0.0, 1e-8); + parameters.epsilon_bulk = 1.0; + const ModuleSccs::CavityPoint vacuum = ModuleSccs::evaluate_cavity(1e-3, parameters); + EXPECT_GT(vacuum.solute, 0.0); + EXPECT_LT(vacuum.solute, 1.0); + EXPECT_DOUBLE_EQ(vacuum.epsilon, 1.0); + EXPECT_DOUBLE_EQ(vacuum.depsilon_drho, 0.0); } -TEST_F(SccsCavityTest, LogarithmicMidpoint) +// Analytic values at the logarithmic midpoint and finite differences across +// the transition. +TEST_F(SccsCavityTest, MidpointAndDerivativesAcrossTransition) { const double product = parameters.density_min * parameters.density_max; - const double density = std::sqrt(product); + const double midpoint_density = std::sqrt(product); const double ratio = parameters.density_max / parameters.density_min; const double width = std::log(ratio); const double epsilon = std::sqrt(parameters.epsilon_bulk); const double log_epsilon = std::log(parameters.epsilon_bulk); - const double dsolute = 2.0 / (width * density); + const double dsolute = 2.0 / (width * midpoint_density); const double depsilon = -epsilon * log_epsilon * dsolute; - ModuleSccs::CavityPoint point; - point = ModuleSccs::evaluate_cavity(density, parameters); - EXPECT_NEAR(point.solute, 0.5, 1e-14); - EXPECT_NEAR(point.epsilon, epsilon, 1e-13); - EXPECT_NEAR(point.dsolute_drho, dsolute, 1e-10); - EXPECT_NEAR(point.depsilon_drho, depsilon, 1e-9); -} + const ModuleSccs::CavityPoint midpoint = ModuleSccs::evaluate_cavity(midpoint_density, parameters); + EXPECT_NEAR(midpoint.solute, 0.5, 1e-14); + EXPECT_NEAR(midpoint.epsilon, epsilon, 1e-13); + EXPECT_NEAR(midpoint.dsolute_drho, dsolute, 1e-10); + EXPECT_NEAR(midpoint.depsilon_drho, depsilon, 1e-9); -TEST_F(SccsCavityTest, DerivativesAcrossTransition) -{ const double fractions[] = {0.2, 0.5, 0.8}; - const double ratio = parameters.density_max / parameters.density_min; for (double fraction : fractions) { const double scale = std::pow(ratio, fraction); @@ -88,32 +107,4 @@ TEST_F(SccsCavityTest, DerivativesAcrossTransition) } } -TEST_F(SccsCavityTest, SmoothThresholds) -{ - const double near_min = parameters.density_min * (1.0 + 1e-8); - const double near_max = parameters.density_max * (1.0 - 1e-8); - ModuleSccs::CavityPoint lower; - ModuleSccs::CavityPoint upper; - lower = ModuleSccs::evaluate_cavity(near_min, parameters); - upper = ModuleSccs::evaluate_cavity(near_max, parameters); - EXPECT_NEAR(lower.solute, 0.0, 1e-14); - EXPECT_NEAR(upper.solute, 1.0, 1e-14); - EXPECT_NEAR(lower.epsilon, 78.3, 1e-12); - EXPECT_NEAR(upper.epsilon, 1.0, 1e-12); - EXPECT_NEAR(lower.dsolute_drho, 0.0, 1e-10); - EXPECT_NEAR(upper.dsolute_drho, 0.0, 1e-10); - EXPECT_NEAR(lower.depsilon_drho, 0.0, 1e-8); - EXPECT_NEAR(upper.depsilon_drho, 0.0, 1e-8); -} - -TEST_F(SccsCavityTest, VacuumHasNoDielectricResponse) -{ - parameters.epsilon_bulk = 1.0; - ModuleSccs::CavityPoint point; - point = ModuleSccs::evaluate_cavity(1e-3, parameters); - EXPECT_GT(point.solute, 0.0); - EXPECT_LT(point.solute, 1.0); - EXPECT_DOUBLE_EQ(point.epsilon, 1.0); - EXPECT_DOUBLE_EQ(point.depsilon_drho, 0.0); -} } // namespace diff --git a/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp b/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp index 47ac6fc713a..aeedf8f4447 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_parameters.cpp @@ -1,24 +1,22 @@ #include "../sccs_parameters.h" #include -TEST(SccsParameters, PresetNames) +TEST(SccsParameters, ExactPresetNamesOnly) { EXPECT_EQ(ModuleSccs::parse_preset("water-neutral"), ModuleSccs::Preset::WaterNeutral); EXPECT_EQ(ModuleSccs::parse_preset("water-cation"), ModuleSccs::Preset::WaterCation); EXPECT_EQ(ModuleSccs::parse_preset("water-anion"), ModuleSccs::Preset::WaterAnion); EXPECT_EQ(ModuleSccs::parse_preset("vacuum"), ModuleSccs::Preset::Vacuum); EXPECT_EQ(ModuleSccs::parse_preset("custom"), ModuleSccs::Preset::Custom); -} - -TEST(SccsParameters, UnknownPresetStops) -{ testing::internal::CaptureStdout(); EXPECT_EXIT(ModuleSccs::parse_preset("unknown"), ::testing::ExitedWithCode(1), ""); EXPECT_EXIT(ModuleSccs::parse_preset("Water-Neutral"), ::testing::ExitedWithCode(1), ""); testing::internal::GetCapturedStdout(); } -TEST(SccsParameters, OriginalWaterPresets) +// The water presets in Hartree atomic units, which also fixes the dyn/cm and +// GPa conversions, and the vacuum preset. +TEST(SccsParameters, PresetValues) { const ModuleSccs::Preset presets[] = {ModuleSccs::Preset::WaterNeutral, ModuleSccs::Preset::WaterCation, @@ -37,20 +35,9 @@ TEST(SccsParameters, OriginalWaterPresets) EXPECT_NEAR(config.surface_tension, tensions[index], 1e-18); EXPECT_NEAR(config.pressure, pressures[index], 1e-18); } + const ModuleSccs::SccsConfig vacuum = ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum); + EXPECT_DOUBLE_EQ(vacuum.cavity.epsilon_bulk, 1.0); + EXPECT_DOUBLE_EQ(vacuum.surface_tension, 0.0); + EXPECT_DOUBLE_EQ(vacuum.pressure, 0.0); } -TEST(SccsParameters, Vacuum) -{ - const ModuleSccs::SccsConfig config = ModuleSccs::make_sccs_config(ModuleSccs::Preset::Vacuum); - EXPECT_DOUBLE_EQ(config.cavity.epsilon_bulk, 1.0); - EXPECT_DOUBLE_EQ(config.surface_tension, 0.0); - EXPECT_DOUBLE_EQ(config.pressure, 0.0); -} - -TEST(SccsParameters, PhysicalUnitConversions) -{ - const double tension = ModuleSccs::dyn_per_cm_to_hartree_per_bohr2(1.0); - const double pressure = ModuleSccs::gpa_to_hartree_per_bohr3(1.0); - EXPECT_NEAR(tension, 6.423048558616410e-7, 1e-20); - EXPECT_NEAR(pressure, 3.398930921743166e-5, 1e-18); -} diff --git a/source/source_hamilt/module_sccs/test/test_sccs_response.cpp b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp index d7f65667898..1ff9242bc88 100644 --- a/source/source_hamilt/module_sccs/test/test_sccs_response.cpp +++ b/source/source_hamilt/module_sccs/test/test_sccs_response.cpp @@ -75,7 +75,7 @@ TEST_F(SccsResponseTest, ManufacturedNonuniformDielectricSolution) EXPECT_GT(response.polarization.iterations, 1); } -TEST_F(SccsResponseTest, ConvergenceFailureAndExplicitWarmStart) +TEST_F(SccsResponseTest, ConvergenceFailureBothTolerancesAndExplicitWarmStart) { const std::vector mode_x = cosine_mode(0); std::vector charge = cosine_mode(1); @@ -125,26 +125,10 @@ TEST_F(SccsResponseTest, ConvergenceFailureAndExplicitWarmStart) { EXPECT_NEAR(warm.polarization.potential[ir], cold.polarization.potential[ir], 1e-9); } -} -TEST_F(SccsResponseTest, RequiresBothResidualTolerances) -{ - const std::vector mode_x = cosine_mode(0); - std::vector charge = cosine_mode(1); - std::vector density(basis.nrxx); - for (int ir = 0; ir < basis.nrxx; ++ir) - { - density[ir] = 0.009 + 0.008 * mode_x[ir]; - charge[ir] *= 1e-3; - } - ModuleSccs::CavityParameters cavity; - cavity.density_min = 0.0024; - cavity.density_max = 0.0155; - cavity.epsilon_bulk = 78.3; - ModuleSccs::PolarizationSolverParameters solver; + // Each of the two tolerances alone drives the iteration count. solver.tolerance_rms = 1e-5; solver.tolerance_max = 1e-5; - const std::vector cold_start; ModuleSccs::SccsResponse loose; ModuleSccs::SccsResponse tight_maximum; ModuleSccs::SccsResponse tight_rms; @@ -162,3 +146,4 @@ TEST_F(SccsResponseTest, RequiresBothResidualTolerances) EXPECT_GT(tight_maximum.polarization.iterations, loose.polarization.iterations); EXPECT_GT(tight_rms.polarization.iterations, loose.polarization.iterations); } + From d198e1b46a7719112d8f2447d1290cf56c06547f Mon Sep 17 00:00:00 2001 From: LKFEIYI Date: Thu, 8 Oct 2026 19:55:54 -0600 Subject: [PATCH 41/41] test(hsolver): stub the solvation hooks of Potential in the hsolver mock --- .../test/hsolver_supplementary_mock.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/source/source_hsolver/test/hsolver_supplementary_mock.h b/source/source_hsolver/test/hsolver_supplementary_mock.h index 76da6b23eba..e3c52fbd540 100644 --- a/source/source_hsolver/test/hsolver_supplementary_mock.h +++ b/source/source_hsolver/test/hsolver_supplementary_mock.h @@ -86,6 +86,21 @@ void Potential::cal_fixed_v(double* vl_pseudo) { } +void Potential::get_solvation_energy(double& electrostatic, double& non_electrostatic) const +{ + electrostatic = 0.0; + non_electrostatic = 0.0; +} + +void Potential::add_solvation_force(const UnitCell& cell, ModuleBase::matrix& force) const +{ +} + +const std::vector* Potential::solvent_electrostatic_potential() const +{ + return nullptr; +} + } // namespace elecstate #include "source_cell/klist.h"