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/source/source_base/parallel_cell.cpp b/source/source_base/parallel_cell.cpp index 1f13e552d4a..0531e288ce6 100644 --- a/source/source_base/parallel_cell.cpp +++ b/source/source_base/parallel_cell.cpp @@ -1,5 +1,8 @@ #include "source_base/parallel_cell.h" +#include +#include + namespace ModuleBase { #ifdef __MPI @@ -69,6 +72,63 @@ double CommunicationDomain::max(double value) const return value; } +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 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] > count_limit) + { + 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 batch limit."); + } + 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..bd02ccaa035 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,9 @@ class CommunicationDomain int max(int value) const; int sum(int value) const; double max(double 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/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/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/md_func.cpp b/source/source_md/md_func.cpp index a549e87732c..3536dd4a0b7 100644 --- a/source/source_md/md_func.cpp +++ b/source/source_md/md_func.cpp @@ -2,34 +2,21 @@ #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 +#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() { @@ -407,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, @@ -414,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; @@ -441,44 +481,102 @@ 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); + } } - local << "\n\n"; + 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 - 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 + if (domain.max(local_overflow) != 0) + { + throw std::length_error("A single MD_dump row exceeds the MPI count range."); + } + 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) + { + 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", "cannot open MD_dump."); + } + 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) + { + 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 (domain.rank() == 0) + { + 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/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/CMakeLists.txt b/source/source_md/test/CMakeLists.txt index 3392af2467c..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 @@ -147,6 +156,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 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; +} 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/10_NO_HHG_TDDFT/INPUT b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/INPUT similarity index 56% rename from tests/05_rtTDDFT/10_NO_HHG_TDDFT/INPUT rename to tests/05_rtTDDFT/10_NO_MD_boundary_shift/INPUT index 668ee118580..6ba92a2a4e5 100644 --- a/tests/05_rtTDDFT/10_NO_HHG_TDDFT/INPUT +++ b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/INPUT @@ -1,5 +1,4 @@ -INPUT_PARAMETERS -#Parameters (General) +INPUT_PARAMETERS suffix autotest pseudo_dir ../../PP_ORB orbital_dir ../../PP_ORB @@ -7,39 +6,38 @@ calculation md esolver_type tddft basis_type lcao gamma_only 0 -nbands 5 +symmetry -1 +nbands 2 ecutwfc 20 -#Parameter (Electronic Structure) -scf_nmax 50 - +scf_thr 1e-8 +scf_nmax 100 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 +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 +# Keep the early-time fields weak to preserve the boundary regression. td_vext 1 -td_vext_dire 2*1 +td_vext_dire 2 3 td_stype 0 td_ttype 2*0 -td_tstart 1 +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*0 -td_gauss_sigma 2*30 +td_gauss_t0 2*70 +td_gauss_sigma 2*0.5 diff --git a/tests/05_rtTDDFT/10_NO_MD_boundary_shift/KPT b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/KPT new file mode 100644 index 00000000000..5184d98cf62 --- /dev/null +++ b/tests/05_rtTDDFT/10_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/10_NO_MD_boundary_shift/README b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/README new file mode 100644 index 00000000000..0e2ac070bbe --- /dev/null +++ b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/README @@ -0,0 +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/10_NO_MD_boundary_shift/STRU b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/STRU new file mode 100644 index 00000000000..667f27b620d --- /dev/null +++ b/tests/05_rtTDDFT/10_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/10_NO_MD_boundary_shift/result.ref b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/result.ref new file mode 100644 index 00000000000..deb20c48fb5 --- /dev/null +++ b/tests/05_rtTDDFT/10_NO_MD_boundary_shift/result.ref @@ -0,0 +1,3 @@ +etotref -30.89775847665366 +etotperatomref -15.4488792383 +totaltimeref 0.58 diff --git a/tests/05_rtTDDFT/CASES_CPU.txt b/tests/05_rtTDDFT/CASES_CPU.txt index 949a7e13a1a..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 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