From e6435c42aedbd28d5ab0669d6337eb8f863b8788 Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 16:01:58 +0800 Subject: [PATCH 01/11] fix(md): keep wrapped coordinates inside the half-open cell --- source/source_md/md_base.cpp | 12 +++- source/source_md/test/CMakeLists.txt | 1 + source/source_md/test/test_md_base.cpp | 82 ++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 source/source_md/test/test_md_base.cpp diff --git a/source/source_md/md_base.cpp b/source/source_md/md_base.cpp index cb00647a865..6146e25117e 100644 --- a/source/source_md/md_base.cpp +++ b/source/source_md/md_base.cpp @@ -109,9 +109,15 @@ void MD_base::update_pos() } pos = pos * mdcell.GT(); atom.frac += pos; - atom.frac.x -= std::floor(atom.frac.x); - atom.frac.y -= std::floor(atom.frac.y); - atom.frac.z -= std::floor(atom.frac.z); + for (int k = 0; k < 3; ++k) + { + atom.frac[k] -= std::floor(atom.frac[k]); + // A tiny negative value can round to exactly 1.0 after adding one. + if (atom.frac[k] >= 1.0) + { + atom.frac[k] = 0.0; + } + } atom.cart = atom.frac * mdcell.latvec(); } diff --git a/source/source_md/test/CMakeLists.txt b/source/source_md/test/CMakeLists.txt index 3392af2467c..e2e8d658ce5 100644 --- a/source/source_md/test/CMakeLists.txt +++ b/source/source_md/test/CMakeLists.txt @@ -147,6 +147,7 @@ addtest( device SOURCES verlet_test.cpp + test_md_base.cpp ../md_base.cpp ../verlet.cpp ${depend_files}) diff --git a/source/source_md/test/test_md_base.cpp b/source/source_md/test/test_md_base.cpp new file mode 100644 index 00000000000..2d0f69cfdb7 --- /dev/null +++ b/source/source_md/test/test_md_base.cpp @@ -0,0 +1,82 @@ +#include "source_md/md_base.h" +#include "source_base/constants.h" +#include "source_base/parallel_cell.h" + +#include + +#include +#include + +namespace +{ +MDCell make_position_test_cell() +{ + ModuleBase::Matrix3 lattice; + lattice.e11 = 1.0; + lattice.e22 = 1.0; + lattice.e33 = 1.0; + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + std::vector atoms(2); + for (int i = 0; i < 2; ++i) + { + atoms[i].type_index = i; + atoms[i].mass = 1.0; + atoms[i].mbl.set(1, 1, 1); + atoms[i].frac.set(0.25, 0.5, 0.75); + atoms[i].cart = atoms[i].frac * lattice; + } + const std::vector labels(1, "X"); + const std::vector masses(1, 1.0); + const std::vector counts(1, 2); + MDCell cell; + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 2, atoms, labels, masses, counts, 0.0, domain); + return cell; +} + +TEST(MdPositionTest, TinyNegativeDisplacementRemainsInsideHalfOpenCell) +{ + MDCell cell = make_position_test_cell(); + MD_para parameters; + parameters.md_dt = ModuleBase::AU_to_FS; + parameters.md_tfirst = 0.0; + parameters.md_tlast = 0.0; + MD_base integrator(parameters, false, true, 0, cell); + LocalAtom& moving = cell.owned_atoms()[0]; + moving.frac.set(0.0, 0.0, 0.0); + moving.cart.set(0.0, 0.0, 0.0); + moving.vel.set(-1.0e-21, -2.0e-21, -3.0e-21); + std::ofstream log; + integrator.first_half(log); + for (int axis = 0; axis < 3; ++axis) + { + EXPECT_GE(moving.frac[axis], 0.0); + EXPECT_LT(moving.frac[axis], 1.0); + EXPECT_DOUBLE_EQ(moving.frac[axis], 0.0); + EXPECT_DOUBLE_EQ(moving.cart[axis], 0.0); + } +} + +TEST(MdPositionTest, FiniteBoundaryCrossingsAndFrozenComponentsKeepTheirPositions) +{ + MDCell cell = make_position_test_cell(); + MD_para parameters; + parameters.md_dt = ModuleBase::AU_to_FS; + parameters.md_tfirst = 0.0; + parameters.md_tlast = 0.0; + MD_base integrator(parameters, false, true, 0, cell); + cell.owned_atoms()[0].vel.set(-0.5, 1.75, -1.25); + cell.owned_atoms()[1].mbl.set(0, 0, 0); + cell.owned_atoms()[1].vel.set(-0.5, 1.75, -1.25); + std::ofstream log; + integrator.first_half(log); + const LocalAtom& moving = cell.owned_atoms()[0]; + EXPECT_DOUBLE_EQ(moving.frac.x, 0.75); + EXPECT_DOUBLE_EQ(moving.frac.y, 0.25); + EXPECT_DOUBLE_EQ(moving.frac.z, 0.5); + const LocalAtom& frozen = cell.owned_atoms()[1]; + EXPECT_DOUBLE_EQ(frozen.frac.x, 0.25); + EXPECT_DOUBLE_EQ(frozen.frac.y, 0.5); + EXPECT_DOUBLE_EQ(frozen.frac.z, 0.75); +} +} // namespace From ef87e89600e966787173e63675835f333b906061 Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 16:14:19 +0800 Subject: [PATCH 02/11] fix(md): preserve global atom order in MD_dump Gather frame text through CommunicationDomain and write rows by the stable type-major index. Emit frame separators once, including when some MPI ranks own no atoms. Add serial-launch and four-rank tests for reversed ownership, an empty rank, empty frames, and append mode. No documentation update needed: this restores trajectory ordering without changing INPUT parameters, units, or columns. --- source/source_base/parallel_cell.cpp | 57 +++++++ source/source_base/parallel_cell.h | 4 + source/source_md/md_func.cpp | 96 ++++++----- source/source_md/test/CMakeLists.txt | 17 ++ source/source_md/test/test_md_func_mpi.cpp | 179 +++++++++++++++++++++ 5 files changed, 310 insertions(+), 43 deletions(-) create mode 100644 source/source_md/test/test_md_func_mpi.cpp diff --git a/source/source_base/parallel_cell.cpp b/source/source_base/parallel_cell.cpp index 1f13e552d4a..6534c190044 100644 --- a/source/source_base/parallel_cell.cpp +++ b/source/source_base/parallel_cell.cpp @@ -1,5 +1,9 @@ #include "source_base/parallel_cell.h" +#include +#include +#include + namespace ModuleBase { #ifdef __MPI @@ -69,6 +73,59 @@ double CommunicationDomain::max(double value) const return value; } +std::string CommunicationDomain::gather_text(const std::string& value) const +{ +#ifdef __MPI + if (communicator_ != MPI_COMM_NULL) + { + const std::size_t maximum_count = std::numeric_limits::max(); + const int local_overflow = value.size() > maximum_count; + if (max(local_overflow) != 0) + { + throw std::length_error("CommunicationDomain text exceeds the MPI count range."); + } + const int local_size = static_cast(value.size()); + const int process_count = size(); + std::vector lengths(process_count, 0); + std::vector offsets(process_count, 0); + MPI_Gather(&local_size, 1, MPI_INT, lengths.data(), 1, MPI_INT, 0, communicator_); + std::size_t total_size = 0; + int total_overflow = 0; + if (rank_ == 0) + { + for (int rank = 0; rank < process_count; ++rank) + { + if (total_size + lengths[rank] > maximum_count) + { + total_overflow = 1; + break; + } + offsets[rank] = static_cast(total_size); + total_size += lengths[rank]; + } + } + MPI_Bcast(&total_overflow, 1, MPI_INT, 0, communicator_); + if (total_overflow != 0) + { + throw std::length_error("CommunicationDomain gathered text exceeds the MPI count range."); + } + std::string gathered; + if (rank_ == 0) + { + gathered.resize(total_size); + } + char* buffer = nullptr; + if (!gathered.empty()) + { + buffer = &gathered[0]; + } + MPI_Gatherv(value.data(), local_size, MPI_CHAR, buffer, lengths.data(), offsets.data(), MPI_CHAR, 0, communicator_); + return gathered; + } +#endif + return value; +} + CommunicationDomain world_comm_domain() { CommunicationDomain comm_domain; diff --git a/source/source_base/parallel_cell.h b/source/source_base/parallel_cell.h index 1bfb7ef2222..5f9fda234cb 100644 --- a/source/source_base/parallel_cell.h +++ b/source/source_base/parallel_cell.h @@ -1,6 +1,8 @@ #ifndef PARALLEL_CELL_H #define PARALLEL_CELL_H +#include + #ifdef __MPI #include #endif @@ -20,6 +22,8 @@ class CommunicationDomain int max(int value) const; int sum(int value) const; double max(double value) const; + /// Concatenate text on rank zero; other ranks receive an empty string. + std::string gather_text(const std::string& value) const; private: #ifdef __MPI diff --git a/source/source_md/md_func.cpp b/source/source_md/md_func.cpp index a549e87732c..3e4a48d40e0 100644 --- a/source/source_md/md_func.cpp +++ b/source/source_md/md_func.cpp @@ -2,34 +2,17 @@ #include "source_cell/domain_decomposition.h" #include "source_base/global_variable.h" +#include "source_base/parallel_cell.h" #include "source_base/timer.h" #include "source_io/module_output/output_log.h" -#include -#include -#include -#include +#include +#include +#include namespace MD_func { -#ifdef __MPI -namespace -{ -bool write_dump_at(const int file, const std::string& data, const MPI_Offset offset) -{ - std::size_t written = 0; - while (written < data.size()) - { - const ssize_t count = pwrite(file, data.data() + written, data.size() - written, - static_cast(offset + written)); - if (count <= 0) return false; - written += static_cast(count); - } - return true; -} -} -#endif double gaussrand() { @@ -455,30 +438,57 @@ void dump_info(const int& step, local << " " << atom.vel.x * unit_vel << " " << atom.vel.y * unit_vel << " " << atom.vel.z * unit_vel; local << "\n"; } - local << "\n\n"; + ModuleBase::CommunicationDomain domain; #ifdef __MPI - const MPI_Comm comm = mdcell.communicator(); - int rank = 0; MPI_Comm_rank(comm, &rank); - MPI_Offset base = 0; - if (rank == 0) - { - const int file_descriptor = open(file.str().c_str(), O_CREAT | O_WRONLY | (step == 0 ? O_TRUNC : O_APPEND), 0666); - if (file_descriptor < 0) ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot open MD_dump."); - base = lseek(file_descriptor, 0, SEEK_END); - if (!write_dump_at(file_descriptor, header.str(), base) || close(file_descriptor) != 0) ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot write MD_dump header."); - } - MPI_Bcast(&base, 1, MPI_OFFSET, 0, comm); - const MPI_Offset atom_offset = base + static_cast(header.str().size()); - const MPI_Offset local_size = static_cast(local.str().size()); - MPI_Offset rank_offset = 0; - MPI_Exscan(&local_size, &rank_offset, 1, MPI_OFFSET, MPI_SUM, comm); - if (rank == 0) rank_offset = 0; - const int file_descriptor = open(file.str().c_str(), O_WRONLY); - if (file_descriptor < 0 || !write_dump_at(file_descriptor, local.str(), atom_offset + rank_offset) || close(file_descriptor) != 0) ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot write MD_dump atoms."); -#else - std::ofstream ofs(file.str().c_str(), step == 0 ? std::ios::trunc : std::ios::app); - ofs << header.str() << local.str(); + domain.initialize(mdcell.communicator()); #endif + const std::string local_text = local.str(); + const std::string gathered = domain.gather_text(local_text); + if (domain.rank() != 0) + { + return; + } + + // Spatial ownership is unrelated to the stable type-major atom index. + std::map rows; + std::istringstream input(gathered); + std::string line; + while (std::getline(input, line)) + { + std::istringstream record(line); + std::int64_t index = 0; + if (!(record >> index) || index < 0 || index >= type_offsets.back()) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "invalid MD_dump atom index."); + } + const bool inserted = rows.emplace(index, line).second; + if (!inserted) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "duplicate MD_dump atom index."); + } + } + if (rows.size() != static_cast(type_offsets.back())) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "missing MD_dump atoms."); + } + const std::string filename = file.str(); + const std::ios_base::openmode mode = step == 0 ? std::ios::trunc : std::ios::app; + std::ofstream ofs(filename.c_str(), mode); + if (!ofs) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot open MD_dump."); + } + ofs << header.str(); + for (const auto& row : rows) + { + ofs << row.second << "\n"; + } + ofs << "\n\n"; + ofs.close(); + if (!ofs) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot write MD_dump."); + } } void get_mass_mbl(const UnitCell& unit_in, diff --git a/source/source_md/test/CMakeLists.txt b/source/source_md/test/CMakeLists.txt index e2e8d658ce5..9b09c7704d2 100644 --- a/source/source_md/test/CMakeLists.txt +++ b/source/source_md/test/CMakeLists.txt @@ -193,3 +193,20 @@ addtest( ../md_base.cpp ../langevin.cpp ${depend_files}) + +if(ENABLE_MPI) + addtest( + TARGET MODULE_MD_dump_mpi + KEEP_FEATURE_DEFINITIONS __MPI + LIBS parameter psi device + SOURCES test_md_func_mpi.cpp ../../source_base/parallel_common.cpp + ../../source_cell/magnetism.cpp ${depend_files}) + add_test( + NAME MODULE_MD_dump_mpi_np4 + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 + $ + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + set_tests_properties( + MODULE_MD_dump_mpi MODULE_MD_dump_mpi_np4 + PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1;MKL_NUM_THREADS=1;I_MPI_PIN=0" TIMEOUT 60) +endif() diff --git a/source/source_md/test/test_md_func_mpi.cpp b/source/source_md/test/test_md_func_mpi.cpp new file mode 100644 index 00000000000..f5dd6c1524b --- /dev/null +++ b/source/source_md/test/test_md_func_mpi.cpp @@ -0,0 +1,179 @@ +#include "source_md/md_func.h" +#include "source_base/parallel_cell.h" +#include "source_base/parallel_common.h" +#include "source_base/parallel_global.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +std::string make_dump_directory(const ModuleBase::CommunicationDomain& domain) +{ + std::string directory; + if (domain.rank() == 0) + { + std::string pattern = testing::TempDir() + "abacus-md-dump-XXXXXX"; + char* created = mkdtemp(&pattern[0]); + if (created != nullptr) + { + directory = created; + directory += "/"; + } + } + Parallel_Common::bcast_string(directory); + return directory; +} + +TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + ModuleBase::Matrix3 lattice; + lattice.e11 = 1.0; + lattice.e22 = 1.0; + lattice.e33 = 1.0; + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const int owner_count = std::min(domain.size(), 3); + std::vector atoms; + for (int index = 3; index >= 0; --index) + { + const int owner = (2 * index + 1) % owner_count; + if (owner != domain.rank()) + { + continue; + } + LocalAtom atom; + atom.type = index / 2; + atom.type_index = index % 2; + const double x = 0.1 * (index + 1); + atom.frac.set(x, 0.5, 0.5); + atom.cart = atom.frac; + atoms.push_back(atom); + } + const std::vector labels = {"A", "B"}; + const std::vector masses(2, 1.0); + const std::vector counts(2, 2); + MDCell cell; + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 4, atoms, labels, masses, counts, 0.0, domain); + const std::string directory = make_dump_directory(domain); + ASSERT_FALSE(directory.empty()); + MD_para parameters; + ModuleBase::matrix virial(3, 3); + MD_func::dump_info(0, directory, cell, parameters, false, virial); + MD_func::dump_info(1, directory, cell, parameters, false, virial); + if (domain.rank() == 0) + { + const std::string filename = directory + "MD_dump"; + std::ifstream input(filename.c_str()); + ASSERT_TRUE(input); + std::string line; + int frame_count = 0; + int atom_count = 0; + bool reading_atoms = false; + while (std::getline(input, line)) + { + if (line.find("MDSTEP:") == 0) + { + if (frame_count > 0) + { + EXPECT_EQ(atom_count, 4); + } + ++frame_count; + atom_count = 0; + reading_atoms = false; + } + else if (line.find("INDEX") == 0) + { + reading_atoms = true; + } + else if (reading_atoms && line.empty()) + { + EXPECT_EQ(atom_count, 4); + reading_atoms = false; + } + else if (reading_atoms) + { + std::istringstream row(line); + int index = -1; + std::string label; + double x = 0.0; + row >> index >> label >> x; + ASSERT_TRUE(row); + EXPECT_EQ(index, atom_count); + const std::string expected_label = atom_count < 2 ? "A" : "B"; + EXPECT_EQ(label, expected_label); + const double expected_x = 0.1 * (atom_count + 1) / ModuleBase::ANGSTROM_AU; + EXPECT_NEAR(x, expected_x, 1.0e-12); + ++atom_count; + } + } + EXPECT_EQ(frame_count, 2); + EXPECT_EQ(atom_count, 4); + input.close(); + std::remove(filename.c_str()); + rmdir(directory.c_str()); + } +} + +TEST(MdDumpMpiTest, EmptyRanksCanWriteAnEmptyFrame) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + ModuleBase::Matrix3 lattice; + lattice.e11 = 1.0; + lattice.e22 = 1.0; + lattice.e33 = 1.0; + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const std::vector atoms; + const std::vector labels(1, "X"); + const std::vector masses(1, 1.0); + const std::vector counts(1, 0); + MDCell cell; + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 0, atoms, labels, masses, counts, 0.0, domain); + const std::string directory = make_dump_directory(domain); + ASSERT_FALSE(directory.empty()); + MD_para parameters; + ModuleBase::matrix virial(3, 3); + MD_func::dump_info(0, directory, cell, parameters, false, virial); + if (domain.rank() == 0) + { + const std::string filename = directory + "MD_dump"; + std::ifstream input(filename.c_str()); + ASSERT_TRUE(input); + const std::string contents((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + EXPECT_NE(contents.find("MDSTEP: 0"), std::string::npos); + EXPECT_NE(contents.find("INDEX"), std::string::npos); + input.close(); + std::remove(filename.c_str()); + rmdir(directory.c_str()); + } +} +} // namespace + +int main(int argc, char** argv) +{ + int process_count = 1; + int thread_count = 1; + int rank = 0; + Parallel_Global::read_pal_param(argc, argv, process_count, thread_count, rank); + // These application communicators are not created by this focused test. + POOL_WORLD = MPI_COMM_NULL; + KP_WORLD = MPI_COMM_NULL; + INT_BGROUP = MPI_COMM_NULL; + BP_WORLD = MPI_COMM_NULL; + GRID_WORLD = MPI_COMM_NULL; + DIAG_WORLD = MPI_COMM_NULL; + ::testing::InitGoogleTest(&argc, argv); + const int local_result = RUN_ALL_TESTS(); + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + const int result = domain.max(local_result); + Parallel_Global::finalize_mpi(); + return result; +} From 62cf3612e7fac2cd9dd4932c2f1b7ce4e3840032 Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 16:51:36 +0800 Subject: [PATCH 03/11] test(rt): cover boundary shifts with a minimal two-orbital model Use two synthetic atoms and Gamma plus a non-Gamma point. Exercise the real MD update and backing-cell sync for sub-roundoff negative motion, and check H/S/coefficient phase covariance for all axes, both crossing directions, both atoms, and clearing a previous shift. Verify the test fails when the coordinate fix is reverted or the Bloch phase sign is reversed. Restore both mutations and pass six related CTest targets, including the new single- and four-rank RT tests. No documentation update required: tests only. --- .../source_lcao/module_rt/test/CMakeLists.txt | 22 ++ .../module_rt/test/test_boundary_fix.cpp | 239 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 source/source_lcao/module_rt/test/test_boundary_fix.cpp diff --git a/source/source_lcao/module_rt/test/CMakeLists.txt b/source/source_lcao/module_rt/test/CMakeLists.txt index e4a2c5e0efb..d7bb4f09339 100644 --- a/source/source_lcao/module_rt/test/CMakeLists.txt +++ b/source/source_lcao/module_rt/test/CMakeLists.txt @@ -165,3 +165,25 @@ if(USE_CUDA) rad_interp_cuda_test.cu ../radial_interpolation.cpp) endif() + +addtest( + TARGET MODULE_LCAO_tddft_boundary_fix_test + LIBS parameter base device container cell symmetry neighbor neighbor_search + domain_decomposition psi + SOURCES + test_boundary_fix.cpp + ../boundary_fix.cpp + ../../../source_base/test/mpi_test_main.cpp + ../../../source_basis/module_ao/parallel_orbitals.cpp + ../../../source_md/md_base.cpp + ../../../source_md/md_func.cpp + ../../../source_io/module_output/output_log.cpp + ../../../source_io/module_output/print_info.cpp) +add_test( + NAME MODULE_LCAO_tddft_boundary_fix_test_np4 + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 + $ + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +set_tests_properties( + MODULE_LCAO_tddft_boundary_fix_test MODULE_LCAO_tddft_boundary_fix_test_np4 + PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1;MKL_NUM_THREADS=1;I_MPI_PIN=0" TIMEOUT 60) diff --git a/source/source_lcao/module_rt/test/test_boundary_fix.cpp b/source/source_lcao/module_rt/test/test_boundary_fix.cpp new file mode 100644 index 00000000000..4a91ac90379 --- /dev/null +++ b/source/source_lcao/module_rt/test/test_boundary_fix.cpp @@ -0,0 +1,239 @@ +#include "source_lcao/module_rt/boundary_fix.h" +#include "source_base/constants.h" +#include "source_base/parallel_cell.h" +#include "source_cell/mdcell.h" +#include "source_cell/update_cell.h" +#include "source_md/md_base.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +using Complex = std::complex; + +// Two artificial atoms, one orbital each, and Gamma plus a non-Gamma point. +// No structure, pseudopotential, orbitals, or SCF reference data are needed. +class BoundaryFixTest : public testing::Test +{ + protected: + UnitCell cell; + std::vector species; + K_Vectors kpoints; + Parallel_Orbitals orbitals; + const std::array initial_h = {{2.0, Complex(0.3, 0.4), Complex(0.3, -0.4), 3.0}}; + const std::array initial_s = {{1.0, Complex(0.2, 0.1), Complex(0.2, -0.1), 1.5}}; + const std::array initial_c = {{1.0, Complex(0.0, 0.5)}}; + + void SetUp() override + { + species.resize(1); + cell.atoms = species.data(); + cell.ntype = 1; + cell.nat = 2; + cell.lat0 = 1.0; + cell.omega = 1.0; + cell.latvec = ModuleBase::Matrix3(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + cell.GT = cell.latvec; + Atom& atom = species[0]; + atom.na = 2; + atom.nw = 1; + atom.taud.resize(2); + atom.tau.resize(2); + atom.dis.resize(2); + atom.vel.resize(2); + atom.mbl.resize(2); + atom.boundary_shift.resize(2); + atom.taud[0].set(0.0, 0.0, 0.0); + atom.taud[1].set(0.5, 0.5, 0.5); + atom.tau = atom.taud; + cell.set_iat2itia(); + cell.set_iat2iwt(1); + orbitals.set_serial(2, 2); + orbitals.ncol_bands = 1; + const int* atom_offsets = cell.get_iat2iwt(); + orbitals.set_atomic_trace(atom_offsets, 2, 2); + kpoints.set_nks(2); + kpoints.kvec_d.resize(2); + kpoints.kvec_d[0].set(0.0, 0.0, 0.0); + kpoints.kvec_d[1].set(0.25, 0.125, -0.25); + } + + Complex quadratic_form(const Complex* matrix, const Complex* coefficients) const + { + Complex result = 0.0; + for (int column = 0; column < 2; ++column) + { + for (int row = 0; row < 2; ++row) + { + result += std::conj(coefficients[row]) * matrix[row + 2 * column] * coefficients[column]; + } + } + return result; + } + + void expect_phase_correction(const std::array& phases) + { + const ct::TensorShape matrix_shape({8}); + ct::Tensor h(ct::DataType::DT_COMPLEX_DOUBLE, matrix_shape); + ct::Tensor s(ct::DataType::DT_COMPLEX_DOUBLE, matrix_shape); + psi::Psi coefficients(2, 1, 2, 2, true); + for (int ik = 0; ik < 2; ++ik) + { + Complex* h_block = h.data() + 4 * ik; + Complex* s_block = s.data() + 4 * ik; + std::copy(initial_h.begin(), initial_h.end(), h_block); + std::copy(initial_s.begin(), initial_s.end(), s_block); + coefficients.fix_k(ik); + std::copy(initial_c.begin(), initial_c.end(), coefficients.get_pointer()); + } + module_rt::reset_matrix_boundary(cell, kpoints, &orbitals, h, s, &coefficients, 4); + for (int ik = 0; ik < 2; ++ik) + { + const Complex* h_block = h.data() + 4 * ik; + const Complex* s_block = s.data() + 4 * ik; + coefficients.fix_k(ik); + const Complex* c_block = coefficients.get_pointer(); + for (int column = 0; column < 2; ++column) + { + const Complex column_phase = ik == 0 ? Complex(1.0, 0.0) : phases[column]; + const Complex expected_c = std::conj(column_phase) * initial_c[column]; + const double c_error = std::abs(c_block[column] - expected_c); + EXPECT_NEAR(c_error, 0.0, 1.0e-13); + for (int row = 0; row < 2; ++row) + { + const Complex row_phase = ik == 0 ? Complex(1.0, 0.0) : phases[row]; + const int index = row + 2 * column; + const Complex factor = std::conj(row_phase) * column_phase; + const Complex expected_h = initial_h[index] * factor; + const Complex expected_s = initial_s[index] * factor; + const double h_error = std::abs(h_block[index] - expected_h); + const double s_error = std::abs(s_block[index] - expected_s); + EXPECT_NEAR(h_error, 0.0, 1.0e-13); + EXPECT_NEAR(s_error, 0.0, 1.0e-13); + } + } + const Complex original_norm = quadratic_form(initial_s.data(), initial_c.data()); + const Complex original_energy = quadratic_form(initial_h.data(), initial_c.data()); + const Complex corrected_norm = quadratic_form(s_block, c_block); + const Complex corrected_energy = quadratic_form(h_block, c_block); + const double norm_error = std::abs(corrected_norm - original_norm); + const double energy_error = std::abs(corrected_energy - original_energy); + EXPECT_NEAR(norm_error, 0.0, 1.0e-13); + EXPECT_NEAR(energy_error, 0.0, 1.0e-13); + } + + // A subsequent adjustment with no crossing must clear the image shift, + // so the previous step's phase cannot be applied a second time. + std::array corrected_h; + std::array corrected_s; + std::array corrected_c; + std::copy_n(h.data(), 8, corrected_h.begin()); + std::copy_n(s.data(), 8, corrected_s.begin()); + for (int ik = 0; ik < 2; ++ik) + { + coefficients.fix_k(ik); + Complex* destination = corrected_c.data() + 2 * ik; + std::copy_n(coefficients.get_pointer(), 2, destination); + } + unitcell::periodic_boundary_adjustment(cell.atoms, cell.latvec, cell.ntype); + module_rt::reset_matrix_boundary(cell, kpoints, &orbitals, h, s, &coefficients, 4); + for (int index = 0; index < 8; ++index) + { + EXPECT_EQ(h.data()[index], corrected_h[index]); + EXPECT_EQ(s.data()[index], corrected_s[index]); + } + for (int ik = 0; ik < 2; ++ik) + { + coefficients.fix_k(ik); + for (int basis = 0; basis < 2; ++basis) + { + const int index = 2 * ik + basis; + EXPECT_EQ(coefficients.get_pointer()[basis], corrected_c[index]); + } + } + } +}; + +TEST_F(BoundaryFixTest, TinyNegativeMdDisplacementHasNoSpuriousImageOrBlochPhase) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + std::vector owned; + if (domain.rank() == 0) + { + owned.resize(2); + for (int index = 0; index < 2; ++index) + { + owned[index].type_index = index; + owned[index].frac = species[0].taud[index]; + owned[index].cart = owned[index].frac; + } + } + const std::vector labels(1, "X"); + const std::vector masses(1, 1.0); + const std::vector counts(1, 2); + MDCell moving_cell; + moving_cell.initialize_from_owned_atoms(cell.latvec, cell.GT, 1.0, 1.0, 2, + owned, labels, masses, counts, 0.0, domain); + moving_cell.set_backing_unitcell(cell); + MD_para parameters; + parameters.md_dt = ModuleBase::AU_to_FS; + parameters.md_tfirst = 0.0; + parameters.md_tlast = 0.0; + MD_base integrator(parameters, false, true, 0, moving_cell); + if (!moving_cell.owned_atoms().empty()) + { + moving_cell.owned_atoms()[0].vel.set(-1.0e-21, -2.0e-21, -3.0e-21); + } + std::ofstream log; + integrator.first_half(log); + moving_cell.sync_backing_unitcell(); + // Check before another boundary adjustment could hide an invalid 1.0. + for (int axis = 0; axis < 3; ++axis) + { + EXPECT_DOUBLE_EQ(species[0].taud[0][axis], 0.0); + EXPECT_EQ(species[0].boundary_shift[0][axis], 0); + } + const std::array identity_phases = {{1.0, 1.0}}; + expect_phase_correction(identity_phases); +} + +TEST_F(BoundaryFixTest, BothCrossingDirectionsCorrectMatricesAndCoefficientsForEachAxis) +{ + // exp(-2*pi*i*k_axis) at k=(1/4,1/8,-1/4), independent of production sincos. + const double half_sqrt_two = std::sqrt(0.5); + const std::array positive_shift_phases = {{Complex(0.0, -1.0), + Complex(half_sqrt_two, -half_sqrt_two), + Complex(0.0, 1.0)}}; + for (int atom = 0; atom < 2; ++atom) + { + for (int axis = 0; axis < 3; ++axis) + { + for (int shift = -1; shift <= 1; shift += 2) + { + SCOPED_TRACE(atom); + SCOPED_TRACE(axis); + SCOPED_TRACE(shift); + species[0].taud[0].set(0.5, 0.5, 0.5); + species[0].taud[1].set(0.5, 0.5, 0.5); + species[0].taud[atom][axis] = shift == 1 ? -0.125 : 1.125; + unitcell::periodic_boundary_adjustment(cell.atoms, cell.latvec, cell.ntype); + EXPECT_EQ(species[0].boundary_shift[atom][axis], shift); + const double expected_coordinate = shift == 1 ? 0.875 : 0.125; + EXPECT_DOUBLE_EQ(species[0].taud[atom][axis], expected_coordinate); + std::array phases = {{1.0, 1.0}}; + phases[atom] = shift == 1 ? positive_shift_phases[axis] : std::conj(positive_shift_phases[axis]); + expect_phase_correction(phases); + } + } + } +} +} // namespace From 0f3b0bbed10c2c3ae30c29d831ab18d2b259afba Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 17:04:09 +0800 Subject: [PATCH 04/11] Revert "test(rt): cover boundary shifts with a minimal two-orbital model" This reverts commit 62cf3612e7fac2cd9dd4932c2f1b7ce4e3840032. --- .../source_lcao/module_rt/test/CMakeLists.txt | 22 -- .../module_rt/test/test_boundary_fix.cpp | 239 ------------------ 2 files changed, 261 deletions(-) delete mode 100644 source/source_lcao/module_rt/test/test_boundary_fix.cpp diff --git a/source/source_lcao/module_rt/test/CMakeLists.txt b/source/source_lcao/module_rt/test/CMakeLists.txt index d7bb4f09339..e4a2c5e0efb 100644 --- a/source/source_lcao/module_rt/test/CMakeLists.txt +++ b/source/source_lcao/module_rt/test/CMakeLists.txt @@ -165,25 +165,3 @@ if(USE_CUDA) rad_interp_cuda_test.cu ../radial_interpolation.cpp) endif() - -addtest( - TARGET MODULE_LCAO_tddft_boundary_fix_test - LIBS parameter base device container cell symmetry neighbor neighbor_search - domain_decomposition psi - SOURCES - test_boundary_fix.cpp - ../boundary_fix.cpp - ../../../source_base/test/mpi_test_main.cpp - ../../../source_basis/module_ao/parallel_orbitals.cpp - ../../../source_md/md_base.cpp - ../../../source_md/md_func.cpp - ../../../source_io/module_output/output_log.cpp - ../../../source_io/module_output/print_info.cpp) -add_test( - NAME MODULE_LCAO_tddft_boundary_fix_test_np4 - COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 - $ - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -set_tests_properties( - MODULE_LCAO_tddft_boundary_fix_test MODULE_LCAO_tddft_boundary_fix_test_np4 - PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1;MKL_NUM_THREADS=1;I_MPI_PIN=0" TIMEOUT 60) diff --git a/source/source_lcao/module_rt/test/test_boundary_fix.cpp b/source/source_lcao/module_rt/test/test_boundary_fix.cpp deleted file mode 100644 index 4a91ac90379..00000000000 --- a/source/source_lcao/module_rt/test/test_boundary_fix.cpp +++ /dev/null @@ -1,239 +0,0 @@ -#include "source_lcao/module_rt/boundary_fix.h" -#include "source_base/constants.h" -#include "source_base/parallel_cell.h" -#include "source_cell/mdcell.h" -#include "source_cell/update_cell.h" -#include "source_md/md_base.h" - -#include - -#include -#include -#include -#include -#include -#include - -namespace -{ -using Complex = std::complex; - -// Two artificial atoms, one orbital each, and Gamma plus a non-Gamma point. -// No structure, pseudopotential, orbitals, or SCF reference data are needed. -class BoundaryFixTest : public testing::Test -{ - protected: - UnitCell cell; - std::vector species; - K_Vectors kpoints; - Parallel_Orbitals orbitals; - const std::array initial_h = {{2.0, Complex(0.3, 0.4), Complex(0.3, -0.4), 3.0}}; - const std::array initial_s = {{1.0, Complex(0.2, 0.1), Complex(0.2, -0.1), 1.5}}; - const std::array initial_c = {{1.0, Complex(0.0, 0.5)}}; - - void SetUp() override - { - species.resize(1); - cell.atoms = species.data(); - cell.ntype = 1; - cell.nat = 2; - cell.lat0 = 1.0; - cell.omega = 1.0; - cell.latvec = ModuleBase::Matrix3(1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0); - cell.GT = cell.latvec; - Atom& atom = species[0]; - atom.na = 2; - atom.nw = 1; - atom.taud.resize(2); - atom.tau.resize(2); - atom.dis.resize(2); - atom.vel.resize(2); - atom.mbl.resize(2); - atom.boundary_shift.resize(2); - atom.taud[0].set(0.0, 0.0, 0.0); - atom.taud[1].set(0.5, 0.5, 0.5); - atom.tau = atom.taud; - cell.set_iat2itia(); - cell.set_iat2iwt(1); - orbitals.set_serial(2, 2); - orbitals.ncol_bands = 1; - const int* atom_offsets = cell.get_iat2iwt(); - orbitals.set_atomic_trace(atom_offsets, 2, 2); - kpoints.set_nks(2); - kpoints.kvec_d.resize(2); - kpoints.kvec_d[0].set(0.0, 0.0, 0.0); - kpoints.kvec_d[1].set(0.25, 0.125, -0.25); - } - - Complex quadratic_form(const Complex* matrix, const Complex* coefficients) const - { - Complex result = 0.0; - for (int column = 0; column < 2; ++column) - { - for (int row = 0; row < 2; ++row) - { - result += std::conj(coefficients[row]) * matrix[row + 2 * column] * coefficients[column]; - } - } - return result; - } - - void expect_phase_correction(const std::array& phases) - { - const ct::TensorShape matrix_shape({8}); - ct::Tensor h(ct::DataType::DT_COMPLEX_DOUBLE, matrix_shape); - ct::Tensor s(ct::DataType::DT_COMPLEX_DOUBLE, matrix_shape); - psi::Psi coefficients(2, 1, 2, 2, true); - for (int ik = 0; ik < 2; ++ik) - { - Complex* h_block = h.data() + 4 * ik; - Complex* s_block = s.data() + 4 * ik; - std::copy(initial_h.begin(), initial_h.end(), h_block); - std::copy(initial_s.begin(), initial_s.end(), s_block); - coefficients.fix_k(ik); - std::copy(initial_c.begin(), initial_c.end(), coefficients.get_pointer()); - } - module_rt::reset_matrix_boundary(cell, kpoints, &orbitals, h, s, &coefficients, 4); - for (int ik = 0; ik < 2; ++ik) - { - const Complex* h_block = h.data() + 4 * ik; - const Complex* s_block = s.data() + 4 * ik; - coefficients.fix_k(ik); - const Complex* c_block = coefficients.get_pointer(); - for (int column = 0; column < 2; ++column) - { - const Complex column_phase = ik == 0 ? Complex(1.0, 0.0) : phases[column]; - const Complex expected_c = std::conj(column_phase) * initial_c[column]; - const double c_error = std::abs(c_block[column] - expected_c); - EXPECT_NEAR(c_error, 0.0, 1.0e-13); - for (int row = 0; row < 2; ++row) - { - const Complex row_phase = ik == 0 ? Complex(1.0, 0.0) : phases[row]; - const int index = row + 2 * column; - const Complex factor = std::conj(row_phase) * column_phase; - const Complex expected_h = initial_h[index] * factor; - const Complex expected_s = initial_s[index] * factor; - const double h_error = std::abs(h_block[index] - expected_h); - const double s_error = std::abs(s_block[index] - expected_s); - EXPECT_NEAR(h_error, 0.0, 1.0e-13); - EXPECT_NEAR(s_error, 0.0, 1.0e-13); - } - } - const Complex original_norm = quadratic_form(initial_s.data(), initial_c.data()); - const Complex original_energy = quadratic_form(initial_h.data(), initial_c.data()); - const Complex corrected_norm = quadratic_form(s_block, c_block); - const Complex corrected_energy = quadratic_form(h_block, c_block); - const double norm_error = std::abs(corrected_norm - original_norm); - const double energy_error = std::abs(corrected_energy - original_energy); - EXPECT_NEAR(norm_error, 0.0, 1.0e-13); - EXPECT_NEAR(energy_error, 0.0, 1.0e-13); - } - - // A subsequent adjustment with no crossing must clear the image shift, - // so the previous step's phase cannot be applied a second time. - std::array corrected_h; - std::array corrected_s; - std::array corrected_c; - std::copy_n(h.data(), 8, corrected_h.begin()); - std::copy_n(s.data(), 8, corrected_s.begin()); - for (int ik = 0; ik < 2; ++ik) - { - coefficients.fix_k(ik); - Complex* destination = corrected_c.data() + 2 * ik; - std::copy_n(coefficients.get_pointer(), 2, destination); - } - unitcell::periodic_boundary_adjustment(cell.atoms, cell.latvec, cell.ntype); - module_rt::reset_matrix_boundary(cell, kpoints, &orbitals, h, s, &coefficients, 4); - for (int index = 0; index < 8; ++index) - { - EXPECT_EQ(h.data()[index], corrected_h[index]); - EXPECT_EQ(s.data()[index], corrected_s[index]); - } - for (int ik = 0; ik < 2; ++ik) - { - coefficients.fix_k(ik); - for (int basis = 0; basis < 2; ++basis) - { - const int index = 2 * ik + basis; - EXPECT_EQ(coefficients.get_pointer()[basis], corrected_c[index]); - } - } - } -}; - -TEST_F(BoundaryFixTest, TinyNegativeMdDisplacementHasNoSpuriousImageOrBlochPhase) -{ - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - std::vector owned; - if (domain.rank() == 0) - { - owned.resize(2); - for (int index = 0; index < 2; ++index) - { - owned[index].type_index = index; - owned[index].frac = species[0].taud[index]; - owned[index].cart = owned[index].frac; - } - } - const std::vector labels(1, "X"); - const std::vector masses(1, 1.0); - const std::vector counts(1, 2); - MDCell moving_cell; - moving_cell.initialize_from_owned_atoms(cell.latvec, cell.GT, 1.0, 1.0, 2, - owned, labels, masses, counts, 0.0, domain); - moving_cell.set_backing_unitcell(cell); - MD_para parameters; - parameters.md_dt = ModuleBase::AU_to_FS; - parameters.md_tfirst = 0.0; - parameters.md_tlast = 0.0; - MD_base integrator(parameters, false, true, 0, moving_cell); - if (!moving_cell.owned_atoms().empty()) - { - moving_cell.owned_atoms()[0].vel.set(-1.0e-21, -2.0e-21, -3.0e-21); - } - std::ofstream log; - integrator.first_half(log); - moving_cell.sync_backing_unitcell(); - // Check before another boundary adjustment could hide an invalid 1.0. - for (int axis = 0; axis < 3; ++axis) - { - EXPECT_DOUBLE_EQ(species[0].taud[0][axis], 0.0); - EXPECT_EQ(species[0].boundary_shift[0][axis], 0); - } - const std::array identity_phases = {{1.0, 1.0}}; - expect_phase_correction(identity_phases); -} - -TEST_F(BoundaryFixTest, BothCrossingDirectionsCorrectMatricesAndCoefficientsForEachAxis) -{ - // exp(-2*pi*i*k_axis) at k=(1/4,1/8,-1/4), independent of production sincos. - const double half_sqrt_two = std::sqrt(0.5); - const std::array positive_shift_phases = {{Complex(0.0, -1.0), - Complex(half_sqrt_two, -half_sqrt_two), - Complex(0.0, 1.0)}}; - for (int atom = 0; atom < 2; ++atom) - { - for (int axis = 0; axis < 3; ++axis) - { - for (int shift = -1; shift <= 1; shift += 2) - { - SCOPED_TRACE(atom); - SCOPED_TRACE(axis); - SCOPED_TRACE(shift); - species[0].taud[0].set(0.5, 0.5, 0.5); - species[0].taud[1].set(0.5, 0.5, 0.5); - species[0].taud[atom][axis] = shift == 1 ? -0.125 : 1.125; - unitcell::periodic_boundary_adjustment(cell.atoms, cell.latvec, cell.ntype); - EXPECT_EQ(species[0].boundary_shift[atom][axis], shift); - const double expected_coordinate = shift == 1 ? 0.875 : 0.125; - EXPECT_DOUBLE_EQ(species[0].taud[atom][axis], expected_coordinate); - std::array phases = {{1.0, 1.0}}; - phases[atom] = shift == 1 ? positive_shift_phases[axis] : std::conj(positive_shift_phases[axis]); - expect_phase_correction(phases); - } - } - } -} -} // namespace From 1daa02ddd9e0d7c852683e6169f9f4d748715953 Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 17:15:29 +0800 Subject: [PATCH 05/11] test(rt): add a minimal H2 MD boundary integration regression Add INPUT, STRU and KPT for two H atoms on a periodic face with one non-Gamma k point and a supplied sub-roundoff transverse velocity. Register the case in CASES_CPU.txt and check post-initial SCF iterations and convergence through an opt-in md_scf.ref collector. Original beta11 fails Autotest.sh with both one and four MPI ranks: step 2 reaches scf_nmax=8 without convergence. The fixed binary and beta11 with only the coordinate patch pass with one step-2 iteration. Existing 16_NO_vel_TDDFT and the four focused MD CTest targets also pass. Document the trigger and commands in the case README. --- .gitignore | 1 + .../05_rtTDDFT/29_NO_MD_boundary_shift/INPUT | 29 ++++++++++++++++ tests/05_rtTDDFT/29_NO_MD_boundary_shift/KPT | 4 +++ .../05_rtTDDFT/29_NO_MD_boundary_shift/README | 33 +++++++++++++++++++ tests/05_rtTDDFT/29_NO_MD_boundary_shift/STRU | 22 +++++++++++++ .../29_NO_MD_boundary_shift/md_scf.ref | 1 + .../29_NO_MD_boundary_shift/result.ref | 4 +++ tests/05_rtTDDFT/CASES_CPU.txt | 1 + .../integrate/validation_tools/props_tddft.sh | 33 +++++++++++++++++++ 9 files changed, 128 insertions(+) create mode 100644 tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT create mode 100644 tests/05_rtTDDFT/29_NO_MD_boundary_shift/KPT create mode 100644 tests/05_rtTDDFT/29_NO_MD_boundary_shift/README create mode 100644 tests/05_rtTDDFT/29_NO_MD_boundary_shift/STRU create mode 100644 tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref create mode 100644 tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref diff --git a/.gitignore b/.gitignore index e2970790b34..7ba03078995 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ obj OUT.* log.txt result.out +md_scf.out test.sum *.dat .DS_Store diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT new file mode 100644 index 00000000000..ae6b09dae9c --- /dev/null +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT @@ -0,0 +1,29 @@ +INPUT_PARAMETERS +suffix autotest +pseudo_dir ../../PP_ORB +orbital_dir ../../PP_ORB +calculation md +esolver_type tddft +basis_type lcao +gamma_only 0 +symmetry -1 +nbands 2 +ecutwfc 20 + +scf_thr 1e-8 +scf_nmax 8 +mixing_type broyden +mixing_beta 0.7 +mixing_gg0 0.0 + +md_type nve +md_nstep 2 +estep_per_md 1 +md_dt 0.05 +td_dt 0.05 +init_vel 1 +# Keep the supplied sub-roundoff velocity rather than rescaling to zero. +md_tfirst -1 +md_dumpfreq 1 +md_restartfreq 1 +td_vext 0 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/KPT b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/KPT new file mode 100644 index 00000000000..5184d98cf62 --- /dev/null +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/KPT @@ -0,0 +1,4 @@ +K_POINTS +1 +Direct +0.25 0.0 0.0 1.0 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README new file mode 100644 index 00000000000..1b3a30eb4df --- /dev/null +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README @@ -0,0 +1,33 @@ +Minimal LCAO rt-TDDFT regression for a sub-roundoff displacement at a periodic boundary. + +Two H atoms, one non-Gamma k point, two MD steps, and no external field. +The molecule lies parallel to z on the x=0 boundary of a 15-Bohr cube. +Only the first atom can move, along x; its initial velocity is -1e-16 a.u. +The 0.05-fs step gives a fractional displacement of about -1.38e-17. +The transverse force is zero by symmetry, so this deterministic velocity +sets the displacement sign without relying on force roundoff or a user structure. + +On unmodified beta11, x-floor(x) rounds this negative coordinate to exactly +1.0. MDCell then passes an inconsistent cell image to rt-TDDFT without the +matching boundary_shift phase correction. The second electronic step loses +continuity despite the negligible physical motion. + +With the coordinate fix, x is canonicalized to 0.0 and step 2 converges in +one iteration. The eight-iteration SCF budget is sufficient for the initial +ground state (seven iterations in the verified CPU runs), while beta11 step 2 +reaches the limit without converging. With a larger budget beta11 needs ten +iterations and has a spurious energy jump of roughly 79 eV. + +The normal Autotest.sh energy checks are supplemented by md_scf.ref: + MD step, electronic iteration count, number of converged electronic solves + 2 1 1 +The opt-in collector also requires the initial SCF to converge. The initial +SCF iteration count is deliberately not compared across numerical backends. + +From tests/05_rtTDDFT: + OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 bash ../integrate/Autotest.sh \ + -a /absolute/path/to/abacus_std_para -n 4 -o 1 -j 1 \ + -r '^29_NO_MD_boundary_shift$' + +The case is registered in CASES_CPU.txt. CPU runs with one and four MPI ranks +were verified; it is not registered for GPU runs. diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/STRU b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/STRU new file mode 100644 index 00000000000..667f27b620d --- /dev/null +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/STRU @@ -0,0 +1,22 @@ +ATOMIC_SPECIES +H 1.00794 H.LDA.UPF + +NUMERICAL_ORBITAL +H_lda_8.0au_100Ry_2s1p.orb + +LATTICE_CONSTANT +15 + +LATTICE_VECTORS +1 0 0 +0 1 0 +0 0 1 + +ATOMIC_POSITIONS +Direct + +H +0 +2 +0.0 0.5 0.45 m 1 0 0 v -1e-16 0 0 +0.0 0.5 0.55 m 0 0 0 v 0 0 0 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref new file mode 100644 index 00000000000..33821be6660 --- /dev/null +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref @@ -0,0 +1 @@ +2 1 1 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref new file mode 100644 index 00000000000..72e0a4f6b01 --- /dev/null +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref @@ -0,0 +1,4 @@ +etotref -30.89775847665366 +etotperatomref -15.4488792383 +CompareMdScf_pass 0 +totaltimeref 0.67 diff --git a/tests/05_rtTDDFT/CASES_CPU.txt b/tests/05_rtTDDFT/CASES_CPU.txt index 949a7e13a1a..2e48e383611 100644 --- a/tests/05_rtTDDFT/CASES_CPU.txt +++ b/tests/05_rtTDDFT/CASES_CPU.txt @@ -26,3 +26,4 @@ 26_PW_single_TDDFT 27_PW_gmres_recycle_TDDFT 28_PW_gmres_subspace_TDDFT +29_NO_MD_boundary_shift diff --git a/tests/integrate/validation_tools/props_tddft.sh b/tests/integrate/validation_tools/props_tddft.sh index 5d3f7d1eace..a3260920639 100644 --- a/tests/integrate/validation_tools/props_tddft.sh +++ b/tests/integrate/validation_tools/props_tddft.sh @@ -8,6 +8,39 @@ run_tddft_props(){ +# Opt-in continuity check for MD cases with one electronic step per MD step. +# md_scf.ref lists post-initial steps as: step, iterations, converged solves. +# Also require the initial ground-state SCF to converge, without fixing its +# iteration count across different numerical backends. +if [ "$calculation" = md ] && [ "$esolver_type" = tddft ] && [ -f md_scf.ref ]; then + md_scf_status=1 + if awk ' + function emit_step() { + if (step == 1) initial_steps++ + if (step > 0 && converged != 1) invalid = 1 + if (step > 1) { + print step, iterations, converged + emitted++ + } + } + /STEP OF MOLECULAR DYNAMICS:/ { + emit_step() + step = $NF + iterations = 0 + converged = 0 + } + /#ELEC ITER#/ { iterations = $NF } + /#SCF IS CONVERGED#/ { converged++ } + END { + emit_step() + if (emitted == 0 || initial_steps != 1 || invalid) exit 1 + } + ' "$running_path" > md_scf.out && cmp -s md_scf.ref md_scf.out; then + md_scf_status=0 + fi + echo "CompareMdScf_pass $md_scf_status" >> "$props_result_file" +fi + #-------------------------------------------- # check currents in rt-TDDFT #-------------------------------------------- From d2f1f5fb00c8867b22bd96a330051240b973d2c8 Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 17:22:10 +0800 Subject: [PATCH 06/11] test(rt): use existing energy checks for the boundary regression Raise scf_nmax to 100, remove the opt-in SCF collector and its reference file, and keep the case README to its single purpose sentence. With one and four MPI ranks, the fixed case passes in one second-step iteration. Original beta11 takes ten iterations but reaches an energy about 79 eV away from the reference, so the existing Autotest.sh energy comparison fails without a custom validator or timeout. --- .gitignore | 1 - .../05_rtTDDFT/29_NO_MD_boundary_shift/INPUT | 2 +- .../05_rtTDDFT/29_NO_MD_boundary_shift/README | 32 ------------------ .../29_NO_MD_boundary_shift/md_scf.ref | 1 - .../29_NO_MD_boundary_shift/result.ref | 3 +- .../integrate/validation_tools/props_tddft.sh | 33 ------------------- 6 files changed, 2 insertions(+), 70 deletions(-) delete mode 100644 tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref diff --git a/.gitignore b/.gitignore index 7ba03078995..e2970790b34 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ obj OUT.* log.txt result.out -md_scf.out test.sum *.dat .DS_Store diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT index ae6b09dae9c..d54fba228be 100644 --- a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT @@ -11,7 +11,7 @@ nbands 2 ecutwfc 20 scf_thr 1e-8 -scf_nmax 8 +scf_nmax 100 mixing_type broyden mixing_beta 0.7 mixing_gg0 0.0 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README index 1b3a30eb4df..1068279a329 100644 --- a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README @@ -1,33 +1 @@ Minimal LCAO rt-TDDFT regression for a sub-roundoff displacement at a periodic boundary. - -Two H atoms, one non-Gamma k point, two MD steps, and no external field. -The molecule lies parallel to z on the x=0 boundary of a 15-Bohr cube. -Only the first atom can move, along x; its initial velocity is -1e-16 a.u. -The 0.05-fs step gives a fractional displacement of about -1.38e-17. -The transverse force is zero by symmetry, so this deterministic velocity -sets the displacement sign without relying on force roundoff or a user structure. - -On unmodified beta11, x-floor(x) rounds this negative coordinate to exactly -1.0. MDCell then passes an inconsistent cell image to rt-TDDFT without the -matching boundary_shift phase correction. The second electronic step loses -continuity despite the negligible physical motion. - -With the coordinate fix, x is canonicalized to 0.0 and step 2 converges in -one iteration. The eight-iteration SCF budget is sufficient for the initial -ground state (seven iterations in the verified CPU runs), while beta11 step 2 -reaches the limit without converging. With a larger budget beta11 needs ten -iterations and has a spurious energy jump of roughly 79 eV. - -The normal Autotest.sh energy checks are supplemented by md_scf.ref: - MD step, electronic iteration count, number of converged electronic solves - 2 1 1 -The opt-in collector also requires the initial SCF to converge. The initial -SCF iteration count is deliberately not compared across numerical backends. - -From tests/05_rtTDDFT: - OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 bash ../integrate/Autotest.sh \ - -a /absolute/path/to/abacus_std_para -n 4 -o 1 -j 1 \ - -r '^29_NO_MD_boundary_shift$' - -The case is registered in CASES_CPU.txt. CPU runs with one and four MPI ranks -were verified; it is not registered for GPU runs. diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref deleted file mode 100644 index 33821be6660..00000000000 --- a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/md_scf.ref +++ /dev/null @@ -1 +0,0 @@ -2 1 1 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref index 72e0a4f6b01..deb20c48fb5 100644 --- a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref +++ b/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref @@ -1,4 +1,3 @@ etotref -30.89775847665366 etotperatomref -15.4488792383 -CompareMdScf_pass 0 -totaltimeref 0.67 +totaltimeref 0.58 diff --git a/tests/integrate/validation_tools/props_tddft.sh b/tests/integrate/validation_tools/props_tddft.sh index a3260920639..5d3f7d1eace 100644 --- a/tests/integrate/validation_tools/props_tddft.sh +++ b/tests/integrate/validation_tools/props_tddft.sh @@ -8,39 +8,6 @@ run_tddft_props(){ -# Opt-in continuity check for MD cases with one electronic step per MD step. -# md_scf.ref lists post-initial steps as: step, iterations, converged solves. -# Also require the initial ground-state SCF to converge, without fixing its -# iteration count across different numerical backends. -if [ "$calculation" = md ] && [ "$esolver_type" = tddft ] && [ -f md_scf.ref ]; then - md_scf_status=1 - if awk ' - function emit_step() { - if (step == 1) initial_steps++ - if (step > 0 && converged != 1) invalid = 1 - if (step > 1) { - print step, iterations, converged - emitted++ - } - } - /STEP OF MOLECULAR DYNAMICS:/ { - emit_step() - step = $NF - iterations = 0 - converged = 0 - } - /#ELEC ITER#/ { iterations = $NF } - /#SCF IS CONVERGED#/ { converged++ } - END { - emit_step() - if (emitted == 0 || initial_steps != 1 || invalid) exit 1 - } - ' "$running_path" > md_scf.out && cmp -s md_scf.ref md_scf.out; then - md_scf_status=0 - fi - echo "CompareMdScf_pass $md_scf_status" >> "$props_result_file" -fi - #-------------------------------------------- # check currents in rt-TDDFT #-------------------------------------------- From d1e2235b019141976623770a48053c32b5ea490f Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 23:35:44 +0800 Subject: [PATCH 07/11] test(md): honor MPI launcher flags and align test naming --- source/source_md/test/CMakeLists.txt | 6 +++--- .../test/{test_md_func_mpi.cpp => mpi/test_md_func.cpp} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename source/source_md/test/{test_md_func_mpi.cpp => mpi/test_md_func.cpp} (100%) diff --git a/source/source_md/test/CMakeLists.txt b/source/source_md/test/CMakeLists.txt index 9b09c7704d2..b6c5146d609 100644 --- a/source/source_md/test/CMakeLists.txt +++ b/source/source_md/test/CMakeLists.txt @@ -199,12 +199,12 @@ if(ENABLE_MPI) TARGET MODULE_MD_dump_mpi KEEP_FEATURE_DEFINITIONS __MPI LIBS parameter psi device - SOURCES test_md_func_mpi.cpp ../../source_base/parallel_common.cpp + SOURCES mpi/test_md_func.cpp ../../source_base/parallel_common.cpp ../../source_cell/magnetism.cpp ${depend_files}) add_test( NAME MODULE_MD_dump_mpi_np4 - COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 - $ + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 ${MPIEXEC_PREFLAGS} + $ ${MPIEXEC_POSTFLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) set_tests_properties( MODULE_MD_dump_mpi MODULE_MD_dump_mpi_np4 diff --git a/source/source_md/test/test_md_func_mpi.cpp b/source/source_md/test/mpi/test_md_func.cpp similarity index 100% rename from source/source_md/test/test_md_func_mpi.cpp rename to source/source_md/test/mpi/test_md_func.cpp From 04a1ba3a26f3205c363091e88e2d121768ff85e3 Mon Sep 17 00:00:00 2001 From: zaotian Date: Fri, 23 Oct 2026 23:47:28 +0800 Subject: [PATCH 08/11] fix(md): stream ordered dump frames in bounded collective batches --- source/source_base/parallel_cell.cpp | 23 +-- source/source_base/parallel_cell.h | 5 +- source/source_md/md_func.cpp | 182 +++++++++++++++------ source/source_md/md_func.h | 9 + source/source_md/test/mpi/test_md_func.cpp | 87 ++++++++-- 5 files changed, 234 insertions(+), 72 deletions(-) diff --git a/source/source_base/parallel_cell.cpp b/source/source_base/parallel_cell.cpp index 6534c190044..0531e288ce6 100644 --- a/source/source_base/parallel_cell.cpp +++ b/source/source_base/parallel_cell.cpp @@ -1,6 +1,5 @@ #include "source_base/parallel_cell.h" -#include #include #include @@ -73,17 +72,21 @@ double CommunicationDomain::max(double value) const return value; } -std::string CommunicationDomain::gather_text(const std::string& value) const +std::string CommunicationDomain::gather_text(const std::string& value, int maximum_count) const { + if (maximum_count <= 0) + { + throw std::invalid_argument("CommunicationDomain text batch limit must be positive."); + } + const std::size_t count_limit = static_cast(maximum_count); + const int local_overflow = value.size() > count_limit; + if (max(local_overflow) != 0) + { + throw std::length_error("CommunicationDomain text exceeds the batch limit."); + } #ifdef __MPI if (communicator_ != MPI_COMM_NULL) { - const std::size_t maximum_count = std::numeric_limits::max(); - const int local_overflow = value.size() > maximum_count; - if (max(local_overflow) != 0) - { - throw std::length_error("CommunicationDomain text exceeds the MPI count range."); - } const int local_size = static_cast(value.size()); const int process_count = size(); std::vector lengths(process_count, 0); @@ -95,7 +98,7 @@ std::string CommunicationDomain::gather_text(const std::string& value) const { for (int rank = 0; rank < process_count; ++rank) { - if (total_size + lengths[rank] > maximum_count) + if (total_size + lengths[rank] > count_limit) { total_overflow = 1; break; @@ -107,7 +110,7 @@ std::string CommunicationDomain::gather_text(const std::string& value) const MPI_Bcast(&total_overflow, 1, MPI_INT, 0, communicator_); if (total_overflow != 0) { - throw std::length_error("CommunicationDomain gathered text exceeds the MPI count range."); + throw std::length_error("CommunicationDomain gathered text exceeds the batch limit."); } std::string gathered; if (rank_ == 0) diff --git a/source/source_base/parallel_cell.h b/source/source_base/parallel_cell.h index 5f9fda234cb..bd02ccaa035 100644 --- a/source/source_base/parallel_cell.h +++ b/source/source_base/parallel_cell.h @@ -22,8 +22,9 @@ class CommunicationDomain int max(int value) const; int sum(int value) const; double max(double value) const; - /// Concatenate text on rank zero; other ranks receive an empty string. - std::string gather_text(const std::string& value) const; + /// Gather one bounded text batch on rank zero; other ranks receive an empty string. + /// maximum_count bounds both local payloads and their total in MPI count units. + std::string gather_text(const std::string& value, int maximum_count) const; private: #ifdef __MPI diff --git a/source/source_md/md_func.cpp b/source/source_md/md_func.cpp index 3e4a48d40e0..3536dd4a0b7 100644 --- a/source/source_md/md_func.cpp +++ b/source/source_md/md_func.cpp @@ -6,9 +6,13 @@ #include "source_base/timer.h" #include "source_io/module_output/output_log.h" +#include #include +#include #include #include +#include +#include namespace MD_func @@ -390,6 +394,43 @@ void print_stress(std::ofstream& ofs, const ModuleBase::matrix& virial, const Mo return; } +namespace +{ +void write_dump_rows(std::ostream& output, + const std::string& text, + const std::int64_t begin, + const std::int64_t end) +{ + // Spatial ownership is unrelated to the stable type-major atom index. + std::map rows; + std::istringstream input(text); + std::string line; + while (std::getline(input, line)) + { + std::istringstream record(line); + std::int64_t index = 0; + if (!(record >> index) || index < begin || index >= end) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "invalid MD_dump atom index."); + } + const bool inserted = rows.emplace(index, line).second; + if (!inserted) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "duplicate MD_dump atom index."); + } + } + const std::size_t expected_count = static_cast(end - begin); + if (rows.size() != expected_count) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "missing MD_dump atoms."); + } + for (const auto& row : rows) + { + output << row.second << "\n"; + } +} +} // namespace + void dump_info(const int& step, const std::string& global_out_dir, const MDCell& mdcell, @@ -397,6 +438,22 @@ void dump_info(const int& step, const bool cal_stress, const ModuleBase::matrix& virial) { + const int max_batch_bytes = 1024 * 1024; + dump_info(step, global_out_dir, mdcell, mdp, cal_stress, virial, max_batch_bytes); +} + +void dump_info(const int& step, + const std::string& global_out_dir, + const MDCell& mdcell, + const MD_para& mdp, + const bool cal_stress, + const ModuleBase::matrix& virial, + const int max_batch_bytes) +{ + if (max_batch_bytes <= 0) + { + throw std::invalid_argument("MD_dump batch budget must be positive."); + } std::stringstream file; file << global_out_dir << "MD_dump"; const double unit_pos = mdcell.lat0() / ModuleBase::ANGSTROM_AU; @@ -424,70 +481,101 @@ void dump_info(const int& step, { type_offsets[it + 1] = type_offsets[it] + mdcell.type_atom_counts()[it]; } - std::ostringstream local; - local << std::fixed << std::setprecision(12); - for (int i = 0; i < mdcell.owned_atoms().size(); ++i) + const auto format_atom = [&](const LocalAtom& atom) -> std::string { - const LocalAtom& atom = mdcell.owned_atoms()[static_cast(i)]; - local << " " << type_offsets[static_cast(atom.type)] + atom.type_index - << " " << mdcell.type_labels()[static_cast(atom.type)] - << " " << atom.cart.x * unit_pos << " " << atom.cart.y * unit_pos << " " << atom.cart.z * unit_pos; + std::ostringstream row; + row << std::fixed << std::setprecision(12); + row << " " << type_offsets[static_cast(atom.type)] + atom.type_index + << " " << mdcell.type_labels()[static_cast(atom.type)] + << " " << atom.cart.x * unit_pos << " " << atom.cart.y * unit_pos << " " << atom.cart.z * unit_pos; if (mdp.dump_force) - local << " " << atom.force.x * unit_force << " " << atom.force.y * unit_force << " " << atom.force.z * unit_force; + row << " " << atom.force.x * unit_force << " " << atom.force.y * unit_force << " " << atom.force.z * unit_force; if (mdp.dump_vel) - local << " " << atom.vel.x * unit_vel << " " << atom.vel.y * unit_vel << " " << atom.vel.z * unit_vel; - local << "\n"; + row << " " << atom.vel.x * unit_vel << " " << atom.vel.y * unit_vel << " " << atom.vel.z * unit_vel; + row << "\n"; + return row.str(); + }; + std::vector> ordered_atoms; + const std::size_t local_atom_count = mdcell.owned_atoms().size(); + ordered_atoms.reserve(local_atom_count); + int local_row_bytes = 0; + int local_overflow = 0; + const std::size_t maximum_count = std::numeric_limits::max(); + for (const LocalAtom& atom : mdcell.owned_atoms()) + { + const std::int64_t index = type_offsets[static_cast(atom.type)] + atom.type_index; + const LocalAtom* atom_pointer = &atom; + ordered_atoms.emplace_back(index, atom_pointer); + const std::string row = format_atom(atom); + if (row.size() > maximum_count) + { + local_overflow = 1; + } + else + { + const int row_bytes = static_cast(row.size()); + local_row_bytes = std::max(local_row_bytes, row_bytes); + } } + std::sort(ordered_atoms.begin(), ordered_atoms.end(), + [](const std::pair& left, + const std::pair& right) + { return left.first < right.first; }); ModuleBase::CommunicationDomain domain; #ifdef __MPI domain.initialize(mdcell.communicator()); #endif - const std::string local_text = local.str(); - const std::string gathered = domain.gather_text(local_text); - if (domain.rank() != 0) + if (domain.max(local_overflow) != 0) { - return; + throw std::length_error("A single MD_dump row exceeds the MPI count range."); } - - // Spatial ownership is unrelated to the stable type-major atom index. - std::map rows; - std::istringstream input(gathered); - std::string line; - while (std::getline(input, line)) + const int maximum_row_bytes = domain.max(local_row_bytes); + const int row_bytes = std::max(maximum_row_bytes, 1); + const int batch_bytes = std::max(max_batch_bytes, row_bytes); + const std::int64_t batch_atoms = batch_bytes / row_bytes; + std::ofstream ofs; + if (domain.rank() == 0) { - std::istringstream record(line); - std::int64_t index = 0; - if (!(record >> index) || index < 0 || index >= type_offsets.back()) + const std::string filename = file.str(); + const std::ios_base::openmode mode = step == 0 ? std::ios::trunc : std::ios::app; + ofs.open(filename.c_str(), mode); + if (!ofs) { - ModuleBase::WARNING_QUIT("MD_func::dump_info", "invalid MD_dump atom index."); + ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot open MD_dump."); } - const bool inserted = rows.emplace(index, line).second; - if (!inserted) + ofs << header.str(); + } + std::size_t local_index = 0; + for (std::int64_t begin = 0; begin < type_offsets.back();) + { + // Each global-index range contains at most batch_atoms rows across all ranks. + // Counts and displacements are bounded per batch, independent of frame size. + const std::int64_t remaining = type_offsets.back() - begin; + const std::int64_t count = std::min(batch_atoms, remaining); + const std::int64_t end = begin + count; + std::ostringstream local; + while (local_index < ordered_atoms.size() && ordered_atoms[local_index].first < end) { - ModuleBase::WARNING_QUIT("MD_func::dump_info", "duplicate MD_dump atom index."); + const LocalAtom& atom = *ordered_atoms[local_index].second; + local << format_atom(atom); + ++local_index; } + const std::string local_text = local.str(); + const std::string gathered = domain.gather_text(local_text, batch_bytes); + if (domain.rank() == 0) + { + write_dump_rows(ofs, gathered, begin, end); + } + begin = end; } - if (rows.size() != static_cast(type_offsets.back())) - { - ModuleBase::WARNING_QUIT("MD_func::dump_info", "missing MD_dump atoms."); - } - const std::string filename = file.str(); - const std::ios_base::openmode mode = step == 0 ? std::ios::trunc : std::ios::app; - std::ofstream ofs(filename.c_str(), mode); - if (!ofs) - { - ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot open MD_dump."); - } - ofs << header.str(); - for (const auto& row : rows) - { - ofs << row.second << "\n"; - } - ofs << "\n\n"; - ofs.close(); - if (!ofs) + if (domain.rank() == 0) { - ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot write MD_dump."); + ofs << "\n\n"; + ofs.close(); + if (!ofs) + { + ModuleBase::WARNING_QUIT("MD_func::dump_info", "cannot write MD_dump."); + } } } diff --git a/source/source_md/md_func.h b/source/source_md/md_func.h index c8095d069e8..5dc21f2f85a 100644 --- a/source/source_md/md_func.h +++ b/source/source_md/md_func.h @@ -149,6 +149,15 @@ void dump_info(const int& step, const bool cal_stress, const ModuleBase::matrix& virial); +/// Explicit buffer budget for bounded dump collectives; one row is the minimum batch. +void dump_info(const int& step, + const std::string& global_out_dir, + const MDCell& mdcell, + const MD_para& mdp, + const bool cal_stress, + const ModuleBase::matrix& virial, + const int max_batch_bytes); + /** * @brief obtain the atomic mass and whether the freedom is fixed * diff --git a/source/source_md/test/mpi/test_md_func.cpp b/source/source_md/test/mpi/test_md_func.cpp index f5dd6c1524b..986dec221ab 100644 --- a/source/source_md/test/mpi/test_md_func.cpp +++ b/source/source_md/test/mpi/test_md_func.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -33,7 +34,11 @@ std::string make_dump_directory(const ModuleBase::CommunicationDomain& domain) return directory; } -TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) +class MdDumpBatchTest : public testing::TestWithParam +{ +}; + +TEST_P(MdDumpBatchTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) { const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); ModuleBase::Matrix3 lattice; @@ -43,7 +48,8 @@ TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) const ModuleBase::Matrix3 inverse = lattice.Inverse(); const int owner_count = std::min(domain.size(), 3); std::vector atoms; - for (int index = 3; index >= 0; --index) + const int total_atoms = 13; + for (int index = total_atoms - 1; index >= 0; --index) { const int owner = (2 * index + 1) % owner_count; if (owner != domain.rank()) @@ -51,24 +57,25 @@ TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) continue; } LocalAtom atom; - atom.type = index / 2; - atom.type_index = index % 2; - const double x = 0.1 * (index + 1); + atom.type = index < 6 ? 0 : 1; + atom.type_index = index - 6 * atom.type; + const double x = 0.01 * (index + 1); atom.frac.set(x, 0.5, 0.5); atom.cart = atom.frac; atoms.push_back(atom); } const std::vector labels = {"A", "B"}; const std::vector masses(2, 1.0); - const std::vector counts(2, 2); + const std::vector counts = {6, 7}; MDCell cell; - cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 4, atoms, labels, masses, counts, 0.0, domain); + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, total_atoms, atoms, labels, masses, counts, 0.0, domain); const std::string directory = make_dump_directory(domain); ASSERT_FALSE(directory.empty()); MD_para parameters; ModuleBase::matrix virial(3, 3); MD_func::dump_info(0, directory, cell, parameters, false, virial); - MD_func::dump_info(1, directory, cell, parameters, false, virial); + const int batch_bytes = GetParam(); + MD_func::dump_info(1, directory, cell, parameters, false, virial, batch_bytes); if (domain.rank() == 0) { const std::string filename = directory + "MD_dump"; @@ -77,6 +84,9 @@ TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) std::string line; int frame_count = 0; int atom_count = 0; + std::size_t frame_bytes = 0; + std::vector rank_bytes(domain.size(), 0); + std::vector first_frame; bool reading_atoms = false; while (std::getline(input, line)) { @@ -84,10 +94,12 @@ TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) { if (frame_count > 0) { - EXPECT_EQ(atom_count, 4); + EXPECT_EQ(atom_count, total_atoms); } ++frame_count; atom_count = 0; + frame_bytes = 0; + std::fill(rank_bytes.begin(), rank_bytes.end(), 0); reading_atoms = false; } else if (line.find("INDEX") == 0) @@ -96,7 +108,24 @@ TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) } else if (reading_atoms && line.empty()) { - EXPECT_EQ(atom_count, 4); + EXPECT_EQ(atom_count, total_atoms); + if (batch_bytes == 512) + { + // The whole frame exceeds the injected collective count limit. + EXPECT_GT(frame_bytes, 512u); + } + if (batch_bytes == 1024) + { + EXPECT_GT(frame_bytes, 1024u); + if (domain.size() > 2) + { + // Reproduce the aggregate-only overflow: each rank fits on its own. + for (const std::size_t bytes : rank_bytes) + { + EXPECT_LE(bytes, 1024u); + } + } + } reading_atoms = false; } else if (reading_atoms) @@ -108,21 +137,53 @@ TEST(MdDumpMpiTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) row >> index >> label >> x; ASSERT_TRUE(row); EXPECT_EQ(index, atom_count); - const std::string expected_label = atom_count < 2 ? "A" : "B"; + const std::string expected_label = atom_count < 6 ? "A" : "B"; EXPECT_EQ(label, expected_label); - const double expected_x = 0.1 * (atom_count + 1) / ModuleBase::ANGSTROM_AU; + const double expected_x = 0.01 * (atom_count + 1) / ModuleBase::ANGSTROM_AU; EXPECT_NEAR(x, expected_x, 1.0e-12); + if (frame_count == 1) + { + first_frame.push_back(line); + } + else + { + ASSERT_LT(atom_count, first_frame.size()); + EXPECT_EQ(line, first_frame[atom_count]); + } + frame_bytes += line.size() + 1; + const int owner = (2 * index + 1) % owner_count; + rank_bytes[owner] += line.size() + 1; ++atom_count; } } EXPECT_EQ(frame_count, 2); - EXPECT_EQ(atom_count, 4); + EXPECT_EQ(atom_count, total_atoms); input.close(); std::remove(filename.c_str()); rmdir(directory.c_str()); } } +// One-row, several-row, and normal budgets must produce identical complete frames. +INSTANTIATE_TEST_SUITE_P(BufferBudgets, MdDumpBatchTest, testing::Values(1, 512, 1024, 1048576)); + +TEST(MdDumpMpiTest, BoundedGatherRejectsOversizedBatchesCollectively) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + const int local_bytes = domain.rank() == 0 ? 17 : 0; + const std::string oversized(local_bytes, 'x'); + EXPECT_THROW(domain.gather_text(oversized, 16), std::length_error); + if (domain.size() > 2) + { + // Every rank fits individually; their aggregate must also be checked on all ranks. + const std::string local(8, 'x'); + EXPECT_THROW(domain.gather_text(local, 16), std::length_error); + } + const std::string empty; + const std::string gathered = domain.gather_text(empty, 1); + EXPECT_TRUE(gathered.empty()); +} + TEST(MdDumpMpiTest, EmptyRanksCanWriteAnEmptyFrame) { const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); From d4028da3410df85c9617e1bccd302054f366dbf7 Mon Sep 17 00:00:00 2001 From: zaotian Date: Sat, 24 Oct 2026 00:13:55 +0800 Subject: [PATCH 09/11] test(rt): merge boundary regression into the two-pulse case --- .ci/slurm/config.ini | 2 +- tests/05_rtTDDFT/10_NO_HHG_TDDFT/INPUT | 45 ------------------- tests/05_rtTDDFT/10_NO_HHG_TDDFT/KPT | 4 -- tests/05_rtTDDFT/10_NO_HHG_TDDFT/STRU | 22 --------- tests/05_rtTDDFT/10_NO_HHG_TDDFT/result.ref | 5 --- .../INPUT | 16 ++++++- .../KPT | 0 .../README | 1 + .../STRU | 0 .../result.ref | 0 tests/05_rtTDDFT/CASES_CPU.txt | 3 +- tests/05_rtTDDFT/CASES_GPU.txt | 2 +- 12 files changed, 19 insertions(+), 81 deletions(-) delete mode 100644 tests/05_rtTDDFT/10_NO_HHG_TDDFT/INPUT delete mode 100644 tests/05_rtTDDFT/10_NO_HHG_TDDFT/KPT delete mode 100644 tests/05_rtTDDFT/10_NO_HHG_TDDFT/STRU delete mode 100644 tests/05_rtTDDFT/10_NO_HHG_TDDFT/result.ref rename tests/05_rtTDDFT/{29_NO_MD_boundary_shift => 10_NO_MD_boundary_shift}/INPUT (62%) rename tests/05_rtTDDFT/{29_NO_MD_boundary_shift => 10_NO_MD_boundary_shift}/KPT (100%) rename tests/05_rtTDDFT/{29_NO_MD_boundary_shift => 10_NO_MD_boundary_shift}/README (54%) rename tests/05_rtTDDFT/{29_NO_MD_boundary_shift => 10_NO_MD_boundary_shift}/STRU (100%) rename tests/05_rtTDDFT/{29_NO_MD_boundary_shift => 10_NO_MD_boundary_shift}/result.ref (100%) diff --git a/.ci/slurm/config.ini b/.ci/slurm/config.ini index 4c265dcc20d..299d16c30bf 100644 --- a/.ci/slurm/config.ini +++ b/.ci/slurm/config.ini @@ -235,7 +235,7 @@ resource = gpu2 runner = autotest_gpu [case.035] suite = 05_rtTDDFT -name = 10_NO_HHG_TDDFT +name = 10_NO_MD_boundary_shift resource = gpu2 runner = autotest_gpu [case.036] diff --git a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/INPUT b/tests/05_rtTDDFT/10_NO_HHG_TDDFT/INPUT deleted file mode 100644 index 668ee118580..00000000000 --- a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/INPUT +++ /dev/null @@ -1,45 +0,0 @@ -INPUT_PARAMETERS -#Parameters (General) -suffix autotest -pseudo_dir ../../PP_ORB -orbital_dir ../../PP_ORB -calculation md -esolver_type tddft -basis_type lcao -gamma_only 0 -nbands 5 -ecutwfc 20 - -#Parameter (Electronic Structure) -scf_nmax 50 - -mixing_type broyden -mixing_beta 0.7 -mixing_gg0 0.0 -scf_thr 1.0e-6 - -cal_stress 1 -stress_thr 1e-6 -cal_force 1 -force_thr_ev 1.0e-3 - -#Parameter (MD) -md_type nve -md_nstep 3 -estep_per_md 1 -md_dt 0.05 -init_vel 1 - -td_vext 1 -td_vext_dire 2*1 -td_stype 0 -td_ttype 2*0 -td_tstart 1 -td_tend 2 -td_lcut1 0.05 -td_lcut2 0.95 -td_gauss_freq 1.164656 0.029116 -td_gauss_amp 2.0 5.0 -td_gauss_phase 2*0.0 -td_gauss_t0 2*0 -td_gauss_sigma 2*30 diff --git a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/KPT b/tests/05_rtTDDFT/10_NO_HHG_TDDFT/KPT deleted file mode 100644 index e769af76382..00000000000 --- a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/KPT +++ /dev/null @@ -1,4 +0,0 @@ -K_POINTS -0 -Gamma -2 1 1 0 0 0 diff --git a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/STRU b/tests/05_rtTDDFT/10_NO_HHG_TDDFT/STRU deleted file mode 100644 index af322391215..00000000000 --- a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/STRU +++ /dev/null @@ -1,22 +0,0 @@ -ATOMIC_SPECIES -H 1.00794 H.LDA.UPF - -NUMERICAL_ORBITAL -H_lda_8.0au_100Ry_2s1p.orb - -LATTICE_CONSTANT -15 - -LATTICE_VECTORS -1 0 0 #latvec1 -0 1 0 #latvec2 -0 0 1 #latvec3 - -ATOMIC_POSITIONS -Cartesian - -H #label -0 #magnetism -2 #number of atoms -0.502095122795 0.474071446432 0.509122850526 m 1 1 1 v -1.58317459417e-05 0.000196314331857 0.000681467807833 -0.497904877205 0.525928553568 0.592277149473 m 1 1 1 v 1.58317459417e-05 -0.000196314331857 -0.000681467807833 diff --git a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/result.ref b/tests/05_rtTDDFT/10_NO_HHG_TDDFT/result.ref deleted file mode 100644 index 9ef1f23d968..00000000000 --- a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/result.ref +++ /dev/null @@ -1,5 +0,0 @@ -etotref 20.46810349845972 -etotperatomref 10.2340517492 -totalforceref 0.449538 -totalstressref 487.715103 -totaltimeref 1.56 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/INPUT similarity index 62% rename from tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT rename to tests/05_rtTDDFT/10_NO_MD_boundary_shift/INPUT index d54fba228be..6ba92a2a4e5 100644 --- a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/INPUT +++ b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/INPUT @@ -26,4 +26,18 @@ init_vel 1 md_tfirst -1 md_dumpfreq 1 md_restartfreq 1 -td_vext 0 + +# Keep the early-time fields weak to preserve the boundary regression. +td_vext 1 +td_vext_dire 2 3 +td_stype 0 +td_ttype 2*0 +td_tstart 0 +td_tend 2 +td_lcut1 0.05 +td_lcut2 0.95 +td_gauss_freq 1.164656 0.029116 +td_gauss_amp 2.0 5.0 +td_gauss_phase 2*0.0 +td_gauss_t0 2*70 +td_gauss_sigma 2*0.5 diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/KPT b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/KPT similarity index 100% rename from tests/05_rtTDDFT/29_NO_MD_boundary_shift/KPT rename to tests/05_rtTDDFT/10_NO_MD_boundary_shift/KPT diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/README similarity index 54% rename from tests/05_rtTDDFT/29_NO_MD_boundary_shift/README rename to tests/05_rtTDDFT/10_NO_MD_boundary_shift/README index 1068279a329..0e2ac070bbe 100644 --- a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/README +++ b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/README @@ -1 +1,2 @@ Minimal LCAO rt-TDDFT regression for a sub-roundoff displacement at a periodic boundary. +Also checks that Gaussian pulses in multiple directions run successfully. diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/STRU b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/STRU similarity index 100% rename from tests/05_rtTDDFT/29_NO_MD_boundary_shift/STRU rename to tests/05_rtTDDFT/10_NO_MD_boundary_shift/STRU diff --git a/tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/result.ref similarity index 100% rename from tests/05_rtTDDFT/29_NO_MD_boundary_shift/result.ref rename to tests/05_rtTDDFT/10_NO_MD_boundary_shift/result.ref diff --git a/tests/05_rtTDDFT/CASES_CPU.txt b/tests/05_rtTDDFT/CASES_CPU.txt index 2e48e383611..8cbca6fadab 100644 --- a/tests/05_rtTDDFT/CASES_CPU.txt +++ b/tests/05_rtTDDFT/CASES_CPU.txt @@ -7,7 +7,7 @@ 07_NO_EDM_TDDFT 08_NO_ETRS_TDDFT 09_NO_HEAV_TDDFT -10_NO_HHG_TDDFT +10_NO_MD_boundary_shift 11_NO_O3_TDDFT 12_NO_re_TDDFT 13_NO_Taylor_TDDFT @@ -26,4 +26,3 @@ 26_PW_single_TDDFT 27_PW_gmres_recycle_TDDFT 28_PW_gmres_subspace_TDDFT -29_NO_MD_boundary_shift diff --git a/tests/05_rtTDDFT/CASES_GPU.txt b/tests/05_rtTDDFT/CASES_GPU.txt index 45764707786..c2cf48ba997 100644 --- a/tests/05_rtTDDFT/CASES_GPU.txt +++ b/tests/05_rtTDDFT/CASES_GPU.txt @@ -7,7 +7,7 @@ 07_NO_EDM_TDDFT #08_NO_ETRS_TDDFT 09_NO_HEAV_TDDFT -10_NO_HHG_TDDFT +10_NO_MD_boundary_shift 11_NO_O3_TDDFT 12_NO_re_TDDFT #13_NO_Taylor_TDDFT From e17e11fbc38d56a4783b86bd8b14dfbb530f569e Mon Sep 17 00:00:00 2001 From: zaotian Date: Tue, 27 Oct 2026 15:23:27 +0800 Subject: [PATCH 10/11] test(md): reuse the existing test source for MPI dump coverage --- source/source_md/test/CMakeLists.txt | 2 +- source/source_md/test/mpi/test_md_func.cpp | 240 --------------------- source/source_md/test/test_md_func.cpp | 240 +++++++++++++++++++++ 3 files changed, 241 insertions(+), 241 deletions(-) delete mode 100644 source/source_md/test/mpi/test_md_func.cpp diff --git a/source/source_md/test/CMakeLists.txt b/source/source_md/test/CMakeLists.txt index b6c5146d609..133e6afdc87 100644 --- a/source/source_md/test/CMakeLists.txt +++ b/source/source_md/test/CMakeLists.txt @@ -199,7 +199,7 @@ if(ENABLE_MPI) TARGET MODULE_MD_dump_mpi KEEP_FEATURE_DEFINITIONS __MPI LIBS parameter psi device - SOURCES mpi/test_md_func.cpp ../../source_base/parallel_common.cpp + SOURCES test_md_func.cpp ../../source_base/parallel_common.cpp ../../source_cell/magnetism.cpp ${depend_files}) add_test( NAME MODULE_MD_dump_mpi_np4 diff --git a/source/source_md/test/mpi/test_md_func.cpp b/source/source_md/test/mpi/test_md_func.cpp deleted file mode 100644 index 986dec221ab..00000000000 --- a/source/source_md/test/mpi/test_md_func.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include "source_md/md_func.h" -#include "source_base/parallel_cell.h" -#include "source_base/parallel_common.h" -#include "source_base/parallel_global.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -std::string make_dump_directory(const ModuleBase::CommunicationDomain& domain) -{ - std::string directory; - if (domain.rank() == 0) - { - std::string pattern = testing::TempDir() + "abacus-md-dump-XXXXXX"; - char* created = mkdtemp(&pattern[0]); - if (created != nullptr) - { - directory = created; - directory += "/"; - } - } - Parallel_Common::bcast_string(directory); - return directory; -} - -class MdDumpBatchTest : public testing::TestWithParam -{ -}; - -TEST_P(MdDumpBatchTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) -{ - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - ModuleBase::Matrix3 lattice; - lattice.e11 = 1.0; - lattice.e22 = 1.0; - lattice.e33 = 1.0; - const ModuleBase::Matrix3 inverse = lattice.Inverse(); - const int owner_count = std::min(domain.size(), 3); - std::vector atoms; - const int total_atoms = 13; - for (int index = total_atoms - 1; index >= 0; --index) - { - const int owner = (2 * index + 1) % owner_count; - if (owner != domain.rank()) - { - continue; - } - LocalAtom atom; - atom.type = index < 6 ? 0 : 1; - atom.type_index = index - 6 * atom.type; - const double x = 0.01 * (index + 1); - atom.frac.set(x, 0.5, 0.5); - atom.cart = atom.frac; - atoms.push_back(atom); - } - const std::vector labels = {"A", "B"}; - const std::vector masses(2, 1.0); - const std::vector counts = {6, 7}; - MDCell cell; - cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, total_atoms, atoms, labels, masses, counts, 0.0, domain); - const std::string directory = make_dump_directory(domain); - ASSERT_FALSE(directory.empty()); - MD_para parameters; - ModuleBase::matrix virial(3, 3); - MD_func::dump_info(0, directory, cell, parameters, false, virial); - const int batch_bytes = GetParam(); - MD_func::dump_info(1, directory, cell, parameters, false, virial, batch_bytes); - if (domain.rank() == 0) - { - const std::string filename = directory + "MD_dump"; - std::ifstream input(filename.c_str()); - ASSERT_TRUE(input); - std::string line; - int frame_count = 0; - int atom_count = 0; - std::size_t frame_bytes = 0; - std::vector rank_bytes(domain.size(), 0); - std::vector first_frame; - bool reading_atoms = false; - while (std::getline(input, line)) - { - if (line.find("MDSTEP:") == 0) - { - if (frame_count > 0) - { - EXPECT_EQ(atom_count, total_atoms); - } - ++frame_count; - atom_count = 0; - frame_bytes = 0; - std::fill(rank_bytes.begin(), rank_bytes.end(), 0); - reading_atoms = false; - } - else if (line.find("INDEX") == 0) - { - reading_atoms = true; - } - else if (reading_atoms && line.empty()) - { - EXPECT_EQ(atom_count, total_atoms); - if (batch_bytes == 512) - { - // The whole frame exceeds the injected collective count limit. - EXPECT_GT(frame_bytes, 512u); - } - if (batch_bytes == 1024) - { - EXPECT_GT(frame_bytes, 1024u); - if (domain.size() > 2) - { - // Reproduce the aggregate-only overflow: each rank fits on its own. - for (const std::size_t bytes : rank_bytes) - { - EXPECT_LE(bytes, 1024u); - } - } - } - reading_atoms = false; - } - else if (reading_atoms) - { - std::istringstream row(line); - int index = -1; - std::string label; - double x = 0.0; - row >> index >> label >> x; - ASSERT_TRUE(row); - EXPECT_EQ(index, atom_count); - const std::string expected_label = atom_count < 6 ? "A" : "B"; - EXPECT_EQ(label, expected_label); - const double expected_x = 0.01 * (atom_count + 1) / ModuleBase::ANGSTROM_AU; - EXPECT_NEAR(x, expected_x, 1.0e-12); - if (frame_count == 1) - { - first_frame.push_back(line); - } - else - { - ASSERT_LT(atom_count, first_frame.size()); - EXPECT_EQ(line, first_frame[atom_count]); - } - frame_bytes += line.size() + 1; - const int owner = (2 * index + 1) % owner_count; - rank_bytes[owner] += line.size() + 1; - ++atom_count; - } - } - EXPECT_EQ(frame_count, 2); - EXPECT_EQ(atom_count, total_atoms); - input.close(); - std::remove(filename.c_str()); - rmdir(directory.c_str()); - } -} - -// One-row, several-row, and normal budgets must produce identical complete frames. -INSTANTIATE_TEST_SUITE_P(BufferBudgets, MdDumpBatchTest, testing::Values(1, 512, 1024, 1048576)); - -TEST(MdDumpMpiTest, BoundedGatherRejectsOversizedBatchesCollectively) -{ - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - const int local_bytes = domain.rank() == 0 ? 17 : 0; - const std::string oversized(local_bytes, 'x'); - EXPECT_THROW(domain.gather_text(oversized, 16), std::length_error); - if (domain.size() > 2) - { - // Every rank fits individually; their aggregate must also be checked on all ranks. - const std::string local(8, 'x'); - EXPECT_THROW(domain.gather_text(local, 16), std::length_error); - } - const std::string empty; - const std::string gathered = domain.gather_text(empty, 1); - EXPECT_TRUE(gathered.empty()); -} - -TEST(MdDumpMpiTest, EmptyRanksCanWriteAnEmptyFrame) -{ - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - ModuleBase::Matrix3 lattice; - lattice.e11 = 1.0; - lattice.e22 = 1.0; - lattice.e33 = 1.0; - const ModuleBase::Matrix3 inverse = lattice.Inverse(); - const std::vector atoms; - const std::vector labels(1, "X"); - const std::vector masses(1, 1.0); - const std::vector counts(1, 0); - MDCell cell; - cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 0, atoms, labels, masses, counts, 0.0, domain); - const std::string directory = make_dump_directory(domain); - ASSERT_FALSE(directory.empty()); - MD_para parameters; - ModuleBase::matrix virial(3, 3); - MD_func::dump_info(0, directory, cell, parameters, false, virial); - if (domain.rank() == 0) - { - const std::string filename = directory + "MD_dump"; - std::ifstream input(filename.c_str()); - ASSERT_TRUE(input); - const std::string contents((std::istreambuf_iterator(input)), std::istreambuf_iterator()); - EXPECT_NE(contents.find("MDSTEP: 0"), std::string::npos); - EXPECT_NE(contents.find("INDEX"), std::string::npos); - input.close(); - std::remove(filename.c_str()); - rmdir(directory.c_str()); - } -} -} // namespace - -int main(int argc, char** argv) -{ - int process_count = 1; - int thread_count = 1; - int rank = 0; - Parallel_Global::read_pal_param(argc, argv, process_count, thread_count, rank); - // These application communicators are not created by this focused test. - POOL_WORLD = MPI_COMM_NULL; - KP_WORLD = MPI_COMM_NULL; - INT_BGROUP = MPI_COMM_NULL; - BP_WORLD = MPI_COMM_NULL; - GRID_WORLD = MPI_COMM_NULL; - DIAG_WORLD = MPI_COMM_NULL; - ::testing::InitGoogleTest(&argc, argv); - const int local_result = RUN_ALL_TESTS(); - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - const int result = domain.max(local_result); - Parallel_Global::finalize_mpi(); - return result; -} diff --git a/source/source_md/test/test_md_func.cpp b/source/source_md/test/test_md_func.cpp index 9e0aef2f6ee..9c55521b640 100644 --- a/source/source_md/test/test_md_func.cpp +++ b/source/source_md/test/test_md_func.cpp @@ -6,8 +6,24 @@ #include +#ifdef __MPI +#include "source_base/parallel_common.h" +#include "source_base/parallel_global.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + namespace { +// Each target retains its existing serial or MPI test set. +#ifndef __MPI class GhostForceSolver : public ModuleESolver::ESolver { public: @@ -75,4 +91,228 @@ TEST(MdForceVirialTest, ReturnsGhostForcesBeforeConvertingUnitsExactlyOnce) } } } +#else +std::string make_dump_directory(const ModuleBase::CommunicationDomain& domain) +{ + std::string directory; + if (domain.rank() == 0) + { + std::string pattern = testing::TempDir() + "abacus-md-dump-XXXXXX"; + char* created = mkdtemp(&pattern[0]); + if (created != nullptr) + { + directory = created; + directory += "/"; + } + } + Parallel_Common::bcast_string(directory); + return directory; +} + +class MdDumpBatchTest : public testing::TestWithParam +{ +}; + +TEST_P(MdDumpBatchTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + ModuleBase::Matrix3 lattice; + lattice.e11 = 1.0; + lattice.e22 = 1.0; + lattice.e33 = 1.0; + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const int owner_count = std::min(domain.size(), 3); + std::vector atoms; + const int total_atoms = 13; + for (int index = total_atoms - 1; index >= 0; --index) + { + const int owner = (2 * index + 1) % owner_count; + if (owner != domain.rank()) + { + continue; + } + LocalAtom atom; + atom.type = index < 6 ? 0 : 1; + atom.type_index = index - 6 * atom.type; + const double x = 0.01 * (index + 1); + atom.frac.set(x, 0.5, 0.5); + atom.cart = atom.frac; + atoms.push_back(atom); + } + const std::vector labels = {"A", "B"}; + const std::vector masses(2, 1.0); + const std::vector counts = {6, 7}; + MDCell cell; + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, total_atoms, atoms, labels, masses, counts, 0.0, domain); + const std::string directory = make_dump_directory(domain); + ASSERT_FALSE(directory.empty()); + MD_para parameters; + ModuleBase::matrix virial(3, 3); + MD_func::dump_info(0, directory, cell, parameters, false, virial); + const int batch_bytes = GetParam(); + MD_func::dump_info(1, directory, cell, parameters, false, virial, batch_bytes); + if (domain.rank() == 0) + { + const std::string filename = directory + "MD_dump"; + std::ifstream input(filename.c_str()); + ASSERT_TRUE(input); + std::string line; + int frame_count = 0; + int atom_count = 0; + std::size_t frame_bytes = 0; + std::vector rank_bytes(domain.size(), 0); + std::vector first_frame; + bool reading_atoms = false; + while (std::getline(input, line)) + { + if (line.find("MDSTEP:") == 0) + { + if (frame_count > 0) + { + EXPECT_EQ(atom_count, total_atoms); + } + ++frame_count; + atom_count = 0; + frame_bytes = 0; + std::fill(rank_bytes.begin(), rank_bytes.end(), 0); + reading_atoms = false; + } + else if (line.find("INDEX") == 0) + { + reading_atoms = true; + } + else if (reading_atoms && line.empty()) + { + EXPECT_EQ(atom_count, total_atoms); + if (batch_bytes == 512) + { + // The whole frame exceeds the injected collective count limit. + EXPECT_GT(frame_bytes, 512u); + } + if (batch_bytes == 1024) + { + EXPECT_GT(frame_bytes, 1024u); + if (domain.size() > 2) + { + // Reproduce the aggregate-only overflow: each rank fits on its own. + for (const std::size_t bytes : rank_bytes) + { + EXPECT_LE(bytes, 1024u); + } + } + } + reading_atoms = false; + } + else if (reading_atoms) + { + std::istringstream row(line); + int index = -1; + std::string label; + double x = 0.0; + row >> index >> label >> x; + ASSERT_TRUE(row); + EXPECT_EQ(index, atom_count); + const std::string expected_label = atom_count < 6 ? "A" : "B"; + EXPECT_EQ(label, expected_label); + const double expected_x = 0.01 * (atom_count + 1) / ModuleBase::ANGSTROM_AU; + EXPECT_NEAR(x, expected_x, 1.0e-12); + if (frame_count == 1) + { + first_frame.push_back(line); + } + else + { + ASSERT_LT(atom_count, first_frame.size()); + EXPECT_EQ(line, first_frame[atom_count]); + } + frame_bytes += line.size() + 1; + const int owner = (2 * index + 1) % owner_count; + rank_bytes[owner] += line.size() + 1; + ++atom_count; + } + } + EXPECT_EQ(frame_count, 2); + EXPECT_EQ(atom_count, total_atoms); + input.close(); + std::remove(filename.c_str()); + rmdir(directory.c_str()); + } +} + +// One-row, several-row, and normal budgets must produce identical complete frames. +INSTANTIATE_TEST_SUITE_P(BufferBudgets, MdDumpBatchTest, testing::Values(1, 512, 1024, 1048576)); + +TEST(MdDumpMpiTest, BoundedGatherRejectsOversizedBatchesCollectively) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + const int local_bytes = domain.rank() == 0 ? 17 : 0; + const std::string oversized(local_bytes, 'x'); + EXPECT_THROW(domain.gather_text(oversized, 16), std::length_error); + if (domain.size() > 2) + { + // Every rank fits individually; their aggregate must also be checked on all ranks. + const std::string local(8, 'x'); + EXPECT_THROW(domain.gather_text(local, 16), std::length_error); + } + const std::string empty; + const std::string gathered = domain.gather_text(empty, 1); + EXPECT_TRUE(gathered.empty()); +} + +TEST(MdDumpMpiTest, EmptyRanksCanWriteAnEmptyFrame) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + ModuleBase::Matrix3 lattice; + lattice.e11 = 1.0; + lattice.e22 = 1.0; + lattice.e33 = 1.0; + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const std::vector atoms; + const std::vector labels(1, "X"); + const std::vector masses(1, 1.0); + const std::vector counts(1, 0); + MDCell cell; + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 0, atoms, labels, masses, counts, 0.0, domain); + const std::string directory = make_dump_directory(domain); + ASSERT_FALSE(directory.empty()); + MD_para parameters; + ModuleBase::matrix virial(3, 3); + MD_func::dump_info(0, directory, cell, parameters, false, virial); + if (domain.rank() == 0) + { + const std::string filename = directory + "MD_dump"; + std::ifstream input(filename.c_str()); + ASSERT_TRUE(input); + const std::string contents((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + EXPECT_NE(contents.find("MDSTEP: 0"), std::string::npos); + EXPECT_NE(contents.find("INDEX"), std::string::npos); + input.close(); + std::remove(filename.c_str()); + rmdir(directory.c_str()); + } +} +#endif } // namespace + +#ifdef __MPI +int main(int argc, char** argv) +{ + int process_count = 1; + int thread_count = 1; + int rank = 0; + Parallel_Global::read_pal_param(argc, argv, process_count, thread_count, rank); + // These application communicators are not created by this focused test. + POOL_WORLD = MPI_COMM_NULL; + KP_WORLD = MPI_COMM_NULL; + INT_BGROUP = MPI_COMM_NULL; + BP_WORLD = MPI_COMM_NULL; + GRID_WORLD = MPI_COMM_NULL; + DIAG_WORLD = MPI_COMM_NULL; + ::testing::InitGoogleTest(&argc, argv); + const int local_result = RUN_ALL_TESTS(); + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + const int result = domain.max(local_result); + Parallel_Global::finalize_mpi(); + return result; +} +#endif From e3714fdad546365f683fb84b483c615fc9a2c04b Mon Sep 17 00:00:00 2001 From: zaotian Date: Tue, 27 Oct 2026 15:48:04 +0800 Subject: [PATCH 11/11] test(md): place MPI dump tests in the standard unittests directory --- source/source_md/CMakeLists.txt | 1 + source/source_md/test/CMakeLists.txt | 26 +-- source/source_md/test/test_md_func.cpp | 240 -------------------- source/source_md/unittests/CMakeLists.txt | 19 ++ source/source_md/unittests/test_md_func.cpp | 240 ++++++++++++++++++++ 5 files changed, 269 insertions(+), 257 deletions(-) create mode 100644 source/source_md/unittests/CMakeLists.txt create mode 100644 source/source_md/unittests/test_md_func.cpp diff --git a/source/source_md/CMakeLists.txt b/source/source_md/CMakeLists.txt index eb7bbf8ee01..ac5d35156ec 100644 --- a/source/source_md/CMakeLists.txt +++ b/source/source_md/CMakeLists.txt @@ -18,5 +18,6 @@ add_subdirectory(module_plumed) if(BUILD_TESTING) if(ENABLE_MPI) add_subdirectory(test) + add_subdirectory(unittests) endif() endif() diff --git a/source/source_md/test/CMakeLists.txt b/source/source_md/test/CMakeLists.txt index 133e6afdc87..36f08d02d41 100644 --- a/source/source_md/test/CMakeLists.txt +++ b/source/source_md/test/CMakeLists.txt @@ -73,6 +73,15 @@ list( ../../source_cell/sep.cpp ../../source_cell/sep_cell.cpp) +# Share canonical source paths with the sibling unittests directory. +set(md_test_depend_files) +foreach(depend_file IN LISTS depend_files) + get_filename_component(depend_path "${depend_file}" ABSOLUTE + BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + list(APPEND md_test_depend_files "${depend_path}") +endforeach() +set(md_test_depend_files "${md_test_depend_files}" PARENT_SCOPE) + addtest( TARGET MODULE_MD_LJ_pot @@ -193,20 +202,3 @@ addtest( ../md_base.cpp ../langevin.cpp ${depend_files}) - -if(ENABLE_MPI) - addtest( - TARGET MODULE_MD_dump_mpi - KEEP_FEATURE_DEFINITIONS __MPI - LIBS parameter psi device - SOURCES test_md_func.cpp ../../source_base/parallel_common.cpp - ../../source_cell/magnetism.cpp ${depend_files}) - add_test( - NAME MODULE_MD_dump_mpi_np4 - COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 ${MPIEXEC_PREFLAGS} - $ ${MPIEXEC_POSTFLAGS} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - set_tests_properties( - MODULE_MD_dump_mpi MODULE_MD_dump_mpi_np4 - PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1;MKL_NUM_THREADS=1;I_MPI_PIN=0" TIMEOUT 60) -endif() diff --git a/source/source_md/test/test_md_func.cpp b/source/source_md/test/test_md_func.cpp index 9c55521b640..9e0aef2f6ee 100644 --- a/source/source_md/test/test_md_func.cpp +++ b/source/source_md/test/test_md_func.cpp @@ -6,24 +6,8 @@ #include -#ifdef __MPI -#include "source_base/parallel_common.h" -#include "source_base/parallel_global.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#endif - namespace { -// Each target retains its existing serial or MPI test set. -#ifndef __MPI class GhostForceSolver : public ModuleESolver::ESolver { public: @@ -91,228 +75,4 @@ TEST(MdForceVirialTest, ReturnsGhostForcesBeforeConvertingUnitsExactlyOnce) } } } -#else -std::string make_dump_directory(const ModuleBase::CommunicationDomain& domain) -{ - std::string directory; - if (domain.rank() == 0) - { - std::string pattern = testing::TempDir() + "abacus-md-dump-XXXXXX"; - char* created = mkdtemp(&pattern[0]); - if (created != nullptr) - { - directory = created; - directory += "/"; - } - } - Parallel_Common::bcast_string(directory); - return directory; -} - -class MdDumpBatchTest : public testing::TestWithParam -{ -}; - -TEST_P(MdDumpBatchTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) -{ - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - ModuleBase::Matrix3 lattice; - lattice.e11 = 1.0; - lattice.e22 = 1.0; - lattice.e33 = 1.0; - const ModuleBase::Matrix3 inverse = lattice.Inverse(); - const int owner_count = std::min(domain.size(), 3); - std::vector atoms; - const int total_atoms = 13; - for (int index = total_atoms - 1; index >= 0; --index) - { - const int owner = (2 * index + 1) % owner_count; - if (owner != domain.rank()) - { - continue; - } - LocalAtom atom; - atom.type = index < 6 ? 0 : 1; - atom.type_index = index - 6 * atom.type; - const double x = 0.01 * (index + 1); - atom.frac.set(x, 0.5, 0.5); - atom.cart = atom.frac; - atoms.push_back(atom); - } - const std::vector labels = {"A", "B"}; - const std::vector masses(2, 1.0); - const std::vector counts = {6, 7}; - MDCell cell; - cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, total_atoms, atoms, labels, masses, counts, 0.0, domain); - const std::string directory = make_dump_directory(domain); - ASSERT_FALSE(directory.empty()); - MD_para parameters; - ModuleBase::matrix virial(3, 3); - MD_func::dump_info(0, directory, cell, parameters, false, virial); - const int batch_bytes = GetParam(); - MD_func::dump_info(1, directory, cell, parameters, false, virial, batch_bytes); - if (domain.rank() == 0) - { - const std::string filename = directory + "MD_dump"; - std::ifstream input(filename.c_str()); - ASSERT_TRUE(input); - std::string line; - int frame_count = 0; - int atom_count = 0; - std::size_t frame_bytes = 0; - std::vector rank_bytes(domain.size(), 0); - std::vector first_frame; - bool reading_atoms = false; - while (std::getline(input, line)) - { - if (line.find("MDSTEP:") == 0) - { - if (frame_count > 0) - { - EXPECT_EQ(atom_count, total_atoms); - } - ++frame_count; - atom_count = 0; - frame_bytes = 0; - std::fill(rank_bytes.begin(), rank_bytes.end(), 0); - reading_atoms = false; - } - else if (line.find("INDEX") == 0) - { - reading_atoms = true; - } - else if (reading_atoms && line.empty()) - { - EXPECT_EQ(atom_count, total_atoms); - if (batch_bytes == 512) - { - // The whole frame exceeds the injected collective count limit. - EXPECT_GT(frame_bytes, 512u); - } - if (batch_bytes == 1024) - { - EXPECT_GT(frame_bytes, 1024u); - if (domain.size() > 2) - { - // Reproduce the aggregate-only overflow: each rank fits on its own. - for (const std::size_t bytes : rank_bytes) - { - EXPECT_LE(bytes, 1024u); - } - } - } - reading_atoms = false; - } - else if (reading_atoms) - { - std::istringstream row(line); - int index = -1; - std::string label; - double x = 0.0; - row >> index >> label >> x; - ASSERT_TRUE(row); - EXPECT_EQ(index, atom_count); - const std::string expected_label = atom_count < 6 ? "A" : "B"; - EXPECT_EQ(label, expected_label); - const double expected_x = 0.01 * (atom_count + 1) / ModuleBase::ANGSTROM_AU; - EXPECT_NEAR(x, expected_x, 1.0e-12); - if (frame_count == 1) - { - first_frame.push_back(line); - } - else - { - ASSERT_LT(atom_count, first_frame.size()); - EXPECT_EQ(line, first_frame[atom_count]); - } - frame_bytes += line.size() + 1; - const int owner = (2 * index + 1) % owner_count; - rank_bytes[owner] += line.size() + 1; - ++atom_count; - } - } - EXPECT_EQ(frame_count, 2); - EXPECT_EQ(atom_count, total_atoms); - input.close(); - std::remove(filename.c_str()); - rmdir(directory.c_str()); - } -} - -// One-row, several-row, and normal budgets must produce identical complete frames. -INSTANTIATE_TEST_SUITE_P(BufferBudgets, MdDumpBatchTest, testing::Values(1, 512, 1024, 1048576)); - -TEST(MdDumpMpiTest, BoundedGatherRejectsOversizedBatchesCollectively) -{ - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - const int local_bytes = domain.rank() == 0 ? 17 : 0; - const std::string oversized(local_bytes, 'x'); - EXPECT_THROW(domain.gather_text(oversized, 16), std::length_error); - if (domain.size() > 2) - { - // Every rank fits individually; their aggregate must also be checked on all ranks. - const std::string local(8, 'x'); - EXPECT_THROW(domain.gather_text(local, 16), std::length_error); - } - const std::string empty; - const std::string gathered = domain.gather_text(empty, 1); - EXPECT_TRUE(gathered.empty()); -} - -TEST(MdDumpMpiTest, EmptyRanksCanWriteAnEmptyFrame) -{ - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - ModuleBase::Matrix3 lattice; - lattice.e11 = 1.0; - lattice.e22 = 1.0; - lattice.e33 = 1.0; - const ModuleBase::Matrix3 inverse = lattice.Inverse(); - const std::vector atoms; - const std::vector labels(1, "X"); - const std::vector masses(1, 1.0); - const std::vector counts(1, 0); - MDCell cell; - cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 0, atoms, labels, masses, counts, 0.0, domain); - const std::string directory = make_dump_directory(domain); - ASSERT_FALSE(directory.empty()); - MD_para parameters; - ModuleBase::matrix virial(3, 3); - MD_func::dump_info(0, directory, cell, parameters, false, virial); - if (domain.rank() == 0) - { - const std::string filename = directory + "MD_dump"; - std::ifstream input(filename.c_str()); - ASSERT_TRUE(input); - const std::string contents((std::istreambuf_iterator(input)), std::istreambuf_iterator()); - EXPECT_NE(contents.find("MDSTEP: 0"), std::string::npos); - EXPECT_NE(contents.find("INDEX"), std::string::npos); - input.close(); - std::remove(filename.c_str()); - rmdir(directory.c_str()); - } -} -#endif } // namespace - -#ifdef __MPI -int main(int argc, char** argv) -{ - int process_count = 1; - int thread_count = 1; - int rank = 0; - Parallel_Global::read_pal_param(argc, argv, process_count, thread_count, rank); - // These application communicators are not created by this focused test. - POOL_WORLD = MPI_COMM_NULL; - KP_WORLD = MPI_COMM_NULL; - INT_BGROUP = MPI_COMM_NULL; - BP_WORLD = MPI_COMM_NULL; - GRID_WORLD = MPI_COMM_NULL; - DIAG_WORLD = MPI_COMM_NULL; - ::testing::InitGoogleTest(&argc, argv); - const int local_result = RUN_ALL_TESTS(); - const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); - const int result = domain.max(local_result); - Parallel_Global::finalize_mpi(); - return result; -} -#endif diff --git a/source/source_md/unittests/CMakeLists.txt b/source/source_md/unittests/CMakeLists.txt new file mode 100644 index 00000000000..14e6a4fe706 --- /dev/null +++ b/source/source_md/unittests/CMakeLists.txt @@ -0,0 +1,19 @@ +abacus_disable_feature_definitions(__MPI __LCAO) +abacus_add_local_feature_definitions(__NORMAL) + +if(ENABLE_MPI) + addtest( + TARGET MODULE_MD_dump_mpi + KEEP_FEATURE_DEFINITIONS __MPI + LIBS parameter psi device + SOURCES test_md_func.cpp ../../source_base/parallel_common.cpp + ../../source_cell/magnetism.cpp ${md_test_depend_files}) + add_test( + NAME MODULE_MD_dump_mpi_np4 + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 ${MPIEXEC_PREFLAGS} + $ ${MPIEXEC_POSTFLAGS} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + set_tests_properties( + MODULE_MD_dump_mpi MODULE_MD_dump_mpi_np4 + PROPERTIES ENVIRONMENT "OMP_NUM_THREADS=1;MKL_NUM_THREADS=1;I_MPI_PIN=0" TIMEOUT 60) +endif() diff --git a/source/source_md/unittests/test_md_func.cpp b/source/source_md/unittests/test_md_func.cpp new file mode 100644 index 00000000000..986dec221ab --- /dev/null +++ b/source/source_md/unittests/test_md_func.cpp @@ -0,0 +1,240 @@ +#include "source_md/md_func.h" +#include "source_base/parallel_cell.h" +#include "source_base/parallel_common.h" +#include "source_base/parallel_global.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +std::string make_dump_directory(const ModuleBase::CommunicationDomain& domain) +{ + std::string directory; + if (domain.rank() == 0) + { + std::string pattern = testing::TempDir() + "abacus-md-dump-XXXXXX"; + char* created = mkdtemp(&pattern[0]); + if (created != nullptr) + { + directory = created; + directory += "/"; + } + } + Parallel_Common::bcast_string(directory); + return directory; +} + +class MdDumpBatchTest : public testing::TestWithParam +{ +}; + +TEST_P(MdDumpBatchTest, OrdersAtomsAcrossRanksAndAppendsCompleteFrames) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + ModuleBase::Matrix3 lattice; + lattice.e11 = 1.0; + lattice.e22 = 1.0; + lattice.e33 = 1.0; + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const int owner_count = std::min(domain.size(), 3); + std::vector atoms; + const int total_atoms = 13; + for (int index = total_atoms - 1; index >= 0; --index) + { + const int owner = (2 * index + 1) % owner_count; + if (owner != domain.rank()) + { + continue; + } + LocalAtom atom; + atom.type = index < 6 ? 0 : 1; + atom.type_index = index - 6 * atom.type; + const double x = 0.01 * (index + 1); + atom.frac.set(x, 0.5, 0.5); + atom.cart = atom.frac; + atoms.push_back(atom); + } + const std::vector labels = {"A", "B"}; + const std::vector masses(2, 1.0); + const std::vector counts = {6, 7}; + MDCell cell; + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, total_atoms, atoms, labels, masses, counts, 0.0, domain); + const std::string directory = make_dump_directory(domain); + ASSERT_FALSE(directory.empty()); + MD_para parameters; + ModuleBase::matrix virial(3, 3); + MD_func::dump_info(0, directory, cell, parameters, false, virial); + const int batch_bytes = GetParam(); + MD_func::dump_info(1, directory, cell, parameters, false, virial, batch_bytes); + if (domain.rank() == 0) + { + const std::string filename = directory + "MD_dump"; + std::ifstream input(filename.c_str()); + ASSERT_TRUE(input); + std::string line; + int frame_count = 0; + int atom_count = 0; + std::size_t frame_bytes = 0; + std::vector rank_bytes(domain.size(), 0); + std::vector first_frame; + bool reading_atoms = false; + while (std::getline(input, line)) + { + if (line.find("MDSTEP:") == 0) + { + if (frame_count > 0) + { + EXPECT_EQ(atom_count, total_atoms); + } + ++frame_count; + atom_count = 0; + frame_bytes = 0; + std::fill(rank_bytes.begin(), rank_bytes.end(), 0); + reading_atoms = false; + } + else if (line.find("INDEX") == 0) + { + reading_atoms = true; + } + else if (reading_atoms && line.empty()) + { + EXPECT_EQ(atom_count, total_atoms); + if (batch_bytes == 512) + { + // The whole frame exceeds the injected collective count limit. + EXPECT_GT(frame_bytes, 512u); + } + if (batch_bytes == 1024) + { + EXPECT_GT(frame_bytes, 1024u); + if (domain.size() > 2) + { + // Reproduce the aggregate-only overflow: each rank fits on its own. + for (const std::size_t bytes : rank_bytes) + { + EXPECT_LE(bytes, 1024u); + } + } + } + reading_atoms = false; + } + else if (reading_atoms) + { + std::istringstream row(line); + int index = -1; + std::string label; + double x = 0.0; + row >> index >> label >> x; + ASSERT_TRUE(row); + EXPECT_EQ(index, atom_count); + const std::string expected_label = atom_count < 6 ? "A" : "B"; + EXPECT_EQ(label, expected_label); + const double expected_x = 0.01 * (atom_count + 1) / ModuleBase::ANGSTROM_AU; + EXPECT_NEAR(x, expected_x, 1.0e-12); + if (frame_count == 1) + { + first_frame.push_back(line); + } + else + { + ASSERT_LT(atom_count, first_frame.size()); + EXPECT_EQ(line, first_frame[atom_count]); + } + frame_bytes += line.size() + 1; + const int owner = (2 * index + 1) % owner_count; + rank_bytes[owner] += line.size() + 1; + ++atom_count; + } + } + EXPECT_EQ(frame_count, 2); + EXPECT_EQ(atom_count, total_atoms); + input.close(); + std::remove(filename.c_str()); + rmdir(directory.c_str()); + } +} + +// One-row, several-row, and normal budgets must produce identical complete frames. +INSTANTIATE_TEST_SUITE_P(BufferBudgets, MdDumpBatchTest, testing::Values(1, 512, 1024, 1048576)); + +TEST(MdDumpMpiTest, BoundedGatherRejectsOversizedBatchesCollectively) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + const int local_bytes = domain.rank() == 0 ? 17 : 0; + const std::string oversized(local_bytes, 'x'); + EXPECT_THROW(domain.gather_text(oversized, 16), std::length_error); + if (domain.size() > 2) + { + // Every rank fits individually; their aggregate must also be checked on all ranks. + const std::string local(8, 'x'); + EXPECT_THROW(domain.gather_text(local, 16), std::length_error); + } + const std::string empty; + const std::string gathered = domain.gather_text(empty, 1); + EXPECT_TRUE(gathered.empty()); +} + +TEST(MdDumpMpiTest, EmptyRanksCanWriteAnEmptyFrame) +{ + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + ModuleBase::Matrix3 lattice; + lattice.e11 = 1.0; + lattice.e22 = 1.0; + lattice.e33 = 1.0; + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const std::vector atoms; + const std::vector labels(1, "X"); + const std::vector masses(1, 1.0); + const std::vector counts(1, 0); + MDCell cell; + cell.initialize_from_owned_atoms(lattice, inverse, 1.0, 1.0, 0, atoms, labels, masses, counts, 0.0, domain); + const std::string directory = make_dump_directory(domain); + ASSERT_FALSE(directory.empty()); + MD_para parameters; + ModuleBase::matrix virial(3, 3); + MD_func::dump_info(0, directory, cell, parameters, false, virial); + if (domain.rank() == 0) + { + const std::string filename = directory + "MD_dump"; + std::ifstream input(filename.c_str()); + ASSERT_TRUE(input); + const std::string contents((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + EXPECT_NE(contents.find("MDSTEP: 0"), std::string::npos); + EXPECT_NE(contents.find("INDEX"), std::string::npos); + input.close(); + std::remove(filename.c_str()); + rmdir(directory.c_str()); + } +} +} // namespace + +int main(int argc, char** argv) +{ + int process_count = 1; + int thread_count = 1; + int rank = 0; + Parallel_Global::read_pal_param(argc, argv, process_count, thread_count, rank); + // These application communicators are not created by this focused test. + POOL_WORLD = MPI_COMM_NULL; + KP_WORLD = MPI_COMM_NULL; + INT_BGROUP = MPI_COMM_NULL; + BP_WORLD = MPI_COMM_NULL; + GRID_WORLD = MPI_COMM_NULL; + DIAG_WORLD = MPI_COMM_NULL; + ::testing::InitGoogleTest(&argc, argv); + const int local_result = RUN_ALL_TESTS(); + const ModuleBase::CommunicationDomain domain = ModuleBase::world_comm_domain(); + const int result = domain.max(local_result); + Parallel_Global::finalize_mpi(); + return result; +}