diff --git a/CMakeLists.txt b/CMakeLists.txt index fb0d9b38a2d..0dd2d92795f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,14 @@ cmake_dependent_option(ENABLE_PEXSI "Enable PEXSI for LCAO" OFF "ENABLE_LCAO;ENABLE_MPI" OFF) cmake_dependent_option(ENABLE_MLALGO "Enable the machine learning algorithms" OFF "ENABLE_LCAO;ENABLE_MPI" OFF) +# Optional external GreenX minimax API for the Delta-Sternheimer solver. Off by +# default so a stock build needs neither Fortran nor GreenX. +cmake_dependent_option( + ENABLE_GREENX_MINIMAX + "Enable the external GreenX minimax API for Delta-Sternheimer" OFF + "ENABLE_LCAO;ENABLE_MPI;ENABLE_LIBRI" OFF) +set(GREENX_DIR "" CACHE PATH + "GreenX installation prefix or CMake package directory") # Two-center FFT is only used in LCAO cmake_dependent_option( @@ -635,6 +643,11 @@ if(ENABLE_LIBRI) find_package(cereal REQUIRED CONFIG) endif() +if(ENABLE_GREENX_MINIMAX) + enable_language(Fortran) + find_package(GreenX REQUIRED) +endif() + if(ENABLE_LIBXC) find_package(Libxc CONFIG REQUIRED) if(Libxc_VERSION VERSION_LESS "5.1.7") diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 941f739d5d6..bc0e30e5ebe 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -836,7 +836,7 @@ endif() if(ENABLE_LIBRI) target_link_libraries(${ABACUS_BIN_NAME} PRIVATE ri module_exx_symmetry) if(ENABLE_LCAO) - target_link_libraries(${ABACUS_BIN_NAME} PRIVATE rpa librpa_io) + target_link_libraries(${ABACUS_BIN_NAME} PRIVATE rpa delta_st librpa_io) endif() endif() if(ENABLE_JSON) diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 4618935b562..5e39c55e52f 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -82,6 +82,7 @@ VPATH=./src_global:\ ./source_lcao/module_ri:\ ./source_lcao/module_ri/module_exx_symmetry:\ ./source_lcao/module_rpa:\ +./source_lcao/module_rpa/module_delta_st:\ ./source_lcao/module_rpa/module_librpa_io:\ ./source_lcao/module_bse:\ ./source_io/module_parameter:\ @@ -937,6 +938,46 @@ OBJS_MODULE_RPA=rpa_abfs_preorth.o\ rpa_lri_basis.o\ rpa_lri_eigenvector_v1.o\ rpa_lri_eigenvector.o\ + sternheimer_abacus_dft_reference.o\ + sternheimer_abacus_dft_zero_order.o\ + sternheimer_abacus_fd_adapter.o\ + sternheimer_abacus_fd_bindings.o\ + sternheimer_abacus_fd_nonlocal.o\ + sternheimer_abacus_fd_smoke.o\ + sternheimer_abacus_st_smoke.o\ + sternheimer_abfs_perturbation.o\ + sternheimer_abfs_strict2d.o\ + sternheimer_channel_resources.o\ + sternheimer_chi0_mpi.o\ + sternheimer_coulomb_whitening.o\ + sternheimer_delta.o\ + sternheimer_dft_zero_order.o\ + sternheimer_fd_hamiltonian.o\ + sternheimer_fd_nonlocal_projector.o\ + sternheimer_fd_preconditioner.o\ + sternheimer_fd_projector_sampler.o\ + sternheimer_fd_solver.o\ + sternheimer_galerkin_audit.o\ + sternheimer_galerkin_operator.o\ + sternheimer_grid_diagnostics.o\ + sternheimer_grid_transfer.o\ + sternheimer_kq.o\ + sternheimer_periodic_solver.o\ + sternheimer_response_grid.o\ + sternheimer_rpa.o\ + sternheimer_runtime_options.o\ + sternheimer_siab_memory.o\ + sternheimer_siab_mpi.o\ + sternheimer_siab_overlap.o\ + sternheimer_siab_provenance.o\ + sternheimer_siab_writer.o\ + sternheimer_supercell_perturbation.o\ + sternheimer_supercell_sector.o\ + sternheimer_wavefunction_diagnostic.o\ + sternheimer_weak_augmented.o\ + sternheimer_weak_grid.o\ + sternheimer_weak_preconditioner.o\ + sternheimer_weak_q_reference.o\ OBJS_BSE=esolver_lr_lcao_bse.o\ hamilt_bse.o\ diff --git a/source/source_io/module_bessel/numerical_basis.cpp b/source/source_io/module_bessel/numerical_basis.cpp index 7226fd5825d..a7108126594 100644 --- a/source/source_io/module_bessel/numerical_basis.cpp +++ b/source/source_io/module_bessel/numerical_basis.cpp @@ -15,6 +15,10 @@ #include #include #include +#include +#include +#include +#include Numerical_Basis::Numerical_Basis() { } @@ -55,6 +59,269 @@ Numerical_Basis::~Numerical_Basis() // this->numerical_atomic_wfc(ik, wfcpw, psi, sf, ucell); // } +int Numerical_Basis::siab_abacus_m(const int conventional_m) +{ + if (conventional_m == 0) + { + return 0; + } + return conventional_m > 0 ? 2 * conventional_m - 1 : -2 * conventional_m; +} + +Numerical_Basis::SIABPrimitiveParameters +Numerical_Basis::siab_parameters_from_input(const SIABPrimitiveInputs& settings, const int rcut_index, const int lmax) +{ + if (rcut_index < 0 || rcut_index >= static_cast(settings.rcuts.size())) + { + throw std::out_of_range("SIAB primitive rcut index is out of range"); + } + const double primitive_ecut_ry + = settings.ecut_spec == "default" ? settings.ecutwfc : std::stod(settings.ecut_spec); + if (lmax < -1) + { + throw std::invalid_argument("SIAB primitive lmax must be -1 or non-negative"); + } + return SIABPrimitiveParameters{ + primitive_ecut_ry, + settings.rcuts[rcut_index], + settings.smooth, + settings.sigma, + settings.tolerance, + lmax, + }; +} + +void Numerical_Basis::initialize_siab_basis(const UnitCell& ucell, + const SIABPrimitiveParameters& parameters) +{ + if (!std::isfinite(parameters.ecut_ry) || !std::isfinite(parameters.rcut_bohr) + || !std::isfinite(parameters.sigma) || !std::isfinite(parameters.tolerance) + || !(parameters.ecut_ry > 0.0) || !(parameters.rcut_bohr > 0.0) + || (parameters.smooth && !(parameters.sigma > 0.0)) || !(parameters.tolerance > 0.0)) + { + throw std::invalid_argument("SIAB primitive parameters are invalid"); + } + + const int target_lmax = parameters.lmax < 0 ? ucell.lmax : parameters.lmax; + const std::vector basis_shape = Numerical_Basis::siab_basis_shape_signature(ucell); + const bool same_parameters = this->initialized_siab_parameters.ecut_ry == parameters.ecut_ry + && this->initialized_siab_parameters.rcut_bohr == parameters.rcut_bohr + && this->initialized_siab_parameters.smooth == parameters.smooth + && this->initialized_siab_parameters.sigma == parameters.sigma + && this->initialized_siab_parameters.tolerance == parameters.tolerance + && this->initialized_siab_parameters.lmax == parameters.lmax; + if (this->init_label && same_parameters && this->initialized_siab_ntype == ucell.ntype + && this->initialized_siab_lmax == target_lmax && this->initialized_siab_nmax == ucell.nmax + && this->initialized_siab_basis_shape == basis_shape) + { + return; + } + + this->bessel_basis.init(false, + parameters.ecut_ry, + ucell.ntype, + target_lmax, + parameters.smooth, + parameters.sigma, + parameters.rcut_bohr, + parameters.tolerance, + ucell); + this->mu_index = this->init_mu_index(ucell); + this->initialized_siab_parameters = parameters; + this->initialized_siab_ntype = ucell.ntype; + this->initialized_siab_lmax = target_lmax; + this->initialized_siab_nmax = ucell.nmax; + this->initialized_siab_basis_shape = basis_shape; + this->init_label = true; +} + +std::vector Numerical_Basis::siab_basis_shape_signature(const UnitCell& ucell) +{ + std::vector signature; + for (int type = 0; type < ucell.ntype; ++type) + { + signature.push_back(ucell.atoms[type].na); + signature.push_back(ucell.atoms[type].nwl); + signature.push_back(static_cast(ucell.atoms[type].l_nchi.size())); + signature.insert(signature.end(), ucell.atoms[type].l_nchi.begin(), ucell.atoms[type].l_nchi.end()); + } + return signature; +} + +void Numerical_Basis::fill_reciprocal_primitive( + const int l, + const int abacus_m, + const int primitive_index, + const std::complex* structure_factor, + const ModuleBase::realArray& flq, + const ModuleBase::matrix& ylm, + const std::vector& gpow, + const double normalization, + std::vector>& values) +{ + const int npw = static_cast(gpow.size()); + if (values.size() < static_cast(npw)) + { + throw std::invalid_argument("SIAB reciprocal primitive storage is too small"); + } + + const int lm = l * l + abacus_m; + const std::complex lphase = normalization * std::pow(ModuleBase::IMAG_UNIT, -l); + for (int ig = 0; ig < npw; ++ig) + { + values[ig] = lphase * structure_factor[ig] * ylm(lm, ig) * flq(l, primitive_index, ig) * gpow[ig]; + } +} + +std::vector Numerical_Basis::siab_primitive_reciprocal_values( + const int ik, + const ModulePW::PW_Basis_K* wfcpw, + const Structure_Factor& sf, + const UnitCell& ucell, + const SIABPrimitiveParameters& parameters) +{ + if (wfcpw == nullptr) + { + throw std::invalid_argument("SIAB reciprocal primitives require a PW basis"); + } + if (ik < 0 || ik >= wfcpw->nks) + { + throw std::out_of_range("SIAB primitive k-point index is out of range"); + } + if (!(ucell.omega > 0.0)) + { + throw std::invalid_argument("SIAB reciprocal primitives require a positive cell volume"); + } + + this->initialize_siab_basis(ucell, parameters); + const int target_lmax = parameters.lmax < 0 ? ucell.lmax : parameters.lmax; + + const int npw = wfcpw->npwk[ik]; + std::unique_ptr flq; + std::unique_ptr ylm; + std::vector gpow; + if (npw > 0) + { + std::vector> gk(npw); + for (int ig = 0; ig < npw; ++ig) + { + gk[ig] = wfcpw->getgpluskcar(ik, ig) * ucell.tpiba; + } + flq.reset(new ModuleBase::realArray(this->cal_flq(gk, target_lmax))); + ylm.reset(new ModuleBase::matrix(Numerical_Basis::cal_ylm(gk, target_lmax))); + gpow.assign(npw, 1.0); + } + const double normalization = 4.0 * ModuleBase::PI / std::sqrt(ucell.omega); + const int nprimitive = this->bessel_basis.get_ecut_number(); + + std::vector blocks; + int offset = 0; + int global_atom = 0; + for (int type = 0; type < ucell.ntype; ++type) + { + for (int atom = 0; atom < ucell.atoms[type].na; ++atom, ++global_atom) + { + std::unique_ptr[]> sk(sf.get_sk(ik, type, atom, wfcpw)); + for (int l = 0; l <= target_lmax; ++l) + { + for (int conventional_m = -l; conventional_m <= l; ++conventional_m) + { + SIABPrimitiveReciprocalBlock block; + block.type_index = type; + block.element = ucell.atoms[type].label; + block.atom_index = global_atom; + block.l = l; + block.m = conventional_m; + block.n_primitive = nprimitive; + block.offset = offset; + block.values.reserve(nprimitive); + + const int abacus_m = Numerical_Basis::siab_abacus_m(conventional_m); + for (int ie = 0; ie < nprimitive; ++ie) + { + std::vector> reciprocal(static_cast(npw), ModuleBase::ZERO); + if (npw > 0) + { + Numerical_Basis::fill_reciprocal_primitive(l, + abacus_m, + ie, + sk.get(), + *flq, + *ylm, + gpow, + normalization, + reciprocal); + } + + block.values.push_back(std::move(reciprocal)); + } + blocks.push_back(std::move(block)); + offset += nprimitive; + } + } + } + } + return blocks; +} + +std::vector Numerical_Basis::siab_primitive_grid_values( + const int ik, + const ModulePW::PW_Basis_K* wfcpw, + const Structure_Factor& sf, + const UnitCell& ucell, + const SIABPrimitiveParameters& parameters) +{ + if (wfcpw == nullptr) + { + throw std::invalid_argument("SIAB primitive grid requires a PW basis"); + } + const auto reciprocal_blocks + = this->siab_primitive_reciprocal_values(ik, wfcpw, sf, ucell, parameters); + const double real_space_scale = 1.0 / std::sqrt(ucell.omega); + std::vector blocks; + blocks.reserve(reciprocal_blocks.size()); + for (const SIABPrimitiveReciprocalBlock& reciprocal_block: reciprocal_blocks) + { + SIABPrimitiveGridBlock block; + block.type_index = reciprocal_block.type_index; + block.element = reciprocal_block.element; + block.atom_index = reciprocal_block.atom_index; + block.l = reciprocal_block.l; + block.m = reciprocal_block.m; + block.n_primitive = reciprocal_block.n_primitive; + block.offset = reciprocal_block.offset; + block.values.reserve(reciprocal_block.values.size()); + for (const std::vector>& reciprocal: reciprocal_block.values) + { + std::vector> local_grid(static_cast(wfcpw->nrxx), ModuleBase::ZERO); + if (wfcpw->gamma_only) + { + std::vector local_real(static_cast(std::max(1, wfcpw->nrxx)), 0.0); + wfcpw->recip2real(reciprocal.data(), local_real.data(), ik); + for (int ir = 0; ir < wfcpw->nrxx; ++ir) + { + local_grid[static_cast(ir)] + = std::complex(local_real[static_cast(ir)] * real_space_scale, 0.0); + } + } + else + { + std::vector> local_complex( + static_cast(std::max(1, wfcpw->nrxx)), ModuleBase::ZERO); + wfcpw->recip2real(reciprocal.data(), local_complex.data(), ik); + for (int ir = 0; ir < wfcpw->nrxx; ++ir) + { + local_grid[static_cast(ir)] + = local_complex[static_cast(ir)] * real_space_scale; + } + } + block.values.push_back(std::move(local_grid)); + } + blocks.push_back(std::move(block)); + } + return blocks; +} + std::vector Numerical_Basis::init_mu_index(const UnitCell& ucell) { GlobalV::ofs_running << " Initialize the mu index" << std::endl; diff --git a/source/source_io/module_bessel/numerical_basis.h b/source/source_io/module_bessel/numerical_basis.h index f7a13c7e531..94bbd210f79 100644 --- a/source/source_io/module_bessel/numerical_basis.h +++ b/source/source_io/module_bessel/numerical_basis.h @@ -5,6 +5,8 @@ //========================================================== #ifndef NUMERICAL_BASIS_H #define NUMERICAL_BASIS_H +#include +#include #include #include "bessel_basis.h" @@ -32,6 +34,111 @@ class Numerical_Basis Numerical_Basis(); ~Numerical_Basis(); + /// Numerical-Bessel (SIAB) primitive parameters carried through the + /// SIAB export path. They mirror the bessel_nao_* input settings that the + /// existing spillage path already uses. + struct SIABPrimitiveParameters + { + SIABPrimitiveParameters() + : ecut_ry(0.0), rcut_bohr(0.0), smooth(false), sigma(0.0), tolerance(0.0), lmax(-1) + { + } + + SIABPrimitiveParameters(const double ecut_ry_in, + const double rcut_bohr_in, + const bool smooth_in, + const double sigma_in, + const double tolerance_in, + const int lmax_in = -1) + : ecut_ry(ecut_ry_in), + rcut_bohr(rcut_bohr_in), + smooth(smooth_in), + sigma(sigma_in), + tolerance(tolerance_in), + lmax(lmax_in) + { + } + + double ecut_ry; + double rcut_bohr; + bool smooth; + double sigma; + double tolerance; + int lmax; + }; + + /// Rank-local SIAB primitive values sampled on the real-space FFT grid. + struct SIABPrimitiveGridBlock + { + int type_index; + std::string element; + int atom_index; + int l; + int m; + int n_primitive; + int offset; + /// Rank-local values ordered by primitive index: values[ie][local_grid_index]. + std::vector>> values; + }; + + /// Physically normalized SIAB primitive coefficients in the PW basis. + struct SIABPrimitiveReciprocalBlock + { + int type_index; + std::string element; + int atom_index; + int l; + int m; + int n_primitive; + int offset; + /// Physically normalized PW coefficients ordered by primitive index. + std::vector>> values; + }; + + /// Caller-provided numerical-Bessel settings for the SIAB primitive path. + /// Passed explicitly so this module does not read process-global INPUT state. + struct SIABPrimitiveInputs + { + SIABPrimitiveInputs() + : ecut_spec("default"), ecutwfc(0.0), smooth(false), sigma(0.0), tolerance(0.0) + { + } + + /// bessel_nao_rcuts: candidate primitive cutoffs in Bohr. + std::vector rcuts; + /// bessel_nao_ecut: either the literal "default" or a numeric cutoff string. + std::string ecut_spec; + /// ecutwfc: wavefunction cutoff in Ry, used when ecut_spec is "default". + double ecutwfc; + /// bessel_nao_smooth / bessel_nao_sigma / bessel_nao_tolerence. + bool smooth; + double sigma; + double tolerance; + }; + + /// Return the ABACUS real-spherical-harmonic index for a conventional m. + static int siab_abacus_m(const int conventional_m); + + /// Build the primitive parameters from explicitly supplied numerical-Bessel settings. + static SIABPrimitiveParameters siab_parameters_from_input(const SIABPrimitiveInputs& settings, + const int rcut_index, + const int lmax = -1); + + /// Build rank-local, physically normalized SIAB primitives on the PW FFT grid. + std::vector siab_primitive_grid_values( + const int ik, + const ModulePW::PW_Basis_K* wfcpw, + const Structure_Factor& sf, + const UnitCell& ucell, + const SIABPrimitiveParameters& parameters); + + std::vector siab_primitive_reciprocal_values( + int ik, + const ModulePW::PW_Basis_K* wfcpw, + const Structure_Factor& sf, + const UnitCell& ucell, + const SIABPrimitiveParameters& parameters); + // void start_from_file_k(const int& ik, ModuleBase::ComplexMatrix& psi, const Structure_Factor& sf, const ModulePW::PW_Basis_K* wfcpw, const UnitCell& ucell); void output_overlap(const psi::Psi>& psi, const Structure_Factor& sf, @@ -42,11 +149,30 @@ class Numerical_Basis private: bool init_label = false; + SIABPrimitiveParameters initialized_siab_parameters = {0.0, 0.0, false, 0.0, 0.0, -1}; + int initialized_siab_ntype = 0; + int initialized_siab_lmax = 0; + int initialized_siab_nmax = 0; + std::vector initialized_siab_basis_shape; Bessel_Basis bessel_basis; std::vector mu_index; static std::vector init_mu_index(const UnitCell& ucell); + static std::vector siab_basis_shape_signature(const UnitCell& ucell); + + void initialize_siab_basis(const UnitCell& ucell, const SIABPrimitiveParameters& parameters); + + static void fill_reciprocal_primitive( + const int l, + const int abacus_m, + const int primitive_index, + const std::complex* structure_factor, + const ModuleBase::realArray& flq, + const ModuleBase::matrix& ylm, + const std::vector& gpow, + const double normalization, + std::vector>& values); void numerical_atomic_wfc(const int& ik, const ModulePW::PW_Basis_K* wfcpw, diff --git a/source/source_io/module_ctrl/ctrl_scf_lcao.cpp b/source/source_io/module_ctrl/ctrl_scf_lcao.cpp index 6c21487e0a4..b0547c9026c 100644 --- a/source/source_io/module_ctrl/ctrl_scf_lcao.cpp +++ b/source/source_io/module_ctrl/ctrl_scf_lcao.cpp @@ -38,6 +38,7 @@ #ifdef __EXX #include "source_lcao/module_ri/exx_lri_interface.h" // use EXX codes #include "source_lcao/module_rpa/rpa_lri.h" // use RPA code +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" #endif #include "../module_qo/to_qo.h" // use toQO #include "source_lcao/module_rdmft/rdmft.h" // use RDMFT codes @@ -67,6 +68,150 @@ void setup_exx_h_params(ModuleIO::WriteHParams& h_params, Exx_NAO +std::vector gather_sternheimer_lcao_occupied_kpoints( + const elecstate::ElecState& elec_state, + const K_Vectors& kv, + const Parallel_Orbitals& parallel_orbitals, + const psi::Psi& psi, + const Input_para& input, + const int nlocal) +{ + if (kv.get_nks() != kv.get_nkstot() + || elec_state.wg.nr != kv.get_nks() + || elec_state.ekb.nr != kv.get_nks() + || psi.get_nk() != kv.get_nks() + || kv.ik2iktot.size() != static_cast(kv.get_nks()) + || kv.kvec_d.size() != static_cast(kv.get_nks()) + || kv.wk.size() != static_cast(kv.get_nks()) + || kv.isk.size() != static_cast(kv.get_nks())) + { + throw std::runtime_error("Sternheimer solid LCAO k-point metadata are incomplete; require KPAR=1."); + } + + std::vector records; + records.reserve(static_cast(kv.get_nks())); + for (int local_k_index = 0; local_k_index != kv.get_nks(); ++local_k_index) + { + const double kweight = kv.wk[static_cast(local_k_index)]; + if (!std::isfinite(kweight) || kweight <= 0.0) + { + throw std::runtime_error("Sternheimer solid LCAO requires positive k-point weights."); + } + + int occupied_count = 0; + for (int ib = 0; ib != elec_state.wg.nc; ++ib) + { + if (elec_state.wg(local_k_index, ib) / kweight > 1.0e-8) + { + occupied_count = ib + 1; + } + } + if (occupied_count == 0) + { + continue; + } + + ModuleRI::SternheimerLCAOOccupiedKPoint record; + record.local_k_index = local_k_index; + record.global_k_index = kv.ik2iktot[static_cast(local_k_index)]; + record.zero_order_k_index = record.global_k_index; + record.spin_index = kv.isk[static_cast(local_k_index)]; + record.kpoint = {kv.kvec_d[static_cast(local_k_index)].x, + kv.kvec_d[static_cast(local_k_index)].y, + kv.kvec_d[static_cast(local_k_index)].z}; + record.kweight = kweight; + record.eigenvalues.reserve(static_cast(occupied_count)); + record.occupations.reserve(static_cast(occupied_count)); + record.coefficients.assign( + static_cast(occupied_count), + std::vector>(static_cast(nlocal), + std::complex(0.0, 0.0))); + for (int ib = 0; ib != occupied_count; ++ib) + { + record.eigenvalues.push_back(elec_state.ekb(local_k_index, ib)); + record.occupations.push_back(elec_state.wg(local_k_index, ib) / kweight); + const int local_band = parallel_orbitals.global2local_col(ib); + if (local_band < 0) + { + continue; + } + for (int local_basis = 0; local_basis != psi.get_nbasis(); ++local_basis) + { + const int global_basis = parallel_orbitals.local2global_row(local_basis); + if (global_basis < 0 || global_basis >= nlocal) + { + throw std::runtime_error("Sternheimer solid LCAO global basis index is out of range."); + } + record.coefficients[static_cast(ib)][static_cast(global_basis)] + = std::complex(psi(local_k_index, local_band, local_basis)); + } +#ifdef __MPI + MPI_Allreduce(MPI_IN_PLACE, + record.coefficients[static_cast(ib)].data(), + nlocal, + MPI_DOUBLE_COMPLEX, + MPI_SUM, + MPI_COMM_WORLD); +#endif + } + + const int unoccupied_count = ModuleRI::sternheimer_lcao_virtual_state_gather_count( + elec_state.ekb.nc - occupied_count, + input.sternheimer_delta, + input.sternheimer_virt_src, + input.sternheimer_virt_max); + record.unoccupied_eigenvalues.reserve(static_cast(unoccupied_count)); + record.unoccupied_coefficients.assign( + static_cast(unoccupied_count), + std::vector>(static_cast(nlocal), + std::complex(0.0, 0.0))); + for (int ib = occupied_count; ib != occupied_count + unoccupied_count; ++ib) + { + const std::size_t virtual_index = static_cast(ib - occupied_count); + record.unoccupied_eigenvalues.push_back(elec_state.ekb(local_k_index, ib)); + const int local_band = parallel_orbitals.global2local_col(ib); + if (local_band < 0) + { + continue; + } + for (int local_basis = 0; local_basis != psi.get_nbasis(); ++local_basis) + { + const int global_basis = parallel_orbitals.local2global_row(local_basis); + if (global_basis < 0 || global_basis >= nlocal) + { + throw std::runtime_error("Sternheimer solid LCAO global unoccupied basis index is out of range."); + } + record.unoccupied_coefficients[virtual_index][static_cast(global_basis)] + = std::complex(psi(local_k_index, local_band, local_basis)); + } +#ifdef __MPI + MPI_Allreduce(MPI_IN_PLACE, + record.unoccupied_coefficients[virtual_index].data(), + nlocal, + MPI_DOUBLE_COMPLEX, + MPI_SUM, + MPI_COMM_WORLD); +#endif + } + records.push_back(std::move(record)); + } + + ModuleRI::validate_sternheimer_lcao_occupied_kpoints(records, + kv.get_nks(), + kv.get_nkstot(), + input.nspin, + nlocal, + -1, + true); + return records; +} + +} // namespace #endif template @@ -168,7 +313,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, inp.dos_emin_ev, PARAM.globalv.two_fermi, inp.bndpar, - PARAM.globalv.global_out_dir, + global_out_dir, ofs_running); } @@ -280,7 +425,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, ModuleIO::write_hsr(hr_vec, sr, &ucell, inp.out_hsr[0], precision, pv, out_app_flag, gamma_only, ucell.get_iat2iwt(), ucell.nat, istep, - PARAM.globalv.global_out_dir, pelec->eferm, + global_out_dir, pelec->eferm, ofs_running); } @@ -290,7 +435,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, const bool output_hsr_npz = inp.out_hsr[0] == 3 || inp.out_hsr_npz_compat; if (output_hsr_npz) { - std::string zipname = PARAM.globalv.global_out_dir + "sr_nao.npz"; + std::string zipname = global_out_dir + "sr_nao.npz"; ModuleIO::output_mat_npz(ucell, zipname, *(p_hamilt->getSR())); } @@ -300,7 +445,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, for (int ispin = 0; ispin < hr_vec.size(); ++ispin) { std::string zipname - = PARAM.globalv.global_out_dir + "hrs" + std::to_string(ispin + 1) + "_nao.npz"; + = global_out_dir + "hrs" + std::to_string(ispin + 1) + "_nao.npz"; ModuleIO::output_mat_npz(ucell, zipname, *(hr_vec[ispin])); } } @@ -311,7 +456,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, for (int ispin = 0; ispin < dmr_vec.size(); ++ispin) { std::string zipname - = PARAM.globalv.global_out_dir + "output_DM" + std::to_string(ispin) + ".npz"; + = global_out_dir + "output_DM" + std::to_string(ispin) + ".npz"; ModuleIO::output_mat_npz(ucell, zipname, *(dmr_vec[ispin])); } } @@ -695,6 +840,39 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, if (inp.rpa_out_vel) rpa_lri_double.out_velocity(ucell, gd, two_center_bundle, pv, *psi, pelec); } + + if (inp.out_sternheimer_librpa || inp.out_sternheimer_siab) + { + if (pelec == nullptr || pelec->pot == nullptr || pw_rho == nullptr || psi == nullptr) + { + ModuleBase::WARNING_QUIT("ctrl_scf_lcao", + "Sternheimer LCAO output requires potential, grid, and KS states."); + } + ModuleRI::SternheimerAbacusRuntime sternheimer_runtime = { + inp, + pv.get_global_row_size(), + Parallel_Common::my_rank(MPI_COMM_WORLD), + Parallel_Common::nproc(MPI_COMM_WORLD), + PARAM.globalv.kpar_lcao, + PARAM.globalv.nthread_per_proc, + PARAM.globalv.deepks_setorb, + exx_info.info_ri.files_abfs, + exx_info.info_ri.kmesh_times, + ofs_running}; + const std::vector occupied_kpoints = + gather_sternheimer_lcao_occupied_kpoints(*pelec, kv, pv, *psi, inp, sternheimer_runtime.nlocal); + ModuleRI::run_sternheimer_abacus_lcao_chi0_output(*(pelec->pot), + *pw_rho, + ucell, + *pelec, + orb, + occupied_kpoints, + {kv.nmp[0], kv.nmp[1], kv.nmp[2]}, + pw_wfc, + &sf, + inp.rpa_outdir, + sternheimer_runtime); + } #endif //------------------------------------------------------------------ diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index 820deeebf50..9951678db1c 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -704,6 +704,28 @@ struct Input_para std::string out_v3d_method = "ewald"; ///< ewald or direct_reciprocal for reader-v1 full V double out_v3d_ecut = 0.0; ///< reciprocal cutoff in Ry for direct 3D Coulomb bool out_abf_overlap = false; ///< output raw active-ABF overlap for LibRPA v1 PSD diagnostics + bool out_sternheimer_librpa = false; ///< whether to output Sternheimer chi0 files for LibRPA + bool out_sternheimer_siab = false; ///< whether to output Sternheimer first-order-wavefunction targets for SIAB + int sternheimer_siab_lmax = -1; ///< maximum angular momentum of SIAB target primitives; -1 uses orbital lmax + double sternheimer_siab_vthr = 1.0e-10; ///< relative eigenvalue cutoff for global SIAB Coulomb whitening + int sternheimer_nfreq = 6; ///< number of minimax imaginary-frequency points for Sternheimer chi0 output + int sternheimer_q_index = 0; ///< 1-based nonzero q index on the full k mesh; 0 keeps the Gamma molecular path + std::string sternheimer_freq_file = ""; ///< optional fixed omega/weight grid for Sternheimer chi0 output + bool sternheimer_frequency_mpi = false; ///< whether to split Sternheimer chi0 frequencies over MPI ranks + bool sternheimer_channel_mpi = false; ///< whether to split Sternheimer equations over MPI ranks + std::string sternheimer_mpi_layout = "frequency_grouped"; ///< MPI ownership layout for response equations + bool sternheimer_delta = false; ///< whether to use Delta-Sternheimer projected solver for Sternheimer chi0 output + std::string sternheimer_molecular_coulomb = "none"; ///< none or explicit isolated_ri molecular boundary contract + std::string sternheimer_ao_file = ""; ///< producer-matched AO Coulomb matrices in Hartree + bool sternheimer_grid_diagnostics = false; ///< whether to write Delta-Sternheimer grid component diagnostics + int sternheimer_fd_order = 2; ///< Cartesian finite-difference Laplacian order: 2, 4, 6, or 8 + double sternheimer_response_ecutwfc = 0.0; ///< response-grid cutoff in Ry; 0 reuses the PBE grid + int sternheimer_response_nx = 0; ///< explicit Delta-Sternheimer response-grid x dimension; 0 disables + int sternheimer_response_ny = 0; ///< explicit Delta-Sternheimer response-grid y dimension; 0 disables + int sternheimer_response_nz = 0; ///< explicit Delta-Sternheimer response-grid z dimension; 0 disables + std::string sternheimer_virt_src = "ks_bands"; ///< fixed virtual source: complete unoccupied KS bands by default + int sternheimer_virt_max = 0; ///< maximum fixed KS/AO virtual states; 0 means all accepted candidates + double sternheimer_vnorm_tol = 1.0e-10; ///< norm threshold for fixed AO virtual-state orthogonalization bool exx_coul_moment = false; ///< whether to use moment method for Coulomb calculation bool exx_rotate_abfs = false; ///< whether to rotate auxiliary basis for Coulomb calculation double exx_multip_moments_threshold = 1e-10; ///< threshold to screen multipole moments in Coulomb calculation diff --git a/source/source_io/module_parameter/read_inp_out.cpp b/source/source_io/module_parameter/read_inp_out.cpp index ab958789f49..a5c4dc124ab 100644 --- a/source/source_io/module_parameter/read_inp_out.cpp +++ b/source/source_io/module_parameter/read_inp_out.cpp @@ -1631,6 +1631,465 @@ If EXX(exact exchange) is calculated (i.e. dft_fuctional==hse/hf/pbe0/scan0 or r this->add_item(item); } { + Input_Item item("out_sternheimer_librpa"); + item.annotation = "true: output Sternheimer chi0 files for LibRPA; false: default"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "Generate Sternheimer independent-particle response files for LibRPA. " + "This switch is independent of rpa=True and currently requires " + "out_librpa_ver=1."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao"); + read_sync_bool(input.out_sternheimer_librpa); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.out_sternheimer_librpa && para.input.out_librpa_ver != 1) + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " currently requires out_librpa_ver 1."); + } + }; + this->add_item(item); + } + { + Input_Item item("out_sternheimer_siab"); + item.annotation = "true: output Sternheimer first-order-wavefunction targets for SIAB; false: default"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "Write deterministic Sternheimer-SIAB v1 targets to " + "OUT.ABACUS/sternheimer_matrix.dat for basis_type=lcao. Delta-ST requires the loaded " + "LCAO orbitals, exactly one explicit bessel_nao_rcut, and a globally Coulomb-orthonormal " + "perturbation space. This output-only switch is independent of rpa and mutually exclusive " + "with out_sternheimer_librpa."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao"); + read_sync_bool(input.out_sternheimer_siab); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.out_sternheimer_siab && para.input.basis_type != "lcao") + { + ModuleBase::WARNING_QUIT( + "ReadInput", + item.label + " requires basis_type=lcao so Delta-ST can use the loaded LCAO orbitals."); + } + if (para.input.out_sternheimer_siab && para.input.out_sternheimer_librpa) + { + ModuleBase::WARNING_QUIT( + "ReadInput", + item.label + " cannot be combined with out_sternheimer_librpa because global Coulomb whitening " + "removes the raw atom-block auxiliary-channel meaning."); + } + if (para.input.out_sternheimer_siab && !para.input.sternheimer_delta) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " requires sternheimer_delta True."); + } + if (para.input.out_sternheimer_siab && para.input.bessel_nao_rcuts.size() != 1) + { + ModuleBase::WARNING_QUIT( + "ReadInput", + item.label + " requires exactly one explicit bessel_nao_rcut; the H campaign uses 8 bohr."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_siab_lmax"); + item.annotation = "Maximum angular momentum of Sternheimer-SIAB target primitives"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Set an output-only angular cutoff for the spherical-Bessel primitive blocks in " + "sternheimer_matrix.dat. The default -1 follows the loaded orbital lmax; use 2 to " + "include complete d blocks without adding d orbitals to the Delta-ST fixed subspace."; + item.default_value = "-1"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_siab==true"); + read_sync_int(input.sternheimer_siab_lmax); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_siab_lmax < -1) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be -1 or a non-negative integer."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_siab_vthr"); + item.annotation = "Relative eigenvalue threshold for global Coulomb whitening of Sternheimer-SIAB targets"; + item.category = "Output information"; + item.type = "Real"; + item.description = "Retain full-molecule auxiliary Coulomb eigenvectors with lambda greater than this value " + "times the largest eigenvalue. The retained perturbations satisfy W^T V W=I."; + item.default_value = "1e-10"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_siab==true"); + read_sync_double(input.sternheimer_siab_vthr); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (!std::isfinite(para.input.sternheimer_siab_vthr) + || para.input.sternheimer_siab_vthr <= 0.0 + || para.input.sternheimer_siab_vthr >= 1.0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be finite and strictly between zero and one."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_nfreq"); + item.annotation = "Number of minimax imaginary-frequency points for Sternheimer chi0 output"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Set the number of GreenX minimax imaginary-frequency points used by " + "out_sternheimer_librpa. For direct comparison with LibRPA SOS chi0, this value must " + "match nfreq in librpa.in."; + item.default_value = "6"; + item.unit = ""; + item.set_availability("basis_type==lcao"); + read_sync_int(input.sternheimer_nfreq); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_nfreq <= 0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be a positive integer."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_q_index"); + item.annotation = "One-based nonzero q-point index for periodic Sternheimer chi0 output"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Select one nonzero q point by its one-based global index on the full k mesh. " + "The default 0 preserves the single-k Gamma molecular path."; + item.default_value = "0"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true"); + read_sync_int(input.sternheimer_q_index); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_q_index < 0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be zero or a positive integer."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_freq_file"); + item.annotation = "Optional fixed imaginary-frequency grid file for Sternheimer chi0 output"; + item.category = "Output information"; + item.type = "String"; + item.description = "Read Sternheimer imaginary-frequency points and quadrature weights from a text file " + "instead of generating a GreenX minimax grid. Each non-comment row must contain either " + "\"omega_Ha weight_Ha\" or \"index omega_Ha weight_Ha\". The number of rows must match " + "sternheimer_nfreq. Use this option to compare Sternheimer chi0 with LibRPA SOS chi0 " + "on exactly the same frequency grid."; + item.default_value = ""; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true"); + read_sync_string(input.sternheimer_freq_file); + this->add_item(item); + } + { + Input_Item item("sternheimer_frequency_mpi"); + item.annotation = "true: split Sternheimer frequency points over MPI ranks; false: default"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "When Sternheimer output is enabled, distribute independent " + "imaginary-frequency Sternheimer solves over MPI ranks. The zero-order FD states are " + "solved once on rank 0 and broadcast; each rank then processes its assigned frequency " + "points."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao and (out_sternheimer_librpa==true or out_sternheimer_siab==true)"); + read_sync_bool(input.sternheimer_frequency_mpi); + this->add_item(item); + } + { + Input_Item item("sternheimer_channel_mpi"); + item.annotation = "true: split each Sternheimer frequency over MPI ranks; false: default"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "With sternheimer_frequency_mpi enabled, distribute occupied-state/auxiliary-channel " + "equations within each frequency group or across all ranks in global_equation mode. " + "LibRPA partial chi0 columns are reduced before reader-v1 output."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao and (out_sternheimer_librpa==true or out_sternheimer_siab==true)"); + read_sync_bool(input.sternheimer_channel_mpi); + this->add_item(item); + } + { + Input_Item item("sternheimer_mpi_layout"); + item.annotation = "MPI ownership layout for Sternheimer response equations"; + item.category = "Output information"; + item.type = "String"; + item.description = "Use frequency_grouped to assign a fixed MPI rank group to each imaginary frequency, " + "or global_equation to distribute occupied-state/frequency/auxiliary-channel equations " + "over all MPI ranks. The global_equation layout requires Sternheimer frequency MPI and " + "channel MPI, and supports SIAB or LibRPA output."; + item.default_value = "frequency_grouped"; + item.unit = ""; + item.set_availability("basis_type==lcao and (out_sternheimer_librpa==true or out_sternheimer_siab==true)"); + read_sync_string(input.sternheimer_mpi_layout); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_mpi_layout != "frequency_grouped" + && para.input.sternheimer_mpi_layout != "global_equation") + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " must be frequency_grouped or global_equation."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_delta"); + item.annotation = "true: use Delta-Sternheimer projected solver for Sternheimer chi0 output; false: default"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "When out_sternheimer_librpa is enabled, replace the standard Sternheimer linear solve " + "with a Delta-Sternheimer solve in the complement of a fixed AO/NAO virtual subspace. " + "The final LibRPA v1 chi0 file format is unchanged."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao"); + read_sync_bool(input.sternheimer_delta); + this->add_item(item); + } + { + Input_Item item("sternheimer_molecular_coulomb"); + item.annotation = "isolated molecular RI/grid Coulomb response; none preserves the legacy path"; + item.category = "Output information"; + item.type = "String"; + item.description = "isolated_ri declares an isolated molecule/atom: use producer-matched AO Coulomb " + "vertices on both sides and charge-moment exterior tails in the grid complement. " + "Requires sternheimer_ao_file, Delta-ST, Gamma, symmetry -1, and limits Coulomb. " + "This is not a periodic-solid or ordinary SOS option."; + item.default_value = "none"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_string(input.sternheimer_molecular_coulomb); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_molecular_coulomb != "none" + && para.input.sternheimer_molecular_coulomb != "isolated_ri") + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be none or isolated_ri."); + } + if (para.input.sternheimer_molecular_coulomb == "isolated_ri") + { + if (!para.input.out_sternheimer_librpa) + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " isolated_ri requires out_sternheimer_librpa True."); + } + if (!para.input.sternheimer_delta) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " isolated_ri requires sternheimer_delta True."); + } + if (para.input.sternheimer_ao_file.empty()) + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " isolated_ri requires sternheimer_ao_file."); + } + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_ao_file"); + item.annotation = "producer-matched AO Coulomb tensor for isolated molecular Delta-ST"; + item.category = "Output information"; + item.type = "String"; + item.description = "Path to a versioned Hartree AO tensor exported from the same producer Cs and " + "complete free-space V, in the finalized auxiliary ordering. " + "Only used with sternheimer_molecular_coulomb isolated_ri."; + item.default_value = ""; + item.unit = "Hartree"; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_string(input.sternheimer_ao_file); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (!para.input.sternheimer_ao_file.empty() + && para.input.sternheimer_molecular_coulomb != "isolated_ri") + { + ModuleBase::WARNING_QUIT("ReadInput", + item.label + " requires sternheimer_molecular_coulomb isolated_ri."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_grid_diagnostics"); + item.annotation = "write Delta-Sternheimer grid component diagnostics"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "Write frequency-independent grid operator matrices and frequency-dependent response " + "components for diagnosing Sternheimer real-space-grid convergence."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_bool(input.sternheimer_grid_diagnostics); + this->add_item(item); + } + { + Input_Item item("sternheimer_fd_order"); + item.annotation = "finite-difference Laplacian order for Sternheimer grid equations"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Use the second-, fourth-, sixth-, or eighth-order centered Cartesian finite-difference Laplacian in the " + "Sternheimer real-space Hamiltonian. The default remains second order."; + item.default_value = "2"; + item.unit = ""; + item.set_availability("basis_type==lcao and (out_sternheimer_librpa==true or out_sternheimer_siab==true)"); + read_sync_int(input.sternheimer_fd_order); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_fd_order != 2 && para.input.sternheimer_fd_order != 4 + && para.input.sternheimer_fd_order != 6 && para.input.sternheimer_fd_order != 8) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be 2, 4, 6, or 8."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_response_ecutwfc"); + item.annotation = "independent Delta-Sternheimer response-grid cutoff"; + item.category = "Output information"; + item.type = "Real"; + item.description = "Use an independent real-space response grid generated from this wave-function cutoff. " + "The value 0 reuses the converged PBE grid. A positive value must not exceed ecutwfc."; + item.default_value = "0.0"; + item.unit = "Ry"; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_double(input.sternheimer_response_ecutwfc); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_response_ecutwfc < 0.0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be zero or positive."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_response_nx"); + item.annotation = "explicit Delta-Sternheimer response-grid x dimension"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Set the x dimension of an explicit Delta-Sternheimer response grid without changing " + "the PBE grid. Use all three response dimensions together; zero disables this route."; + item.default_value = "0"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_int(input.sternheimer_response_nx); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_response_nx < 0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be zero or positive."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_response_ny"); + item.annotation = "explicit Delta-Sternheimer response-grid y dimension"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Set the y dimension of an explicit Delta-Sternheimer response grid without changing " + "the PBE grid. Use all three response dimensions together; zero disables this route."; + item.default_value = "0"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_int(input.sternheimer_response_ny); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_response_ny < 0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be zero or positive."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_response_nz"); + item.annotation = "explicit Delta-Sternheimer response-grid z dimension"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Set the z dimension of an explicit Delta-Sternheimer response grid without changing " + "the PBE grid. Use all three response dimensions together; zero disables this route."; + item.default_value = "0"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_int(input.sternheimer_response_nz); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_response_nz < 0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be zero or positive."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_virt_src"); + item.annotation = "ks_bands: complete LCAO KS virtual space; projected_ao: diagnostic AO projection"; + item.category = "Output information"; + item.type = "String"; + item.description = "Select the fixed LCAO virtual candidates used by Delta-Sternheimer. ks_bands uses every " + "unoccupied LCAO KS eigenvector and requires nbands=nlocal. projected_ao is retained for " + "diagnostic comparison with the earlier projected-AO construction."; + item.default_value = "ks_bands"; + item.unit = ""; + item.set_availability("basis_type==lcao and sternheimer_delta==true"); + read_sync_string(input.sternheimer_virt_src); + item.reset_value = [](const Input_Item& item, Parameter& para) { + std::transform(para.input.sternheimer_virt_src.begin(), + para.input.sternheimer_virt_src.end(), + para.input.sternheimer_virt_src.begin(), + [](const unsigned char character) { return static_cast(std::tolower(character)); }); + }; + item.check_value = [](const Input_Item& item, const Parameter& para) { + const std::vector sources = {"ks_bands", "projected_ao"}; + if (std::find(sources.begin(), sources.end(), para.input.sternheimer_virt_src) + == sources.end()) + { + ModuleBase::WARNING_QUIT("ReadInput", nofound_str(sources, item.label)); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_virt_max"); + item.annotation = "maximum number of fixed KS/AO virtual states for Delta-Sternheimer"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Limit the number of fixed KS-band or projected-AO virtual states used by sternheimer_delta. " + "The value 0 keeps all accepted candidates after occupied-state projection and " + "orthogonalization."; + item.default_value = "0"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_int(input.sternheimer_virt_max); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_virt_max < 0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be non-negative."); + } + }; + this->add_item(item); + } + { + Input_Item item("sternheimer_vnorm_tol"); + item.annotation = "norm threshold for Delta-Sternheimer AO virtual-state orthogonalization"; + item.category = "Output information"; + item.type = "Real"; + item.description = "Discard fixed AO/NAO candidate functions whose norm after projection out of occupied " + "states and previously accepted candidates is below this threshold."; + item.default_value = "1e-10"; + item.unit = ""; + item.set_availability("basis_type==lcao and out_sternheimer_librpa==true and sternheimer_delta==true"); + read_sync_double(input.sternheimer_vnorm_tol); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.sternheimer_vnorm_tol < 0.0) + { + ModuleBase::WARNING_QUIT("ReadInput", item.label + " must be non-negative."); + } + }; + this->add_item(item); } { Input_Item item("out_abf_overlap"); diff --git a/source/source_io/test_serial/read_input_item_test.cpp b/source/source_io/test_serial/read_input_item_test.cpp index 290f4104e1a..21632db7c55 100644 --- a/source/source_io/test_serial/read_input_item_test.cpp +++ b/source/source_io/test_serial/read_input_item_test.cpp @@ -63,6 +63,97 @@ class InputTest : public testing::Test } }; +TEST_F(InputTest, MolecularCoulombModeIsExplicitAndDisabledByDefault) +{ + ModuleIO::ReadInput readinput(0); + const auto mode = find_label("sternheimer_molecular_coulomb", readinput.input_lists); + ASSERT_NE(mode, readinput.input_lists.end()); + EXPECT_EQ(mode->second.default_value, "none"); + const auto file = find_label("sternheimer_ao_file", readinput.input_lists); + ASSERT_NE(file, readinput.input_lists.end()); + ASSERT_TRUE(mode->second.check_value); + ASSERT_TRUE(file->second.check_value); + + Parameter param; + EXPECT_EQ(TestParameters::input(param).sternheimer_molecular_coulomb, "none"); + EXPECT_TRUE(TestParameters::input(param).sternheimer_ao_file.empty()); + EXPECT_NO_THROW(mode->second.check_value(mode->second, param)); + EXPECT_NO_THROW(file->second.check_value(file->second, param)); + + TestParameters::input(param).basis_type = "lcao"; + TestParameters::input(param).symmetry = "1"; + TestParameters::input(param).sternheimer_q_index = 1; + TestParameters::input(param).out_sternheimer_librpa = false; + TestParameters::input(param).sternheimer_delta = false; + EXPECT_NO_THROW(mode->second.check_value(mode->second, param)); + EXPECT_NO_THROW(file->second.check_value(file->second, param)); + EXPECT_EQ(TestParameters::input(param).sternheimer_molecular_coulomb, "none"); + EXPECT_EQ(TestParameters::input(param).symmetry, "1"); + EXPECT_EQ(TestParameters::input(param).sternheimer_q_index, 1); + EXPECT_FALSE(TestParameters::input(param).out_sternheimer_librpa); + EXPECT_FALSE(TestParameters::input(param).sternheimer_delta); +} + +TEST_F(InputTest, MolecularCoulombReaderRequiresResponseInputs) +{ + ModuleIO::ReadInput readinput(0); + const auto mode = find_label("sternheimer_molecular_coulomb", readinput.input_lists); + ASSERT_NE(mode, readinput.input_lists.end()); + ASSERT_TRUE(mode->second.check_value); + const auto file = find_label("sternheimer_ao_file", readinput.input_lists); + ASSERT_NE(file, readinput.input_lists.end()); + + Parameter param; + TestParameters::input(param).basis_type = "lcao"; + TestParameters::input(param).sternheimer_molecular_coulomb = "isolated_ri"; + TestParameters::input(param).sternheimer_ao_file = "ao_potentials.dat"; + TestParameters::input(param).out_sternheimer_librpa = true; + TestParameters::input(param).sternheimer_delta = true; + EXPECT_NO_THROW(mode->second.check_value(mode->second, param)); + + TestParameters::input(param).out_sternheimer_librpa = false; + testing::internal::CaptureStdout(); + EXPECT_EXIT(mode->second.check_value(mode->second, param), ::testing::ExitedWithCode(1), ""); + EXPECT_THAT(testing::internal::GetCapturedStdout(), testing::HasSubstr("requires out_sternheimer_librpa True")); + TestParameters::input(param).out_sternheimer_librpa = true; + + TestParameters::input(param).sternheimer_delta = false; + testing::internal::CaptureStdout(); + EXPECT_EXIT(mode->second.check_value(mode->second, param), ::testing::ExitedWithCode(1), ""); + EXPECT_THAT(testing::internal::GetCapturedStdout(), testing::HasSubstr("requires sternheimer_delta True")); + TestParameters::input(param).sternheimer_delta = true; + + TestParameters::input(param).sternheimer_ao_file.clear(); + testing::internal::CaptureStdout(); + EXPECT_EXIT(mode->second.check_value(mode->second, param), ::testing::ExitedWithCode(1), ""); + EXPECT_THAT(testing::internal::GetCapturedStdout(), testing::HasSubstr("requires sternheimer_ao_file")); + TestParameters::input(param).sternheimer_ao_file = "ao_potentials.dat"; + + ASSERT_TRUE(file->second.check_value); + EXPECT_NO_THROW(file->second.check_value(file->second, param)); + TestParameters::input(param).sternheimer_molecular_coulomb = "periodic"; + testing::internal::CaptureStdout(); + EXPECT_EXIT(mode->second.check_value(mode->second, param), ::testing::ExitedWithCode(1), ""); + EXPECT_THAT(testing::internal::GetCapturedStdout(), testing::HasSubstr("must be none or isolated_ri")); +} + +TEST_F(InputTest, MolecularCoulombReaderRejectsOrphanAOPotentialFile) +{ + ModuleIO::ReadInput readinput(0); + const auto file = find_label("sternheimer_ao_file", readinput.input_lists); + ASSERT_NE(file, readinput.input_lists.end()); + ASSERT_TRUE(file->second.check_value); + + Parameter param; + TestParameters::input(param).sternheimer_ao_file = "ao_potentials.dat"; + EXPECT_EQ(TestParameters::input(param).sternheimer_molecular_coulomb, "none"); + EXPECT_FALSE(TestParameters::input(param).out_sternheimer_librpa); + testing::internal::CaptureStdout(); + EXPECT_EXIT(file->second.check_value(file->second, param), ::testing::ExitedWithCode(1), ""); + EXPECT_THAT(testing::internal::GetCapturedStdout(), + testing::HasSubstr("requires sternheimer_molecular_coulomb isolated_ri")); +} + TEST_F(InputTest, InvalidExplicitDomainDoesNotReachReadOrResetHooks) { ModuleIO::ReadInput reader(0); diff --git a/source/source_lcao/module_rpa/CMakeLists.txt b/source/source_lcao/module_rpa/CMakeLists.txt index 7f6b5be1eb1..6cf35dae59b 100644 --- a/source/source_lcao/module_rpa/CMakeLists.txt +++ b/source/source_lcao/module_rpa/CMakeLists.txt @@ -33,6 +33,8 @@ target_include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/module_librpa_io) target_link_libraries(rpa PRIVATE ri librpa_io) +add_subdirectory(module_delta_st) + if(BUILD_TESTING AND ENABLE_MPI) add_subdirectory(unittests) endif() diff --git a/source/source_lcao/module_rpa/module_delta_st/CMakeLists.txt b/source/source_lcao/module_rpa/module_delta_st/CMakeLists.txt new file mode 100644 index 00000000000..020400719ee --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/CMakeLists.txt @@ -0,0 +1,70 @@ +set(DELTA_ST_OBJECTS + sternheimer_abacus_dft_reference.cpp + sternheimer_abacus_dft_zero_order.cpp + sternheimer_abacus_fd_adapter.cpp + sternheimer_abacus_fd_bindings.cpp + sternheimer_abacus_fd_nonlocal.cpp + sternheimer_abacus_fd_smoke.cpp + sternheimer_abacus_st_smoke.cpp + sternheimer_abfs_perturbation.cpp + sternheimer_abfs_strict2d.cpp + sternheimer_channel_resources.cpp + sternheimer_chi0_mpi.cpp + sternheimer_coulomb_whitening.cpp + sternheimer_delta.cpp + sternheimer_dft_zero_order.cpp + sternheimer_fd_hamiltonian.cpp + sternheimer_fd_nonlocal_projector.cpp + sternheimer_fd_preconditioner.cpp + sternheimer_fd_projector_sampler.cpp + sternheimer_fd_solver.cpp + sternheimer_galerkin_audit.cpp + sternheimer_galerkin_operator.cpp + sternheimer_grid_diagnostics.cpp + sternheimer_grid_transfer.cpp + sternheimer_kq.cpp + sternheimer_periodic_solver.cpp + sternheimer_response_grid.cpp + sternheimer_rpa.cpp + sternheimer_runtime_options.cpp + sternheimer_siab_memory.cpp + sternheimer_siab_mpi.cpp + sternheimer_siab_overlap.cpp + sternheimer_siab_provenance.cpp + sternheimer_siab_writer.cpp + sternheimer_supercell_perturbation.cpp + sternheimer_supercell_sector.cpp + sternheimer_wavefunction_diagnostic.cpp + sternheimer_weak_augmented.cpp + sternheimer_weak_grid.cpp + sternheimer_weak_preconditioner.cpp + sternheimer_weak_q_reference.cpp +) + +if(ENABLE_GREENX_MINIMAX) + list(APPEND DELTA_ST_OBJECTS greenx_minimax_adapter.f90) +endif() + +add_library(delta_st OBJECT ${DELTA_ST_OBJECTS}) +target_include_directories(delta_st + PRIVATE + ${ABACUS_SOURCE_DIR} + ${ABACUS_SOURCE_DIR}/source_lcao/module_ri + ${ABACUS_SOURCE_DIR}/source_lcao/module_rpa + ${ABACUS_SOURCE_DIR}/source_lcao/module_rpa/module_delta_st + ${ABACUS_SOURCE_DIR}/source_lcao/module_rpa/module_librpa_io +) +target_link_libraries(delta_st PRIVATE ri rpa) + +if(ENABLE_GREENX_MINIMAX) + target_compile_definitions(delta_st PRIVATE __GREENX_MINIMAX) + target_link_libraries(delta_st PRIVATE GreenX::MiniMax) +endif() + +if(ENABLE_COVERAGE) + add_coverage(delta_st) +endif() + +if(BUILD_TESTING AND ENABLE_MPI) + add_subdirectory(test) +endif() diff --git a/source/source_lcao/module_rpa/module_delta_st/greenx_minimax_adapter.f90 b/source/source_lcao/module_rpa/module_delta_st/greenx_minimax_adapter.f90 new file mode 100644 index 00000000000..0c22745c38a --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/greenx_minimax_adapter.f90 @@ -0,0 +1,41 @@ +module gx_minimax_wrp + use iso_c_binding, only: c_int, c_double, c_ptr, c_f_pointer + use gx_minimax, only: gx_minimax_grid_frequency + use kinds, only: dp + implicit none + + private + public :: gx_minimax_grid_frequency_wrp + +contains + + subroutine gx_minimax_grid_frequency_wrp(num_points, e_min, e_max, omega_points, omega_weights, ierr) & + bind(C, name="gx_minimax_grid_frequency_wrp") + integer(kind=c_int), intent(in) :: num_points + real(kind=c_double), intent(in) :: e_min, e_max + type(c_ptr), value :: omega_points + type(c_ptr), value :: omega_weights + integer(kind=c_int), intent(out) :: ierr + + real(dp), allocatable :: omega_points_f(:) + real(dp), allocatable :: omega_weights_f(:) + real(kind=c_double), pointer :: omega_points_fptr(:) + real(kind=c_double), pointer :: omega_weights_fptr(:) + integer :: ig + + call c_f_pointer(omega_points, omega_points_fptr, [num_points]) + call c_f_pointer(omega_weights, omega_weights_fptr, [num_points]) + + call gx_minimax_grid_frequency(num_points, e_min, e_max, omega_points_f, omega_weights_f, ierr) + if (ierr == 0) then + do ig = 1, num_points + omega_points_fptr(ig) = omega_points_f(ig) + omega_weights_fptr(ig) = omega_weights_f(ig) + end do + end if + + if (allocated(omega_points_f)) deallocate(omega_points_f) + if (allocated(omega_weights_f)) deallocate(omega_weights_f) + end subroutine gx_minimax_grid_frequency_wrp + +end module gx_minimax_wrp diff --git a/source/source_lcao/module_rpa/module_delta_st/greenx_minimax_adapter.h b/source/source_lcao/module_rpa/module_delta_st/greenx_minimax_adapter.h new file mode 100644 index 00000000000..d09a9d496d4 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/greenx_minimax_adapter.h @@ -0,0 +1,16 @@ +#ifndef ABACUS_GREENX_MINIMAX_ADAPTER_H +#define ABACUS_GREENX_MINIMAX_ADAPTER_H + +#ifdef __GREENX_MINIMAX +extern "C" +{ +void gx_minimax_grid_frequency_wrp(int* num_points, + double* e_min, + double* e_max, + double* omega_points, + double* omega_weights, + int* ierr); +} +#endif + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.cpp new file mode 100644 index 00000000000..3b9bded535d --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.cpp @@ -0,0 +1,55 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.h" + +#include "source_base/matrix.h" +#include "source_estate/elecstate.h" + +#include +#include + +namespace ModuleRI +{ +namespace +{ + +std::vector copy_matrix_row(const ModuleBase::matrix& values, + const int row_index, + const int num_columns, + const char* quantity_name) +{ + if (row_index < 0 || row_index >= values.nr) + { + throw std::invalid_argument(std::string("Sternheimer DFT reference ") + quantity_name + + " k index is out of range."); + } + + if (num_columns <= 0 || num_columns > values.nc) + { + throw std::invalid_argument(std::string("Sternheimer DFT reference ") + quantity_name + + " band count is out of range."); + } + + std::vector row(num_columns); + for (int ib = 0; ib != num_columns; ++ib) + { + row[ib] = values(row_index, ib); + } + return row; +} + +} // namespace + +std::vector copy_sternheimer_dft_eigenvalues(const elecstate::ElecState& elec_state, + const int k_index, + const int num_bands) +{ + return copy_matrix_row(elec_state.ekb, k_index, num_bands, "eigenvalue"); +} + +std::vector copy_sternheimer_dft_occupations(const elecstate::ElecState& elec_state, + const int k_index, + const int num_bands) +{ + return copy_matrix_row(elec_state.wg, k_index, num_bands, "occupation"); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.h new file mode 100644 index 00000000000..722ecdc8928 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.h @@ -0,0 +1,24 @@ +#ifndef STERNHEIMER_ABACUS_DFT_REFERENCE_H +#define STERNHEIMER_ABACUS_DFT_REFERENCE_H + +#include + +namespace elecstate +{ +class ElecState; +} + +namespace ModuleRI +{ + +std::vector copy_sternheimer_dft_eigenvalues(const elecstate::ElecState& elec_state, + int k_index, + int num_bands); + +std::vector copy_sternheimer_dft_occupations(const elecstate::ElecState& elec_state, + int k_index, + int num_bands); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.cpp new file mode 100644 index 00000000000..9c4cb983d97 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.cpp @@ -0,0 +1,97 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.h" + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_reference.h" + +namespace ModuleRI +{ +namespace +{ + +SternheimerFDZeroOrderStates solve_sternheimer_fd_zero_order_auto(const SternheimerFDHamiltonian& hamiltonian, + const int num_bands, + const double volume_element, + const int max_dense_size, + const int lanczos_max_subspace_size) +{ + if (hamiltonian.grid().size() <= max_dense_size) + { + return solve_sternheimer_fd_zero_order_dense(hamiltonian, num_bands, volume_element, max_dense_size); + } + + SternheimerFDLanczosOptions options; + options.max_subspace_size = lanczos_max_subspace_size; + options.residual_tolerance = 1.0e-8; + options.initial_seed = 1; + return solve_sternheimer_fd_zero_order_lanczos(hamiltonian, num_bands, volume_element, options); +} + +} // namespace + +SternheimerABACUSDFTZeroOrderResult compare_sternheimer_abacus_fd_zero_order_to_dft( + const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const elecstate::ElecState& elec_state, + const int spin, + const int k_index, + const int num_bands, + const double eigenvalue_tolerance, + const int max_dense_size, + const double kinetic_prefactor, + const int lanczos_max_subspace_size) +{ + SternheimerABACUSDFTZeroOrderResult result; + result.hamiltonian_mode = "local_only"; + result.lanczos_max_subspace_size = lanczos_max_subspace_size; + result.grid_data = make_sternheimer_fd_grid(pw_basis); + const std::vector local_potential = copy_sternheimer_local_potential(potential, pw_basis, spin); + const SternheimerFDHamiltonian hamiltonian(result.grid_data.grid, local_potential, kinetic_prefactor); + + result.fd_states + = solve_sternheimer_fd_zero_order_auto(hamiltonian, + num_bands, + result.grid_data.volume_element, + max_dense_size, + lanczos_max_subspace_size); + result.dft_eigenvalues = copy_sternheimer_dft_eigenvalues(elec_state, k_index, num_bands); + result.dft_occupations = copy_sternheimer_dft_occupations(elec_state, k_index, num_bands); + result.comparison + = compare_sternheimer_fd_zero_order_to_dft(result.fd_states, result.dft_eigenvalues, eigenvalue_tolerance); + + return result; +} + +SternheimerABACUSDFTZeroOrderResult compare_sternheimer_abacus_fd_zero_order_to_dft( + const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const int spin, + const int k_index, + const int num_bands, + const double eigenvalue_tolerance, + const int max_dense_size, + const double kinetic_prefactor, + const int lanczos_max_subspace_size) +{ + SternheimerABACUSDFTZeroOrderResult result; + result.hamiltonian_mode = "full_nc_nonlocal"; + result.lanczos_max_subspace_size = lanczos_max_subspace_size; + result.grid_data = make_sternheimer_fd_grid(pw_basis); + const SternheimerFDHamiltonian hamiltonian + = make_sternheimer_fd_hamiltonian(potential, pw_basis, ucell, spin, kinetic_prefactor); + + result.fd_states + = solve_sternheimer_fd_zero_order_auto(hamiltonian, + num_bands, + result.grid_data.volume_element, + max_dense_size, + lanczos_max_subspace_size); + result.dft_eigenvalues = copy_sternheimer_dft_eigenvalues(elec_state, k_index, num_bands); + result.dft_occupations = copy_sternheimer_dft_occupations(elec_state, k_index, num_bands); + result.comparison + = compare_sternheimer_fd_zero_order_to_dft(result.fd_states, result.dft_eigenvalues, eigenvalue_tolerance); + + return result; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.h new file mode 100644 index 00000000000..814fd0cd5d9 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.h @@ -0,0 +1,64 @@ +#ifndef STERNHEIMER_ABACUS_DFT_ZERO_ORDER_H +#define STERNHEIMER_ABACUS_DFT_ZERO_ORDER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.h" + +#include +#include + +class UnitCell; + +namespace elecstate +{ +class ElecState; +class Potential; +} + +namespace ModulePW +{ +class PW_Basis; +} + +namespace ModuleRI +{ + +struct SternheimerABACUSDFTZeroOrderResult +{ + SternheimerABACUSFDGridData grid_data; + SternheimerFDZeroOrderStates fd_states; + std::vector dft_eigenvalues; + std::vector dft_occupations; + SternheimerDFTZeroOrderComparison comparison; + std::string hamiltonian_mode; + int lanczos_max_subspace_size = 0; +}; + +SternheimerABACUSDFTZeroOrderResult compare_sternheimer_abacus_fd_zero_order_to_dft( + const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const elecstate::ElecState& elec_state, + int spin, + int k_index, + int num_bands, + double eigenvalue_tolerance, + int max_dense_size = 4096, + double kinetic_prefactor = 1.0, + int lanczos_max_subspace_size = 320); + +SternheimerABACUSDFTZeroOrderResult compare_sternheimer_abacus_fd_zero_order_to_dft( + const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + int spin, + int k_index, + int num_bands, + double eigenvalue_tolerance, + int max_dense_size = 4096, + double kinetic_prefactor = 1.0, + int lanczos_max_subspace_size = 320); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.cpp new file mode 100644 index 00000000000..f795ac42ae1 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.cpp @@ -0,0 +1,125 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h" + +#include "source_base/matrix3.h" + +#include +#include +#include +#include + +namespace +{ + +double dot_product(const double ax, const double ay, const double az, const double bx, const double by, const double bz) +{ + return ax * bx + ay * by + az * bz; +} + +double vector_norm(const double x, const double y, const double z) +{ + return std::sqrt(dot_product(x, y, z, x, y, z)); +} + +} // namespace + +namespace ModuleRI +{ + +SternheimerABACUSFDGridData make_sternheimer_fd_grid_from_lattice(const int nx, + const int ny, + const int nz, + const int nrxx, + const double lat0, + const ModuleBase::Matrix3& latvec, + const double orthogonality_tolerance) +{ + (void)orthogonality_tolerance; + if (nx <= 0 || ny <= 0 || nz <= 0) + { + throw std::invalid_argument("Sternheimer ABACUS FD grid requires positive grid dimensions."); + } + if (lat0 <= 0.0) + { + throw std::invalid_argument("Sternheimer ABACUS FD grid requires a positive lattice constant."); + } + + const int nxyz = nx * ny * nz; + if (nrxx != nxyz) + { + throw std::invalid_argument( + "Sternheimer ABACUS FD dense prototype requires an undistributed full real-space grid."); + } + + const double a1_norm = vector_norm(latvec.e11, latvec.e12, latvec.e13); + const double a2_norm = vector_norm(latvec.e21, latvec.e22, latvec.e23); + const double a3_norm = vector_norm(latvec.e31, latvec.e32, latvec.e33); + if (a1_norm <= 0.0 || a2_norm <= 0.0 || a3_norm <= 0.0) + { + throw std::invalid_argument("Sternheimer ABACUS FD grid requires nonzero lattice vectors."); + } + + const double determinant + = latvec.e11 * (latvec.e22 * latvec.e33 - latvec.e23 * latvec.e32) + - latvec.e12 * (latvec.e21 * latvec.e33 - latvec.e23 * latvec.e31) + + latvec.e13 * (latvec.e21 * latvec.e32 - latvec.e22 * latvec.e31); + if (std::abs(determinant) <= 1.0e-14 * a1_norm * a2_norm * a3_norm) + { + throw std::invalid_argument("Sternheimer ABACUS FD grid requires a nonsingular lattice."); + } + + SternheimerABACUSFDGridData grid_data; + grid_data.grid.nx = nx; + grid_data.grid.ny = ny; + grid_data.grid.nz = nz; + grid_data.grid.hx = lat0 * a1_norm / nx; + grid_data.grid.hy = lat0 * a2_norm / ny; + grid_data.grid.hz = lat0 * a3_norm / nz; + grid_data.grid.periodic = true; + grid_data.grid.lattice_vectors = {{{lat0 * latvec.e11, lat0 * latvec.e12, lat0 * latvec.e13}, + {lat0 * latvec.e21, lat0 * latvec.e22, lat0 * latvec.e23}, + {lat0 * latvec.e31, lat0 * latvec.e32, lat0 * latvec.e33}}}; + grid_data.volume_element = std::abs(determinant) * lat0 * lat0 * lat0 / static_cast(nxyz); + return grid_data; +} + +std::vector embed_sternheimer_local_z_slab(const std::vector& local_values, + const int nxy, + const int nz, + const int nplane, + const int startz) +{ + if (nxy <= 0 || nz <= 0 || nplane < 0 || startz < 0 || startz + nplane > nz + || local_values.size() != static_cast(nxy) * static_cast(nplane)) + { + throw std::invalid_argument("Invalid Sternheimer local z-slab dimensions."); + } + + std::vector full_values(static_cast(nxy) * static_cast(nz), 0.0); + for (int ixy = 0; ixy != nxy; ++ixy) + { + for (int iz = 0; iz != nplane; ++iz) + { + full_values[static_cast(ixy) * static_cast(nz) + + static_cast(startz + iz)] + = local_values[static_cast(ixy) * static_cast(nplane) + + static_cast(iz)]; + } + } + return full_values; +} + +SternheimerFDHamiltonian make_sternheimer_fd_hamiltonian_from_local_potential( + const SternheimerABACUSFDGridData& grid_data, + std::vector local_potential, + const double kinetic_prefactor, + std::shared_ptr nonlocal_projector, + const int finite_difference_order) +{ + return SternheimerFDHamiltonian(grid_data.grid, + std::move(local_potential), + kinetic_prefactor, + std::move(nonlocal_projector), + finite_difference_order); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h new file mode 100644 index 00000000000..3d4b90bdc47 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h @@ -0,0 +1,100 @@ +#ifndef STERNHEIMER_ABACUS_FD_ADAPTER_H +#define STERNHEIMER_ABACUS_FD_ADAPTER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include +#include + +class UnitCell; + +namespace ModuleBase +{ +class Matrix3; +} + +namespace ModulePW +{ +class PW_Basis; +} + +namespace elecstate +{ +class Potential; +} + +namespace ModuleRI +{ + +struct SternheimerABACUSFDGridData +{ + SternheimerFDHamiltonian::Grid grid; + double volume_element = 0.0; +}; + +SternheimerABACUSFDGridData make_sternheimer_fd_grid_from_lattice(int nx, + int ny, + int nz, + int nrxx, + double lat0, + const ModuleBase::Matrix3& latvec, + double orthogonality_tolerance = 1.0e-12); + +SternheimerABACUSFDGridData make_sternheimer_fd_grid(const ModulePW::PW_Basis& pw_basis, + double orthogonality_tolerance = 1.0e-12); + +SternheimerABACUSFDGridData make_sternheimer_fd_full_grid(const ModulePW::PW_Basis& pw_basis, + double orthogonality_tolerance = 1.0e-12); + +std::vector embed_sternheimer_local_z_slab(const std::vector& local_values, + int nxy, + int nz, + int nplane, + int startz); + +std::vector copy_sternheimer_local_potential(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + int spin); + +std::vector copy_sternheimer_full_local_potential(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + int spin); + +std::vector copy_sternheimer_fixed_local_potential(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis); + +std::vector copy_sternheimer_full_fixed_local_potential(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis); + +SternheimerFDHamiltonian make_sternheimer_fd_hamiltonian_from_local_potential( + const SternheimerABACUSFDGridData& grid_data, + std::vector local_potential, + double kinetic_prefactor = 1.0, + std::shared_ptr nonlocal_projector = nullptr, + int finite_difference_order = 2); + +SternheimerFDHamiltonian make_sternheimer_fd_hamiltonian(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + int spin, + double kinetic_prefactor = 1.0, + int finite_difference_order = 2); + +SternheimerFDHamiltonian make_sternheimer_fd_hamiltonian(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + int spin, + double kinetic_prefactor = 1.0, + const SternheimerReducedKPoint& kpoint = {0.0, 0.0, 0.0}, + int finite_difference_order = 2); + +SternheimerFDHamiltonian make_sternheimer_fd_full_hamiltonian(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + int spin, + double kinetic_prefactor = 1.0, + const SternheimerReducedKPoint& kpoint = {0.0, 0.0, 0.0}, + int finite_difference_order = 2); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_bindings.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_bindings.cpp new file mode 100644 index 00000000000..9373c4aa3c3 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_bindings.cpp @@ -0,0 +1,217 @@ +#include "source_basis/module_pw/pw_basis.h" +#include "source_estate/module_pot/potential_new.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.h" + +#include +#include + +#ifdef __MPI +#include +#endif + +namespace ModuleRI +{ + +SternheimerABACUSFDGridData make_sternheimer_fd_grid(const ModulePW::PW_Basis& pw_basis, + const double orthogonality_tolerance) +{ + return make_sternheimer_fd_grid_from_lattice(pw_basis.nx, + pw_basis.ny, + pw_basis.nz, + pw_basis.nrxx, + pw_basis.lat0, + pw_basis.latvec, + orthogonality_tolerance); +} + +SternheimerABACUSFDGridData make_sternheimer_fd_full_grid(const ModulePW::PW_Basis& pw_basis, + const double orthogonality_tolerance) +{ + return make_sternheimer_fd_grid_from_lattice(pw_basis.nx, + pw_basis.ny, + pw_basis.nz, + pw_basis.nxyz, + pw_basis.lat0, + pw_basis.latvec, + orthogonality_tolerance); +} + +std::vector copy_sternheimer_local_potential(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const int spin) +{ + const SternheimerABACUSFDGridData grid_data = make_sternheimer_fd_grid(pw_basis); + const int grid_size = grid_data.grid.size(); + const ModuleBase::matrix& veff = potential.get_eff_v(); + + if (spin < 0 || spin >= veff.nr) + { + throw std::invalid_argument("Sternheimer ABACUS FD potential spin index is out of range."); + } + if (veff.nc != grid_size) + { + throw std::invalid_argument("Sternheimer ABACUS FD potential size does not match the real-space grid."); + } + + const double* veff_spin = potential.get_eff_v(spin); + if (veff_spin == nullptr) + { + throw std::invalid_argument("Sternheimer ABACUS FD potential is not allocated."); + } + return std::vector(veff_spin, veff_spin + grid_size); +} + +std::vector copy_sternheimer_full_local_potential(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const int spin) +{ + const ModuleBase::matrix& veff = potential.get_eff_v(); + std::vector local_potential; + if (pw_basis.nrxx > 0) + { + if (spin < 0 || spin >= veff.nr) + { + throw std::invalid_argument("Sternheimer ABACUS FD full-grid potential spin index is out of range."); + } + if (veff.nc != pw_basis.nrxx) + { + throw std::invalid_argument("Sternheimer ABACUS FD full-grid potential size does not match local nrxx."); + } + + const double* veff_spin = potential.get_eff_v(spin); + if (veff_spin == nullptr) + { + throw std::invalid_argument("Sternheimer ABACUS FD full-grid potential is not allocated."); + } + local_potential.assign(veff_spin, veff_spin + pw_basis.nrxx); + } + std::vector full_potential = embed_sternheimer_local_z_slab(local_potential, + pw_basis.nxy, + pw_basis.nz, + pw_basis.nplane, + pw_basis.startz_current); + +#ifdef __MPI + if (pw_basis.poolnproc > 1) + { + MPI_Allreduce(MPI_IN_PLACE, + full_potential.data(), + pw_basis.nxyz, + MPI_DOUBLE, + MPI_SUM, + pw_basis.pool_world); + } +#endif + + return full_potential; +} + +std::vector copy_sternheimer_fixed_local_potential(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis) +{ + const SternheimerABACUSFDGridData grid_data = make_sternheimer_fd_grid(pw_basis); + if (grid_data.grid.size() != pw_basis.nrxx || pw_basis.nrxx <= 0) + { + throw std::invalid_argument( + "Sternheimer ABACUS FD fixed potential size does not match the real-space grid."); + } + const double* fixed = potential.get_fixed_v(); + if (fixed == nullptr) + { + throw std::invalid_argument("Sternheimer ABACUS FD fixed potential is not allocated."); + } + return std::vector(fixed, fixed + pw_basis.nrxx); +} + +std::vector copy_sternheimer_full_fixed_local_potential( + const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis) +{ + std::vector local_fixed; + if (pw_basis.nrxx > 0) + { + const double* fixed = potential.get_fixed_v(); + if (fixed == nullptr) + { + throw std::invalid_argument("Sternheimer ABACUS FD fixed potential is not allocated."); + } + local_fixed.assign(fixed, fixed + pw_basis.nrxx); + } + std::vector full_fixed = embed_sternheimer_local_z_slab(local_fixed, + pw_basis.nxy, + pw_basis.nz, + pw_basis.nplane, + pw_basis.startz_current); +#ifdef __MPI + if (pw_basis.poolnproc > 1) + { + MPI_Allreduce(MPI_IN_PLACE, + full_fixed.data(), + pw_basis.nxyz, + MPI_DOUBLE, + MPI_SUM, + pw_basis.pool_world); + } +#endif + return full_fixed; +} + +SternheimerFDHamiltonian make_sternheimer_fd_hamiltonian(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const int spin, + const double kinetic_prefactor, + const int finite_difference_order) +{ + const SternheimerABACUSFDGridData grid_data = make_sternheimer_fd_grid(pw_basis); + return make_sternheimer_fd_hamiltonian_from_local_potential(grid_data, + copy_sternheimer_local_potential(potential, + pw_basis, + spin), + kinetic_prefactor, + nullptr, + finite_difference_order); +} + +SternheimerFDHamiltonian make_sternheimer_fd_hamiltonian(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const int spin, + const double kinetic_prefactor, + const SternheimerReducedKPoint& kpoint, + const int finite_difference_order) +{ + SternheimerABACUSFDGridData grid_data = make_sternheimer_fd_grid(pw_basis); + grid_data.grid.kpoint = kpoint; + auto nonlocal_projector + = make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, grid_data.grid, grid_data.volume_element); + return make_sternheimer_fd_hamiltonian_from_local_potential(grid_data, + copy_sternheimer_local_potential(potential, + pw_basis, + spin), + kinetic_prefactor, + std::move(nonlocal_projector), + finite_difference_order); +} + +SternheimerFDHamiltonian make_sternheimer_fd_full_hamiltonian(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const int spin, + const double kinetic_prefactor, + const SternheimerReducedKPoint& kpoint, + const int finite_difference_order) +{ + SternheimerABACUSFDGridData grid_data = make_sternheimer_fd_full_grid(pw_basis); + grid_data.grid.kpoint = kpoint; + auto nonlocal_projector + = make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, grid_data.grid, grid_data.volume_element); + return make_sternheimer_fd_hamiltonian_from_local_potential( + grid_data, + copy_sternheimer_full_local_potential(potential, pw_basis, spin), + kinetic_prefactor, + std::move(nonlocal_projector), + finite_difference_order); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.cpp new file mode 100644 index 00000000000..a24f7e34576 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.cpp @@ -0,0 +1,121 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.h" + +#include "source_cell/atom_spec.h" +#include "source_cell/unitcell.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.h" + +#include +#include + +namespace +{ + +void validate_grid(const ModuleRI::SternheimerFDHamiltonian::Grid& grid) +{ + if (grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector requires positive grid dimensions."); + } + if (grid.hx <= 0.0 || grid.hy <= 0.0 || grid.hz <= 0.0) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector requires positive grid spacings."); + } +} + +void validate_unitcell(const UnitCell& ucell) +{ + if (ucell.ntype < 0) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector received negative ntype."); + } + if (ucell.lat0 <= 0.0) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector requires a positive lat0."); + } + if (ucell.ntype > 0 && ucell.atoms == nullptr) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector received a null atom list."); + } +} + +void validate_atom_pseudo(const Atom& atom) +{ + if (atom.na < 0) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector received negative atom count."); + } + if (static_cast(atom.tau.size()) < atom.na) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector atom position count mismatch."); + } + if (atom.ncpp.nbeta == 0) + { + return; + } + if (atom.ncpp.tvanp) + { + throw std::invalid_argument( + "Sternheimer ABACUS FD nonlocal projector currently supports norm-conserving pseudopotentials only."); + } + if (atom.ncpp.has_so) + { + throw std::invalid_argument( + "Sternheimer ABACUS FD nonlocal projector currently does not support spin-orbit projectors."); + } + if (atom.ncpp.mesh <= 1 || static_cast(atom.ncpp.r.size()) != atom.ncpp.mesh) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector radial mesh size mismatch."); + } +} + +int grid_size(const ModuleRI::SternheimerFDHamiltonian::Grid& grid) +{ + return grid.nx * grid.ny * grid.nz; +} + +} // namespace + +namespace ModuleRI +{ + +std::shared_ptr +make_sternheimer_fd_nonlocal_projector_from_unitcell(const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const double volume_element) +{ + validate_grid(grid); + validate_unitcell(ucell); + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer ABACUS nonlocal projector requires a positive volume element."); + } + + std::vector blocks; + for (int it = 0; it != ucell.ntype; ++it) + { + const Atom& atom = ucell.atoms[it]; + validate_atom_pseudo(atom); + if (atom.ncpp.nbeta == 0) + { + continue; + } + + const SternheimerFDRadialProjectorSet radial_set + = make_sternheimer_fd_radial_projector_set_from_abacus_matrices(atom.ncpp.r, + atom.ncpp.betar, + atom.ncpp.lll, + atom.ncpp.dion); + for (int ia = 0; ia != atom.na; ++ia) + { + blocks.push_back(sample_sternheimer_fd_projector_block(radial_set, grid, atom.tau[ia] * ucell.lat0)); + } + } + + if (blocks.empty()) + { + return nullptr; + } + return std::make_shared(grid_size(grid), volume_element, blocks); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.h new file mode 100644 index 00000000000..929abd4ae3f --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.h @@ -0,0 +1,21 @@ +#ifndef STERNHEIMER_ABACUS_FD_NONLOCAL_H +#define STERNHEIMER_ABACUS_FD_NONLOCAL_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h" + +#include + +class UnitCell; + +namespace ModuleRI +{ + +std::shared_ptr +make_sternheimer_fd_nonlocal_projector_from_unitcell(const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + double volume_element); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.cpp new file mode 100644 index 00000000000..ff8832eff63 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.cpp @@ -0,0 +1,187 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.h" + +#include "source_basis/module_pw/pw_basis.h" +#include "source_estate/elecstate.h" +#include "source_estate/module_pot/potential_new.h" +#include "source_io/module_parameter/parameter.h" + +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +constexpr const char* kSmokeEnv = "ABACUS_STERNHEIMER_FD_ZERO_ORDER_SMOKE"; +constexpr const char* kOutputEnv = "ABACUS_STERNHEIMER_FD_ZERO_ORDER_OUT"; +constexpr const char* kBandsEnv = "ABACUS_STERNHEIMER_FD_ZERO_ORDER_BANDS"; +constexpr const char* kMaxDenseEnv = "ABACUS_STERNHEIMER_FD_ZERO_ORDER_MAX_DENSE"; +constexpr const char* kToleranceEnv = "ABACUS_STERNHEIMER_FD_ZERO_ORDER_TOLERANCE"; +constexpr const char* kLocalOnlyEnv = "ABACUS_STERNHEIMER_FD_ZERO_ORDER_LOCAL_ONLY"; +constexpr const char* kLanczosSubspaceEnv = "ABACUS_STERNHEIMER_FD_ZERO_ORDER_LANCZOS_SUBSPACE"; + +std::string lower_string(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +bool env_is_true(const char* name) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return false; + } + const std::string value = lower_string(raw); + return !(value.empty() || value == "0" || value == "false" || value == "off" || value == "no"); +} + +int positive_int_from_env(const char* name, const int default_value) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_value; + } + std::size_t parsed = 0; + const int value = std::stoi(raw, &parsed); + if (raw[parsed] != '\0' || value <= 0) + { + throw std::invalid_argument(std::string("Invalid positive integer in ") + name + "."); + } + return value; +} + +double nonnegative_double_from_env(const char* name, const double default_value) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_value; + } + std::size_t parsed = 0; + const double value = std::stod(raw, &parsed); + if (raw[parsed] != '\0' || value < 0.0) + { + throw std::invalid_argument(std::string("Invalid non-negative floating-point value in ") + name + "."); + } + return value; +} + +std::string default_report_path(const std::string& output_dir) +{ + const char* explicit_path = std::getenv(kOutputEnv); + if (explicit_path != nullptr && explicit_path[0] != '\0') + { + return explicit_path; + } + if (output_dir.empty()) + { + return "STERNHEIMER_FD_ZERO_ORDER.dat"; + } + if (output_dir.back() == '/') + { + return output_dir + "STERNHEIMER_FD_ZERO_ORDER.dat"; + } + return output_dir + "/STERNHEIMER_FD_ZERO_ORDER.dat"; +} + +void write_failure_report(std::ofstream& out, const std::string& reason) +{ + out << "# ABACUS Sternheimer FD zero-order smoke test\n"; + out << "status failed\n"; + out << "reason " << reason << '\n'; +} + +} // namespace + +bool sternheimer_fd_zero_order_smoke_enabled() +{ + return env_is_true(kSmokeEnv); +} + +void run_sternheimer_fd_zero_order_smoke(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const std::string& output_dir, + const int my_rank, + const int nproc, + std::ofstream& ofs_running) +{ + if (!sternheimer_fd_zero_order_smoke_enabled() || my_rank != 0) + { + return; + } + + const std::string report_path = default_report_path(output_dir); + std::ofstream out(report_path.c_str(), std::ios::out | std::ios::trunc); + if (!out) + { + ofs_running << " Sternheimer FD zero-order smoke: failed to open " << report_path << std::endl; + return; + } + + try + { + if (nproc != 1) + { + throw std::runtime_error( + "The current dense FD smoke test requires a single MPI rank so pw_basis.nrxx is the full grid."); + } + if (elec_state.ekb.nc <= 0) + { + throw std::runtime_error("ABACUS DFT eigenvalues are not available."); + } + + const int requested_bands = positive_int_from_env(kBandsEnv, 4); + const int num_bands = std::min(requested_bands, elec_state.ekb.nc); + const int max_dense_size = positive_int_from_env(kMaxDenseEnv, 4096); + const int lanczos_max_subspace_size = positive_int_from_env(kLanczosSubspaceEnv, 320); + const double eigenvalue_tolerance = nonnegative_double_from_env(kToleranceEnv, 1.0e-2); + + const bool local_only = env_is_true(kLocalOnlyEnv); + const SternheimerABACUSDFTZeroOrderResult result + = local_only + ? compare_sternheimer_abacus_fd_zero_order_to_dft(potential, + pw_basis, + elec_state, + 0, + 0, + num_bands, + eigenvalue_tolerance, + max_dense_size, + 1.0, + lanczos_max_subspace_size) + : compare_sternheimer_abacus_fd_zero_order_to_dft(potential, + pw_basis, + ucell, + elec_state, + 0, + 0, + num_bands, + eigenvalue_tolerance, + max_dense_size, + 1.0, + lanczos_max_subspace_size); + + out << format_sternheimer_fd_zero_order_report(result); + ofs_running << " Sternheimer FD zero-order smoke report: " << report_path << std::endl; + } + catch (const std::exception& error) + { + write_failure_report(out, error.what()); + ofs_running << " Sternheimer FD zero-order smoke failed: " << error.what() << std::endl; + ofs_running << " Sternheimer FD zero-order smoke report: " << report_path << std::endl; + } +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.h new file mode 100644 index 00000000000..5f5bd133578 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.h @@ -0,0 +1,78 @@ +#ifndef STERNHEIMER_ABACUS_FD_SMOKE_H +#define STERNHEIMER_ABACUS_FD_SMOKE_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_dft_zero_order.h" + +#include +#include +#include + +class UnitCell; + +namespace ModulePW +{ +class PW_Basis; +} + +namespace elecstate +{ +class ElecState; +class Potential; +} // namespace elecstate + +namespace ModuleRI +{ + +inline std::string format_sternheimer_fd_zero_order_report(const SternheimerABACUSDFTZeroOrderResult& result) +{ + std::ostringstream out; + const int grid_size = result.grid_data.grid.nx * result.grid_data.grid.ny * result.grid_data.grid.nz; + out << std::setprecision(16); + out << "# ABACUS Sternheimer FD zero-order smoke test\n"; + out << "grid " << result.grid_data.grid.nx << ' ' << result.grid_data.grid.ny << ' ' + << result.grid_data.grid.nz << " size " << grid_size << " dV " + << result.grid_data.volume_element << '\n'; + out << "spacing " << result.grid_data.grid.hx << ' ' << result.grid_data.grid.hy << ' ' + << result.grid_data.grid.hz << '\n'; + if (!result.hamiltonian_mode.empty()) + { + out << "hamiltonian_mode " << result.hamiltonian_mode << '\n'; + } + if (result.lanczos_max_subspace_size > 0) + { + out << "lanczos_max_subspace_size " << result.lanczos_max_subspace_size << '\n'; + } + out << "band fd_eigenvalue_Ry dft_eigenvalue_Ry fd_minus_dft_Ry occupation fd_residual_norm " + "eigenvalue_within_tolerance\n"; + + for (const SternheimerDFTZeroOrderBandComparison& band: result.comparison.bands) + { + const int band_index = band.band_index; + const double occupation = band_index >= 0 && band_index < static_cast(result.dft_occupations.size()) + ? result.dft_occupations[band_index] + : 0.0; + out << band.band_index << ' ' << band.fd_eigenvalue << ' ' << band.dft_eigenvalue << ' ' + << band.fd_minus_dft << ' ' << occupation << ' ' << band.fd_residual_norm << ' ' + << (band.eigenvalue_within_tolerance ? "yes" : "no") << '\n'; + } + + out << "max_abs_fd_minus_dft_Ry " << result.comparison.max_abs_fd_minus_dft << '\n'; + out << "all_eigenvalues_within_tolerance " + << (result.comparison.all_eigenvalues_within_tolerance ? "yes" : "no") << '\n'; + return out.str(); +} + +bool sternheimer_fd_zero_order_smoke_enabled(); + +void run_sternheimer_fd_zero_order_smoke(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const std::string& output_dir, + const int my_rank, + const int nproc, + std::ofstream& ofs_running); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.cpp new file mode 100644 index 00000000000..e58062193e2 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.cpp @@ -0,0 +1,8270 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" +#include "source_lcao/module_ri/singular_value.h" + +#include "source_basis/module_ao/orb_read.h" +#include "source_basis/module_ao/elem_basis_idx_orb.h" +#include "source_basis/module_pw/pw_basis.h" +#include "source_basis/module_pw/pw_basis_k.h" +#include "source_cell/unitcell.h" +#include "source_estate/elecstate.h" +#include "source_estate/module_pot/potential_new.h" +#include "source_hamilt/module_xc/exx_info.h" +#include "source_io/module_bessel/numerical_basis.h" +#include "source_io/module_parameter/parameter.h" +#include "source_hamilt/module_gint/gint_atom.h" +#include "source_lcao/module_ri/matrix_orbs11.h" +#include "source_lcao/module_ri/conv_coulomb_pot_k.h" +#include "source_lcao/module_ri/exx_abfs_ctor_orbs.h" +#include "source_lcao/module_ri/exx_abfs_io.h" +#include "source_lcao/module_rpa/rpa_abfs_preorth.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_channel_parallel.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_delta.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_matrix_audit.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_unit.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_dispatch.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.h" +#include +#include "source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.h" +#include "source_cell/stru_fac.h" +#include "source_base/module_external/blas_connector.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef COMMIT_INFO +#include "commit.h" +#endif + +#ifdef __MPI +#include +#endif + +#ifdef _OPENMP +#include +#endif + +namespace ModuleRI +{ +namespace +{ + +namespace siab = ::module_ri::sternheimer_siab; +namespace sternheimer_chi0 = ::module_ri::sternheimer_chi0; + +std::string sternheimer_abfs_perturbation_source(const std::vector& explicit_abfs_files) +{ + return explicit_abfs_files.empty() ? "product_pca" : "explicit_abfs"; +} + +bool sternheimer_builds_product_pca_auxiliary_basis(const std::vector& explicit_abfs_files) +{ + return explicit_abfs_files.empty(); +} + +constexpr const char* kSmokeEnv = "ABACUS_STERNHEIMER_FD_ST_SMOKE"; +constexpr const char* kOutputEnv = "ABACUS_STERNHEIMER_FD_ST_OUT"; +constexpr const char* kBandsEnv = "ABACUS_STERNHEIMER_FD_ST_BANDS"; +constexpr const char* kChannelsEnv = "ABACUS_STERNHEIMER_FD_ST_CHANNELS"; +constexpr const char* kChannelThreadsEnv = "ABACUS_STERNHEIMER_CHANNEL_THREADS"; +constexpr const char* kChannelMaxWorkersEnv = "ABACUS_STERNHEIMER_CHANNEL_MAX_WORKERS"; +constexpr const char* kMaxDenseEnv = "ABACUS_STERNHEIMER_FD_ST_MAX_DENSE"; +constexpr const char* kLanczosSubspaceEnv = "ABACUS_STERNHEIMER_FD_ST_LANCZOS_SUBSPACE"; +constexpr const char* kOmegaEnv = "ABACUS_STERNHEIMER_FD_ST_OMEGA"; +constexpr const char* kSolverToleranceEnv = "ABACUS_STERNHEIMER_FD_ST_SOLVER_TOL"; +constexpr const char* kSolverMaxIterEnv = "ABACUS_STERNHEIMER_FD_ST_MAX_ITER"; +constexpr const char* kSpectralPreconditionerEnv = "ABACUS_STERNHEIMER_FD_ST_SPECTRAL_PRECONDITIONER"; +constexpr const char* kSpectralPreconditionerRegularizationEnv + = "ABACUS_STERNHEIMER_FD_ST_SPECTRAL_PRECONDITIONER_REGULARIZATION"; +constexpr const char* kPCAThresholdEnv = "ABACUS_STERNHEIMER_FD_ST_PCA_THRESHOLD"; +constexpr const char* kCCPRmeshTimesEnv = "ABACUS_STERNHEIMER_FD_ST_CCP_RMESH_TIMES"; +constexpr const char* kOrbitalDirEnv = "ABACUS_STERNHEIMER_FD_ST_ORBITAL_DIR"; +constexpr const char* kOrbitalFilesEnv = "ABACUS_STERNHEIMER_FD_ST_ORBITAL_FILES"; +constexpr const char* kFrequencyRankShiftEnv = "ABACUS_STERNHEIMER_FD_ST_FREQ_RANK_SHIFT"; +constexpr const char* kDeltaComponentDiagnosticEnv = "ABACUS_STERNHEIMER_DELTA_COMPONENT_DIAG"; +constexpr const char* kLCAOSOSDiagnosticEnv = "ABACUS_STERNHEIMER_LCAO_SOS_DIAG"; +constexpr const char* kKResolvedDiagnosticEnv = "ABACUS_STERNHEIMER_KRESOLVED_DIAG"; +constexpr const char* kWavefunctionDiagnosticEnv = "ABACUS_STERNHEIMER_WAVEFUNCTION_DIAGNOSTIC"; +constexpr const char* kSupercellTranslationSumEnv = "ABACUS_STERNHEIMER_SUPERCELL_TRANSLATION_SUM"; +constexpr const char* kSupercellFullResponseEnv = "ABACUS_STERNHEIMER_SUPERCELL_FULL_RESPONSE"; +constexpr const char* kSupercellKPointGroupsEnv = "ABACUS_STERNHEIMER_SUPERCELL_KPOINT_GROUPS"; +constexpr const char* kTruncatedV1DiagnosticEnv = "ABACUS_STERNHEIMER_TRUNCATED_V1_DIAGNOSTIC"; +constexpr const char* kDeltaABlockModeEnv = "ABACUS_STERNHEIMER_DELTA_A_BLOCK"; +constexpr double kHartreeToRydberg = 2.0; + +void hash_u64(siab::Sha256& digest, const std::uint64_t value) +{ + std::array bytes; + for (std::size_t index = 0; index != bytes.size(); ++index) + { + bytes[index] = static_cast((value >> (56U - 8U * index)) & 0xffU); + } + digest.update(bytes.data(), bytes.size()); +} + +void hash_int(siab::Sha256& digest, const int value) +{ + hash_u64(digest, static_cast(static_cast(value))); +} + +void hash_double(siab::Sha256& digest, const double value) +{ + static_assert(sizeof(double) == sizeof(std::uint64_t), "Sternheimer SIAB provenance requires binary64 doubles."); + std::uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + hash_u64(digest, bits); +} + +std::string hash_coulomb_whitening_transform(const SternheimerCoulombWhitening& whitening) +{ + siab::Sha256 digest; + const std::string marker = "ABACUS_STERNHEIMER_COULOMB_WHITENING_V1"; + digest.update(reinterpret_cast(marker.data()), marker.size()); + hash_int(digest, whitening.raw_dimension); + hash_int(digest, whitening.retained_rank); + hash_double(digest, whitening.relative_threshold); + for (const double value: whitening.transform) + { + if (!std::isfinite(value)) + { + throw std::runtime_error("Sternheimer Coulomb whitening transform contains a non-finite value."); + } + hash_double(digest, value); + } + return digest.finish(); +} + +std::string compiled_commit_metadata() +{ +#ifdef COMMIT_INFO + return COMMIT; +#else + return std::string(); +#endif +} + +std::vector cell_vectors_bohr(const UnitCell& ucell) +{ + return {ucell.lat0 * ucell.latvec.e11, + ucell.lat0 * ucell.latvec.e12, + ucell.lat0 * ucell.latvec.e13, + ucell.lat0 * ucell.latvec.e21, + ucell.lat0 * ucell.latvec.e22, + ucell.lat0 * ucell.latvec.e23, + ucell.lat0 * ucell.latvec.e31, + ucell.lat0 * ucell.latvec.e32, + ucell.lat0 * ucell.latvec.e33}; +} + +std::string lower_string(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +bool env_is_true(const char* name) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return false; + } + const std::string value = lower_string(raw); + return !(value.empty() || value == "0" || value == "false" || value == "off" || value == "no"); +} + +SternheimerDeltaABlockMode delta_a_block_mode_from_env() +{ + const char* raw = std::getenv(kDeltaABlockModeEnv); + if (raw == nullptr || raw[0] == '\0') + { + return SternheimerDeltaABlockMode::ReferenceValueGradient; + } + return parse_sternheimer_delta_a_block_mode(lower_string(raw)); +} + +int positive_int_from_env(const char* name, const int default_value) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_value; + } + std::size_t parsed = 0; + const int value = std::stoi(raw, &parsed); + if (raw[parsed] != '\0' || value <= 0) + { + throw std::invalid_argument(std::string("Invalid positive integer in ") + name + "."); + } + return value; +} + +int int_from_env(const char* name, const int default_value) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_value; + } + std::size_t parsed = 0; + const int value = std::stoi(raw, &parsed); + if (raw[parsed] != '\0') + { + throw std::invalid_argument(std::string("Invalid integer in ") + name + "."); + } + return value; +} + +int sternheimer_channel_openmp_threads() +{ +#ifdef _OPENMP + return omp_get_max_threads(); +#else + return 1; +#endif +} + +double positive_double_from_env(const char* name, const double default_value) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_value; + } + std::size_t parsed = 0; + const double value = std::stod(raw, &parsed); + if (raw[parsed] != '\0' || value <= 0.0) + { + throw std::invalid_argument(std::string("Invalid positive floating-point value in ") + name + "."); + } + return value; +} + +double nonnegative_double_from_env(const char* name, const double default_value) +{ + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_value; + } + std::size_t parsed = 0; + const double value = std::stod(raw, &parsed); + if (raw[parsed] != '\0' || value < 0.0) + { + throw std::invalid_argument(std::string("Invalid non-negative floating-point value in ") + name + "."); + } + return value; +} + +std::uint64_t slurm_memory_limit_bytes() +{ + const char* raw = std::getenv("SLURM_MEM_PER_NODE"); + if (raw == nullptr || raw[0] == '\0') + { + return 0; + } + char* end = nullptr; + const unsigned long long memory_mib = std::strtoull(raw, &end, 10); + if (end == raw || *end != '\0' || memory_mib == 0 + || memory_mib > std::numeric_limits::max() / (1024U * 1024U)) + { + return 0; + } + return static_cast(memory_mib) * 1024U * 1024U; +} + +std::string default_report_path(const std::string& output_dir) +{ + const char* explicit_path = std::getenv(kOutputEnv); + if (explicit_path != nullptr && explicit_path[0] != '\0') + { + return explicit_path; + } + if (output_dir.empty()) + { + return "STERNHEIMER_FD_ST.dat"; + } + if (output_dir.back() == '/') + { + return output_dir + "STERNHEIMER_FD_ST.dat"; + } + return output_dir + "/STERNHEIMER_FD_ST.dat"; +} + +std::string join_output_path(const std::string& output_dir, const std::string& filename) +{ + if (output_dir.empty()) + { + return filename; + } + if (output_dir.back() == '/') + { + return output_dir + filename; + } + return output_dir + "/" + filename; +} + +std::string chi0_status_path(const std::string& output_dir, const SternheimerAbacusRuntime& runtime) +{ + return join_output_path(output_dir, + runtime.input.out_sternheimer_siab ? "STERNHEIMER_SIAB_STATUS.dat" + : "STERNHEIMER_CHI0.dat"); +} + +std::string chi0_v1_filename(const int iq, const int ifrequency, const int rank) +{ + std::ostringstream out; + out << "v1_sternheimer_chi0_iq_" << iq << "_ifreq_" << ifrequency << "_rank" << rank << ".dat"; + return out.str(); +} + +std::string delta_component_v1_filename(const std::string& component, + const int iq, + const int ifrequency, + const int rank) +{ + std::ostringstream out; + out << "v1_sternheimer_delta_" << component << "_iq_" << iq << "_ifreq_" << ifrequency + << "_rank" << rank << ".dat"; + return out.str(); +} + +std::string lcao_sos_v1_filename(const int iq, const int ifrequency, const int rank) +{ + std::ostringstream out; + out << "v1_sternheimer_lcao_sos_iq_" << iq << "_ifreq_" << ifrequency << "_rank" << rank << ".dat"; + return out.str(); +} + +std::string chi0_progress_filename(const SternheimerAbacusRuntime& runtime) +{ + std::ostringstream out; + out << (runtime.input.out_sternheimer_siab ? "STERNHEIMER_SIAB_PROGRESS_rank" + : "STERNHEIMER_CHI0_PROGRESS_rank") + << runtime.rank << ".dat"; + return out.str(); +} + +std::string chi0_failure_filename(const int rank) +{ + std::ostringstream out; + out << "STERNHEIMER_CHI0_FAILURE_rank" << rank << ".dat"; + return out.str(); +} + +double elapsed_seconds_since(const std::chrono::steady_clock::time_point& start) +{ + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); +} + +std::string precise_double_string(const double value) +{ + std::ostringstream out; + out << std::setprecision(16) << value; + return out.str(); +} + +void reset_chi0_progress_file(const SternheimerAbacusRuntime& runtime) +{ + std::ofstream out(chi0_progress_filename(runtime).c_str()); + out << std::setprecision(16); + out << "event rank ifrequency owner_rank band channel solved_equations converged iterations " + "solver_relative_residual equation_residual_norm elapsed_s note\n"; +} + +void append_chi0_progress_event(const std::string& event, + const int ifrequency, + const int owner_rank, + const int band, + const int channel, + const int solved_equations, + const SternheimerRPA::SolverResult* solver_result, + const double equation_residual_norm, + const double elapsed_seconds, + const std::string& note, + const SternheimerAbacusRuntime& runtime) +{ + static std::mutex progress_mutex; + const std::lock_guard lock(progress_mutex); + std::ofstream out(chi0_progress_filename(runtime).c_str(), std::ios::app); + out << std::setprecision(16); + out << event << ' ' << runtime.rank << ' ' << ifrequency << ' ' << owner_rank << ' ' << band << ' ' << channel + << ' ' << solved_equations << ' '; + if (solver_result == nullptr) + { + out << "- -1 -1 "; + } + else + { + out << (solver_result->converged ? "yes" : "no") << ' ' << solver_result->iterations << ' ' + << solver_result->relative_residual << ' '; + } + out << equation_residual_norm << ' ' << elapsed_seconds << ' ' << note << '\n'; +} + +int occupied_band_count(const elecstate::ElecState& elec_state, const int k_index) +{ + int count = 0; + for (int ib = 0; ib != elec_state.wg.nc; ++ib) + { + if (elec_state.wg(k_index, ib) > 1.0e-8) + { + count = ib + 1; + } + } + return count; +} + +std::vector eigenvalues_ry_from_elec_state(const elecstate::ElecState& elec_state, const int k_index) +{ + std::vector eigenvalues; + eigenvalues.reserve(static_cast(elec_state.ekb.nc)); + for (int ib = 0; ib != elec_state.ekb.nc; ++ib) + { + eigenvalues.push_back(elec_state.ekb(k_index, ib)); + } + return eigenvalues; +} + +std::vector occupations_from_elec_state(const elecstate::ElecState& elec_state, const int k_index) +{ + std::vector occupations; + occupations.reserve(static_cast(elec_state.wg.nc)); + for (int ib = 0; ib != elec_state.wg.nc; ++ib) + { + occupations.push_back(elec_state.wg(k_index, ib)); + } + return occupations; +} + +SternheimerRPA::TransitionEnergyWindow transition_window_from_all_kpoints( + const elecstate::ElecState& elec_state, + const std::vector& records) +{ + SternheimerRPA::TransitionEnergyWindow combined; + combined.emin_ha = std::numeric_limits::max(); + combined.emax_ha = 0.0; + for (const SternheimerLCAOOccupiedKPoint& record: records) + { + const SternheimerRPA::TransitionEnergyWindow window + = SternheimerRPA::transition_energy_window_from_eigenvalues_ry( + eigenvalues_ry_from_elec_state(elec_state, record.zero_order_k_index), + occupations_from_elec_state(elec_state, record.zero_order_k_index)); + combined.emin_ha = std::min(combined.emin_ha, window.emin_ha); + combined.emax_ha = std::max(combined.emax_ha, window.emax_ha); + } + return combined; +} + +SternheimerRPA::TransitionEnergyWindow transition_window_from_recovered_kpoints( + const std::vector& records) +{ + SternheimerRPA::TransitionEnergyWindow combined; + combined.emin_ha = std::numeric_limits::max(); + combined.emax_ha = 0.0; + for (const SternheimerLCAOOccupiedKPoint& record: records) + { + std::vector eigenvalues = record.eigenvalues; + eigenvalues.insert(eigenvalues.end(), + record.unoccupied_eigenvalues.begin(), + record.unoccupied_eigenvalues.end()); + std::vector occupations = record.occupations; + occupations.resize(eigenvalues.size(), 0.0); + const auto window = SternheimerRPA::transition_energy_window_from_eigenvalues_ry( + eigenvalues, occupations); + combined.emin_ha = std::min(combined.emin_ha, window.emin_ha); + combined.emax_ha = std::max(combined.emax_ha, window.emax_ha); + } + return combined; +} + +struct SternheimerFixedQPermutation +{ + int spatial_isym = -1; + bool time_reversal = false; + std::vector mapped_index_by_full_k; + std::vector> fold_G_by_full_k; +}; + +std::vector build_sternheimer_fixed_q_little_group_permutations( + const UnitCell& ucell, + const std::vector& records, + const SternheimerReducedKPoint& qpoint, + const std::vector& allowed_spatial_operations) +{ + if (ucell.symm.nrotk <= 0 || records.empty() || allowed_spatial_operations.empty()) + { + throw std::runtime_error("Cannot build Sternheimer fixed-q permutations without symmetry operations."); + } + std::vector spatial_operation_allowed(static_cast(ucell.symm.nrotk), false); + for (const int isym: allowed_spatial_operations) + { + if (isym < 0 || isym >= ucell.symm.nrotk) + { + throw std::runtime_error("Sternheimer FD stencil symmetry has an invalid spatial index."); + } + spatial_operation_allowed[static_cast(isym)] = true; + } + const double tolerance = std::max(1.0e-10, ucell.symm.epsilon); + auto canonical = [tolerance](ModuleBase::Vector3 point) { + point.x = std::fmod(point.x + 100.5 - 0.5 * tolerance, 1.0) - 0.5 + 0.5 * tolerance; + point.y = std::fmod(point.y + 100.5 - 0.5 * tolerance, 1.0) - 0.5 + 0.5 * tolerance; + point.z = std::fmod(point.z + 100.5 - 0.5 * tolerance, 1.0) - 0.5 + 0.5 * tolerance; + if (std::abs(point.x) < tolerance) + { + point.x = 0.0; + } + if (std::abs(point.y) < tolerance) + { + point.y = 0.0; + } + if (std::abs(point.z) < tolerance) + { + point.z = 0.0; + } + return point; + }; + auto same_point = [tolerance](const ModuleBase::Vector3& lhs, + const ModuleBase::Vector3& rhs) { + return std::abs(lhs.x - rhs.x) < tolerance && std::abs(lhs.y - rhs.y) < tolerance + && std::abs(lhs.z - rhs.z) < tolerance; + }; + + std::vector> full_kpoints(records.size()); + for (const auto& record: records) + { + if (record.global_k_index < 0 || record.global_k_index >= static_cast(records.size())) + { + throw std::runtime_error("Sternheimer fixed-q permutation found an invalid full-k index."); + } + full_kpoints[static_cast(record.global_k_index)] + = canonical({record.kpoint[0], record.kpoint[1], record.kpoint[2]}); + } + const ModuleBase::Vector3 q = canonical({qpoint[0], qpoint[1], qpoint[2]}); + std::vector permutations; + for (const bool time_reversal: {false, true}) + { + for (int isym = 0; isym != ucell.symm.nrotk; ++isym) + { + if (!spatial_operation_allowed[static_cast(isym)]) + { + continue; + } + ModuleBase::Vector3 transformed_q = q * ucell.symm.kgmatrix[isym]; + if (time_reversal) + { + transformed_q = transformed_q * -1.0; + } + if (!same_point(canonical(transformed_q), q)) + { + continue; + } + + SternheimerFixedQPermutation operation; + operation.spatial_isym = isym; + operation.time_reversal = time_reversal; + operation.mapped_index_by_full_k.assign(records.size(), -1); + operation.fold_G_by_full_k.assign(records.size(), {0, 0, 0}); + for (std::size_t ik = 0; ik != full_kpoints.size(); ++ik) + { + ModuleBase::Vector3 transformed + = full_kpoints[ik] * ucell.symm.kgmatrix[isym]; + if (time_reversal) + { + transformed = transformed * -1.0; + } + const ModuleBase::Vector3 unfolded = transformed; + transformed = canonical(unfolded); + for (std::size_t target = 0; target != full_kpoints.size(); ++target) + { + if (same_point(transformed, full_kpoints[target])) + { + operation.mapped_index_by_full_k[ik] = static_cast(target); + const ModuleBase::Vector3 difference + = unfolded - full_kpoints[target]; + operation.fold_G_by_full_k[ik] + = {static_cast(std::llround(difference.x)), + static_cast(std::llround(difference.y)), + static_cast(std::llround(difference.z))}; + if (std::abs(difference.x - operation.fold_G_by_full_k[ik][0]) >= tolerance + || std::abs(difference.y - operation.fold_G_by_full_k[ik][1]) >= tolerance + || std::abs(difference.z - operation.fold_G_by_full_k[ik][2]) >= tolerance) + { + throw std::runtime_error( + "A Sternheimer fixed-q symmetry route has a noninteger reciprocal fold."); + } + break; + } + } + if (operation.mapped_index_by_full_k[ik] < 0) + { + throw std::runtime_error( + "A Sternheimer fixed-q little-group operation leaves the full k grid."); + } + } + permutations.push_back(std::move(operation)); + } + } + if (permutations.empty()) + { + throw std::runtime_error("Sternheimer fixed-q little group is empty."); + } + return permutations; +} + +std::vector build_sternheimer_discrete_qstar_permutations( + const UnitCell& ucell, + const std::vector& records, + const std::vector& allowed_spatial_operations) +{ + if (ucell.symm.nrotk <= 0 || records.empty() || allowed_spatial_operations.empty()) + { + throw std::runtime_error("Cannot build discrete Sternheimer q-star permutations."); + } + std::vector spatial_operation_allowed(static_cast(ucell.symm.nrotk), false); + for (const int isym: allowed_spatial_operations) + { + if (isym < 0 || isym >= ucell.symm.nrotk) + { + throw std::runtime_error("Sternheimer q-star symmetry has an invalid spatial index."); + } + spatial_operation_allowed[static_cast(isym)] = true; + } + + const double tolerance = std::max(1.0e-10, ucell.symm.epsilon); + auto canonical = [tolerance](ModuleBase::Vector3 point) { + point.x = std::fmod(point.x + 100.5 - 0.5 * tolerance, 1.0) - 0.5 + 0.5 * tolerance; + point.y = std::fmod(point.y + 100.5 - 0.5 * tolerance, 1.0) - 0.5 + 0.5 * tolerance; + point.z = std::fmod(point.z + 100.5 - 0.5 * tolerance, 1.0) - 0.5 + 0.5 * tolerance; + if (std::abs(point.x) < tolerance) + { + point.x = 0.0; + } + if (std::abs(point.y) < tolerance) + { + point.y = 0.0; + } + if (std::abs(point.z) < tolerance) + { + point.z = 0.0; + } + return point; + }; + auto same_point = [tolerance](const ModuleBase::Vector3& lhs, + const ModuleBase::Vector3& rhs) { + return std::abs(lhs.x - rhs.x) < tolerance && std::abs(lhs.y - rhs.y) < tolerance + && std::abs(lhs.z - rhs.z) < tolerance; + }; + + std::vector> full_qpoints(records.size()); + for (const auto& record: records) + { + if (record.global_k_index < 0 || record.global_k_index >= static_cast(records.size())) + { + throw std::runtime_error("Sternheimer q-star permutation found an invalid full-q index."); + } + full_qpoints[static_cast(record.global_k_index)] + = canonical({record.kpoint[0], record.kpoint[1], record.kpoint[2]}); + } + + std::vector permutations; + for (const bool time_reversal: {false, true}) + { + for (int isym = 0; isym != ucell.symm.nrotk; ++isym) + { + if (!spatial_operation_allowed[static_cast(isym)]) + { + continue; + } + SternheimerQStarPermutation operation; + operation.spatial_isym = isym; + operation.time_reversal = time_reversal; + operation.mapped_index_by_full_q.assign(records.size(), -1); + operation.fold_G_by_full_q.assign(records.size(), {0, 0, 0}); + for (std::size_t iq = 0; iq != full_qpoints.size(); ++iq) + { + ModuleBase::Vector3 transformed + = full_qpoints[iq] * ucell.symm.kgmatrix[isym]; + if (time_reversal) + { + transformed = transformed * -1.0; + } + const ModuleBase::Vector3 unfolded = transformed; + transformed = canonical(unfolded); + for (std::size_t target = 0; target != full_qpoints.size(); ++target) + { + if (!same_point(transformed, full_qpoints[target])) + { + continue; + } + operation.mapped_index_by_full_q[iq] = static_cast(target); + const ModuleBase::Vector3 difference = unfolded - full_qpoints[target]; + operation.fold_G_by_full_q[iq] + = {static_cast(std::llround(difference.x)), + static_cast(std::llround(difference.y)), + static_cast(std::llround(difference.z))}; + const auto& fold = operation.fold_G_by_full_q[iq]; + if (std::abs(difference.x - fold[0]) >= tolerance + || std::abs(difference.y - fold[1]) >= tolerance + || std::abs(difference.z - fold[2]) >= tolerance) + { + throw std::runtime_error( + "A Sternheimer q-star route has a noninteger reciprocal fold."); + } + break; + } + if (operation.mapped_index_by_full_q[iq] < 0) + { + throw std::runtime_error( + "A discrete Sternheimer q-star operation leaves the full q grid."); + } + } + permutations.push_back(std::move(operation)); + } + } + if (permutations.empty()) + { + throw std::runtime_error("The discrete Sternheimer q-star group is empty."); + } + return permutations; +} + +std::vector split_orbital_file_list(const std::string& raw) +{ + std::vector files; + std::string current; + for (const char ch: raw) + { + if (ch == ',' || ch == ':') + { + if (!current.empty()) + { + files.push_back(current); + current.clear(); + } + } + else + { + current.push_back(ch); + } + } + if (!current.empty()) + { + files.push_back(current); + } + return files; +} + +std::string join_path(const std::string& dir, const std::string& file) +{ + if (file.empty()) + { + return file; + } + if (!file.empty() && file.front() == '/') + { + return file; + } + if (dir.empty() || dir.back() == '/') + { + return dir + file; + } + return dir + "/" + file; +} + +void validate_orbital_files(const std::string& orbital_dir, const std::vector& orbital_files) +{ + if (orbital_files.empty()) + { + throw std::runtime_error("No numerical orbital files are available. Set NUMERICAL_ORBITAL in STRU or " + "ABACUS_STERNHEIMER_FD_ST_ORBITAL_FILES."); + } + for (const std::string& file: orbital_files) + { + const std::string path = join_path(orbital_dir, file); + std::ifstream in(path.c_str()); + if (!in) + { + throw std::runtime_error("Cannot open Sternheimer ST orbital file: " + path + "."); + } + } +} + +std::vector orbital_files_from_env_or_cell(const UnitCell& ucell) +{ + const char* raw = std::getenv(kOrbitalFilesEnv); + if (raw != nullptr && raw[0] != '\0') + { + return split_orbital_file_list(raw); + } + return ucell.orbital_fn; +} + +std::string orbital_dir_from_env_or_input(const SternheimerAbacusRuntime& runtime) +{ + const char* raw = std::getenv(kOrbitalDirEnv); + if (raw != nullptr) + { + return raw; + } + return runtime.input.orbital_dir; +} + +siab::Provenance make_siab_production_provenance(const UnitCell& ucell, + const std::string& auxiliary_basis_sha256, + const SternheimerRPA::FrequencyGrid& frequency_grid, + const double pca_threshold, + const SternheimerCoulombWhitening& whitening, + const SternheimerAbacusRuntime& runtime) +{ + siab::Provenance provenance; + provenance.abacus_commit = siab::require_source_commit(compiled_commit_metadata()); + provenance.auxiliary_basis_sha256 = auxiliary_basis_sha256; + provenance.cell_bohr = cell_vectors_bohr(ucell); + provenance.ecut_ry = runtime.input.ecutwfc; + provenance.kernel = "full_coulomb"; + const std::vector orbital_files + = siab::resolve_required_input_files(orbital_dir_from_env_or_input(runtime), + orbital_files_from_env_or_cell(ucell), + "initial orbital"); + const std::vector pseudopotential_files + = siab::resolve_required_input_files(runtime.input.pseudo_dir, ucell.pseudo_fn, "pseudopotential"); + provenance.orbital_sha256 = siab::sha256_file_manifest(orbital_files); + provenance.pseudopotential_sha256 = siab::sha256_file_manifest(pseudopotential_files); + provenance.spin_convention = "spin_resolved_occupation_in_reference_rows"; + provenance.executable_sha256 = siab::sha256_file(siab::resolve_executable_path()); + provenance.exx_pca_thr = pca_threshold; + provenance.sternheimer_nfreq = static_cast(frequency_grid.omega_ha.size()); + provenance.frequency_ha = frequency_grid.omega_ha; + provenance.frequency_weights_ha = frequency_grid.weights_ha; + provenance.mpi_ranks = runtime.nproc; + provenance.omp_threads = runtime.nthread_per_proc; + provenance.auxiliary_whitening = "global_full_coulomb_v1"; + provenance.raw_auxiliary_dimension = whitening.raw_dimension; + provenance.whitened_auxiliary_rank = whitening.retained_rank; + provenance.discarded_auxiliary_rank = whitening.discarded_rank; + provenance.coulomb_relative_threshold = whitening.relative_threshold; + provenance.coulomb_eigenvalues = whitening.eigenvalues; + provenance.coulomb_max_orthonormality_error = whitening.max_orthonormality_error; + provenance.coulomb_transform_sha256 = hash_coulomb_whitening_transform(whitening); + return provenance; +} + +void read_sternheimer_orbitals(const UnitCell& ucell, + LCAO_Orbitals& orb, + const SternheimerAbacusRuntime& runtime) +{ + std::vector orbital_files = orbital_files_from_env_or_cell(ucell); + if (static_cast(orbital_files.size()) != ucell.ntype) + { + throw std::runtime_error("Sternheimer ST requires one numerical orbital file per atom type."); + } + + std::string orbital_dir = orbital_dir_from_env_or_input(runtime); + const bool all_absolute = std::all_of(orbital_files.begin(), orbital_files.end(), [](const std::string& file) { + return !file.empty() && file.front() == '/'; + }); + if (all_absolute) + { + orbital_dir.clear(); + } + validate_orbital_files(orbital_dir, orbital_files); + + const double lcao_ecut = runtime.input.lcao_ecut > 0.0 ? runtime.input.lcao_ecut : runtime.input.ecutwfc; + if (lcao_ecut <= 0.0) + { + throw std::runtime_error("Sternheimer ST requires positive lcao_ecut or ecutwfc to read orbitals."); + } + + orb.init(runtime.log, + ucell.ntype, + orbital_dir, + orbital_files.data(), + ucell.descriptor_file, + runtime.input.lmaxmax, + lcao_ecut, + runtime.input.lcao_dk, + runtime.input.lcao_dr, + runtime.input.lcao_rmax, + runtime.deepks_setorb, + runtime.input.out_mat_r[0], + runtime.input.out_element_info, + runtime.input.cal_force, + runtime.rank); +} + +std::map>> + make_fock_hartree_coulomb_param() +{ + return {{Conv_Coulomb_Pot_K::Coulomb_Type::Fock, {{{"alpha", "1"}, {"singularity_correction", "limits"}}}}}; +} + +struct SternheimerABFSInput +{ + std::vector> radials_by_type; + std::vector atom_types; + std::vector> atom_positions; +}; + +SternheimerOrbitalSet build_sternheimer_abfs(const UnitCell& ucell, + const double pca_threshold, + const SternheimerAbacusRuntime& runtime) +{ + LCAO_Orbitals orb; + read_sternheimer_orbitals(ucell, orb, runtime); + std::vector>> abfs; + if (sternheimer_builds_product_pca_auxiliary_basis(runtime.abfs_files)) + { + auto lcaos = Exx_Abfs::Construct_Orbs::change_orbs(orb, runtime.abfs_kmesh_times); + Exx_Abfs::Construct_Orbs::filter_empty_orbs(lcaos); + abfs = Exx_Abfs::Construct_Orbs::abfs_same_atom(ucell, + orb, + lcaos, + runtime.abfs_kmesh_times, + pca_threshold); + } + else + { + abfs = Exx_Abfs::IO::construct_abfs(orb, + runtime.abfs_files, + runtime.abfs_kmesh_times); + } + Exx_Abfs::Construct_Orbs::filter_empty_orbs(abfs); + const PreorthReport preorth_report + = preorth_from_input(abfs, runtime.input, runtime.input.cal_force); + if (runtime.rank == 0) + { + runtime.log << ::ModuleRI::preorth_report(preorth_report); + } + return abfs; +} + +SternheimerABFSInput make_sternheimer_abfs_input(const UnitCell& ucell, + const SternheimerOrbitalSet& orbitals) +{ + SternheimerABFSInput input; + input.radials_by_type = make_sternheimer_radial_perturbations_from_orbitals(orbitals); + input.atom_types.reserve(ucell.nat); + input.atom_positions.reserve(ucell.nat); + for (int it = 0; it != ucell.ntype; ++it) + { + const Atom& atom = ucell.atoms[it]; + for (int ia = 0; ia != atom.na; ++ia) + { + input.atom_types.push_back(it); + input.atom_positions.push_back(atom.tau[ia] * ucell.lat0); + } + } + return input; +} + +SternheimerABFSInput build_abfs_ccp_input(const UnitCell& ucell, + const double pca_threshold, + const double ccp_rmesh_times, + const SternheimerAbacusRuntime& runtime) +{ + const SternheimerOrbitalSet abfs = build_sternheimer_abfs(ucell, pca_threshold, runtime); + const SternheimerOrbitalSet abfs_ccp + = Conv_Coulomb_Pot_K::cal_orbs_ccp(abfs, make_fock_hartree_coulomb_param(), ccp_rmesh_times); + return make_sternheimer_abfs_input(ucell, abfs_ccp); +} + +SternheimerABFSInput build_abfs_density_input(const UnitCell& ucell, + const double pca_threshold, + const SternheimerAbacusRuntime& runtime, + const SternheimerOrbitalSet* reusable_rpa_abfs = nullptr) +{ + if (reusable_rpa_abfs != nullptr) + { + return make_sternheimer_abfs_input(ucell, *reusable_rpa_abfs); + } + const SternheimerOrbitalSet abfs = build_sternheimer_abfs(ucell, pca_threshold, runtime); + return make_sternheimer_abfs_input(ucell, abfs); +} + +struct SternheimerABFBuildData +{ + std::vector channels; + std::vector full_coulomb_metric; + std::vector> radials_by_type; + std::vector atom_types; + std::vector> atom_positions; + PreorthReport preorth_report; +}; + +std::vector build_molecular_coulomb_metric( + const UnitCell& ucell, + const LCAO_Orbitals& orb, + const std::vector>>& abfs_ccp, + const std::vector>>& abfs, + const std::vector& channels, + const SternheimerAbacusRuntime& runtime) +{ + const ModuleBase::Element_Basis_Index::Range range_ccp + = ModuleBase::Element_Basis_Index::construct_range(abfs_ccp); + const ModuleBase::Element_Basis_Index::Range range_abfs + = ModuleBase::Element_Basis_Index::construct_range(abfs); + const ModuleBase::Element_Basis_Index::IndexLNM index_ccp + = ModuleBase::Element_Basis_Index::construct_index(range_ccp); + const ModuleBase::Element_Basis_Index::IndexLNM index_abfs + = ModuleBase::Element_Basis_Index::construct_index(range_abfs); + if (index_ccp.size() != index_abfs.size()) + { + throw std::runtime_error("Sternheimer Coulomb metric ABFS type counts do not match."); + } + for (std::size_t type = 0; type != index_ccp.size(); ++type) + { + if (index_ccp[type].count_size != index_abfs[type].count_size) + { + throw std::runtime_error("Sternheimer Coulomb metric ABFS channel counts do not match."); + } + } + + std::vector atom_offsets; + std::vector atom_types; + std::vector atom_local_indices; + atom_offsets.reserve(static_cast(ucell.nat)); + atom_types.reserve(static_cast(ucell.nat)); + atom_local_indices.reserve(static_cast(ucell.nat)); + std::size_t total_dimension = 0; + for (int type = 0; type != ucell.ntype; ++type) + { + for (int atom = 0; atom != ucell.atoms[type].na; ++atom) + { + atom_offsets.push_back(total_dimension); + atom_types.push_back(type); + atom_local_indices.push_back(atom); + total_dimension += index_abfs[static_cast(type)].count_size; + } + } + if (total_dimension != channels.size()) + { + throw std::runtime_error("Sternheimer Coulomb metric dimension does not match sampled ABFS channels."); + } + for (const SternheimerABFGridChannel& channel: channels) + { + const std::size_t atom = static_cast(channel.atom_index); + if (atom >= atom_offsets.size() + || atom_offsets[atom] + static_cast(channel.atom_local_index) + != static_cast(channel.channel_index)) + { + throw std::runtime_error("Sternheimer Coulomb metric channel ordering is inconsistent with ABFS indices."); + } + } + + Matrix_Orbs11 metric_builder; + metric_builder.init(abfs_ccp, abfs, ucell, orb, runtime.abfs_kmesh_times); + metric_builder.init_radial_table(); + std::vector metric(total_dimension * total_dimension, 0.0); + for (std::size_t atom_a = 0; atom_a != atom_offsets.size(); ++atom_a) + { + const std::size_t type_a = static_cast(atom_types[atom_a]); + const auto& tau_a = ucell.atoms[type_a].tau[static_cast(atom_local_indices[atom_a])]; + const std::size_t count_a = index_ccp[type_a].count_size; + for (std::size_t atom_b = 0; atom_b != atom_offsets.size(); ++atom_b) + { + const std::size_t type_b = static_cast(atom_types[atom_b]); + const auto& tau_b = ucell.atoms[type_b].tau[static_cast(atom_local_indices[atom_b])]; + const std::size_t count_b = index_abfs[type_b].count_size; + const RI::Tensor block + = metric_builder.cal_overlap_matrix(type_a, + type_b, + tau_a, + tau_b, + index_ccp, + index_abfs, + Matrix_Orbs11::Matrix_Order::AB); + for (std::size_t local_a = 0; local_a != count_a; ++local_a) + { + for (std::size_t local_b = 0; local_b != count_b; ++local_b) + { + metric[(atom_offsets[atom_a] + local_a) * total_dimension + + atom_offsets[atom_b] + local_b] + = block(local_a, local_b); + } + } + } + } + return metric; +} + +SternheimerABFBuildData build_abfs_ccp_data(const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const int max_channels, + const double pca_threshold, + const double ccp_rmesh_times, + const bool build_coulomb_metric, + const SternheimerAbacusRuntime& runtime) +{ + LCAO_Orbitals orb; + read_sternheimer_orbitals(ucell, orb, runtime); + std::vector>> abfs; + if (sternheimer_builds_product_pca_auxiliary_basis(runtime.abfs_files)) + { + auto lcaos = Exx_Abfs::Construct_Orbs::change_orbs(orb, runtime.abfs_kmesh_times); + Exx_Abfs::Construct_Orbs::filter_empty_orbs(lcaos); + abfs = Exx_Abfs::Construct_Orbs::abfs_same_atom(ucell, + orb, + lcaos, + runtime.abfs_kmesh_times, + pca_threshold); + } + else + { + abfs = Exx_Abfs::IO::construct_abfs(orb, + runtime.abfs_files, + runtime.abfs_kmesh_times); + } + Exx_Abfs::Construct_Orbs::filter_empty_orbs(abfs); + const PreorthReport preorth_report + = preorth_from_input(abfs, runtime.input, runtime.input.cal_force); + if (runtime.rank == 0) + { + runtime.log << ::ModuleRI::preorth_report(preorth_report); + } + const auto abfs_ccp = Conv_Coulomb_Pot_K::cal_orbs_ccp(abfs, make_fock_hartree_coulomb_param(), ccp_rmesh_times); + auto radials_by_type = make_sternheimer_radial_perturbations_from_orbitals(abfs_ccp); + + SternheimerFDHamiltonian::Grid potential_grid = grid; + if (runtime.input.sternheimer_molecular_coulomb == "isolated_ri") + { + if (build_coulomb_metric) + { + throw std::invalid_argument("isolated_ri cannot be combined with SIAB metric construction."); + } + // Only the perturbation is isolated; the FD Hamiltonian keeps its existing boundary condition. + potential_grid.periodic = false; + std::ofstream audit; + if (runtime.rank == 0) + { + audit.open("STERNHEIMER_COULOMB_TAIL.dat"); + if (!audit) + { + throw std::runtime_error("Cannot write the molecular Coulomb-tail audit."); + } + audit << "# type l radial_index radius_bohr endpoint_Ha charge_moment_coefficient " + "endpoint_extrapolated_coefficient\n" + << std::setprecision(17); + } + for (auto& type_radials: radials_by_type) + { + for (auto& radial: type_radials) + { + const auto& charges = abfs.at(radial.type_index).at(radial.angular_momentum); + const auto charge = std::find_if(charges.begin(), charges.end(), [&](const Numerical_Orbital_Lm& orb) { + return orb.getChi() == radial.radial_index; + }); + if (charge == charges.end()) + { + throw std::runtime_error("Cannot match the Coulomb potential to its finalized auxiliary charge."); + } + set_sternheimer_coulomb_tail_from_charge(radial, *charge); + if (runtime.rank == 0) + { + audit << radial.type_index << ' ' << radial.angular_momentum << ' ' << radial.radial_index << ' ' + << radial.radial_grid.back() << ' ' << radial.radial_values.back() << ' ' + << radial.coulomb_tail_coefficient << ' ' + << radial.radial_values.back() + * std::pow(radial.radial_grid.back(), radial.angular_momentum + 1) + << '\n'; + } + } + } + } + + std::vector atom_types; + std::vector> atom_positions; + atom_types.reserve(ucell.nat); + atom_positions.reserve(ucell.nat); + for (int it = 0; it != ucell.ntype; ++it) + { + const Atom& atom = ucell.atoms[it]; + for (int ia = 0; ia != atom.na; ++ia) + { + atom_types.push_back(it); + atom_positions.push_back(atom.tau[ia] * ucell.lat0); + } + } + + SternheimerABFBuildData result; + result.radials_by_type = std::move(radials_by_type); + result.atom_types = std::move(atom_types); + result.atom_positions = std::move(atom_positions); + result.preorth_report = preorth_report; + result.channels = build_coulomb_metric ? describe_sternheimer_abf_grid_channels(result.radials_by_type, + result.atom_types, + result.atom_positions, + max_channels) + : sample_sternheimer_abf_grid_channels(result.radials_by_type, + result.atom_types, + result.atom_positions, + potential_grid, + max_channels); + if (build_coulomb_metric) + { + if (max_channels > 0) + { + throw std::invalid_argument("A full Sternheimer Coulomb metric cannot be built for truncated channels."); + } + result.full_coulomb_metric + = build_molecular_coulomb_metric(ucell, orb, abfs_ccp, abfs, result.channels, runtime); + } + return result; +} + +struct SIABPrimitiveExportData +{ + std::vector blocks; + std::vector> reciprocal_matrix; + std::vector> overlap_s; + std::unique_ptr serial_pw_basis; + int primitive_count = 0; + int reciprocal_count = 0; +}; + +SIABPrimitiveExportData build_siab_primitive_export_data(const ModulePW::PW_Basis& response_pw_basis, + const Structure_Factor& structure_factor, + const UnitCell& ucell, + const SternheimerAbacusRuntime& runtime) +{ + siab::require_single_primitive_rcut(runtime.input.bessel_nao_rcuts); + SIABPrimitiveExportData result; + result.serial_pw_basis.reset(new ModulePW::PW_Basis_K("cpu", "double")); +#ifdef __MPI + result.serial_pw_basis->initmpi(1, 0, MPI_COMM_SELF); +#endif + result.serial_pw_basis->initgrids(ucell.lat0, + ucell.latvec, + response_pw_basis.nx, + response_pw_basis.ny, + response_pw_basis.nz); + const ModuleBase::Vector3 gamma(0.0, 0.0, 0.0); + result.serial_pw_basis->initparameters(false, runtime.input.ecutwfc, 1, &gamma); + result.serial_pw_basis->fft_bundle.initfftmode(runtime.input.fft_mode); + result.serial_pw_basis->setuptransform(); + result.serial_pw_basis->collect_local_pw( + runtime.input.erf_ecut, runtime.input.erf_height, runtime.input.erf_sigma); + if (result.serial_pw_basis->nxyz != response_pw_basis.nxyz + || result.serial_pw_basis->nrxx != response_pw_basis.nxyz || result.serial_pw_basis->nks != 1) + { + throw std::runtime_error("Sternheimer SIAB serial primitive FFT basis does not cover the complete response grid."); + } + Numerical_Basis::SIABPrimitiveInputs siab_inputs; + siab_inputs.rcuts = runtime.input.bessel_nao_rcuts; + siab_inputs.ecut_spec = runtime.input.bessel_nao_ecut; + siab_inputs.ecutwfc = runtime.input.ecutwfc; + siab_inputs.smooth = runtime.input.bessel_nao_smooth; + siab_inputs.sigma = runtime.input.bessel_nao_sigma; + siab_inputs.tolerance = runtime.input.bessel_nao_tolerence; + const Numerical_Basis::SIABPrimitiveParameters parameters + = Numerical_Basis::siab_parameters_from_input(siab_inputs, 0, runtime.input.sternheimer_siab_lmax); + Numerical_Basis numerical_basis; + const auto reciprocal_blocks = numerical_basis.siab_primitive_reciprocal_values( + 0, result.serial_pw_basis.get(), structure_factor, ucell, parameters); + if (reciprocal_blocks.empty()) + { + throw std::runtime_error("Sternheimer SIAB primitive construction returned no blocks."); + } + for (const auto& block: reciprocal_blocks) + { + siab::PrimitiveBlock output_block; + output_block.element = block.element; + output_block.atom_index = block.atom_index; + output_block.l = block.l; + output_block.m = block.m; + output_block.n_primitive = block.n_primitive; + output_block.offset = block.offset; + result.blocks.push_back(std::move(output_block)); + for (const auto& primitive: block.values) + { + if (result.reciprocal_count == 0) + { + result.reciprocal_count = static_cast(primitive.size()); + } + if (primitive.size() != static_cast(result.reciprocal_count)) + { + throw std::runtime_error("Sternheimer SIAB reciprocal primitive sizes are inconsistent."); + } + result.reciprocal_matrix.insert(result.reciprocal_matrix.end(), primitive.begin(), primitive.end()); + ++result.primitive_count; + } + } + if (result.primitive_count <= 0 || result.reciprocal_count != result.serial_pw_basis->npwk[0]) + { + throw std::runtime_error("Sternheimer SIAB reciprocal primitive matrix is empty or incomplete."); + } + if (runtime.rank == 0) + { + result.overlap_s.assign(static_cast(result.primitive_count) + * static_cast(result.primitive_count), ModuleBase::ZERO); + BlasConnector::gemm('N', + 'C', + result.primitive_count, + result.primitive_count, + result.reciprocal_count, + std::complex(1.0, 0.0), + result.reciprocal_matrix.data(), + result.reciprocal_count, + result.reciprocal_matrix.data(), + result.reciprocal_count, + std::complex(0.0, 0.0), + result.overlap_s.data(), + result.primitive_count); + } + return result; +} + +std::vector> project_siab_response_to_primitives( + const std::vector>& complete_response, + const UnitCell& ucell, + const SIABPrimitiveExportData& primitives) +{ + if (!primitives.serial_pw_basis + || complete_response.size() != static_cast(primitives.serial_pw_basis->nrxx)) + { + throw std::invalid_argument("Sternheimer SIAB response does not match its serial primitive FFT basis."); + } + std::vector> response_coefficients( + static_cast(primitives.serial_pw_basis->npwk[0]), ModuleBase::ZERO); +#ifdef _OPENMP +#pragma omp critical(sternheimer_siab_real2recip) +#endif + { + primitives.serial_pw_basis->real2recip(complete_response.data(), response_coefficients.data(), 0); + } + const double coefficient_scale = std::sqrt(ucell.omega); + for (std::complex& value: response_coefficients) + { + value *= coefficient_scale; + } + return siab::overlap_q_reciprocal_contiguous(response_coefficients, + primitives.reciprocal_matrix, + primitives.primitive_count, + primitives.reciprocal_count); +} + +std::vector build_abfs_ccp_grid_channels(const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const int max_channels, + const double pca_threshold, + const double ccp_rmesh_times, + const SternheimerAbacusRuntime& runtime) +{ + const SternheimerABFSInput input = build_abfs_ccp_input(ucell, pca_threshold, ccp_rmesh_times, runtime); + return sample_sternheimer_abf_grid_channels( + input.radials_by_type, input.atom_types, input.atom_positions, grid, max_channels); +} + +struct SternheimerPeriodicABFGridData +{ + std::vector densities; + std::vector potentials; +}; + +void broadcast_periodic_abf_channels(std::vector& channels, + const int grid_size, + const bool enabled, + const SternheimerAbacusRuntime& runtime) +{ +#ifdef __MPI + if (!enabled || runtime.nproc <= 1) + { + return; + } + + int channel_count = runtime.rank == 0 ? static_cast(channels.size()) : 0; + MPI_Bcast(&channel_count, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (channel_count < 0 || grid_size <= 0) + { + throw std::runtime_error("Invalid periodic ABFS channel broadcast dimensions."); + } + if (channel_count == 0) + { + channels.clear(); + return; + } + if (runtime.rank != 0) + { + channels.resize(static_cast(channel_count)); + } + + for (auto& channel: channels) + { + std::array metadata{}; + if (runtime.rank == 0) + { + metadata = {channel.channel_index, + channel.atom_index, + channel.atom_local_index, + channel.type_index, + channel.angular_momentum, + channel.radial_index, + channel.magnetic_index}; + } + MPI_Bcast(metadata.data(), static_cast(metadata.size()), MPI_INT, 0, MPI_COMM_WORLD); + if (runtime.rank != 0) + { + channel.channel_index = metadata[0]; + channel.atom_index = metadata[1]; + channel.atom_local_index = metadata[2]; + channel.type_index = metadata[3]; + channel.angular_momentum = metadata[4]; + channel.radial_index = metadata[5]; + channel.magnetic_index = metadata[6]; + } + + int label_size = runtime.rank == 0 ? static_cast(channel.label.size()) : 0; + MPI_Bcast(&label_size, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (label_size < 0) + { + throw std::runtime_error("Invalid periodic ABFS channel label size broadcast."); + } + if (runtime.rank != 0) + { + channel.label.assign(static_cast(label_size), '\0'); + } + if (label_size > 0) + { + MPI_Bcast(&channel.label[0], label_size, MPI_CHAR, 0, MPI_COMM_WORLD); + } + + MPI_Bcast(&channel.max_abs, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); + int value_count = runtime.rank == 0 ? static_cast(channel.potential_r.size()) : 0; + MPI_Bcast(&value_count, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (value_count != grid_size) + { + throw std::runtime_error("Periodic ABFS channel size does not match the full response grid."); + } + if (runtime.rank != 0) + { + channel.potential_r.assign(static_cast(value_count), + SternheimerRPA::Complex(0.0, 0.0)); + } + MPI_Bcast(channel.potential_r.data(), + value_count, + MPI_DOUBLE_COMPLEX, + 0, + MPI_COMM_WORLD); + } +#else + (void)channels; + (void)grid_size; + (void)enabled; +#endif +} + +void broadcast_periodic_abfs(SternheimerPeriodicABFGridData& data, + const int grid_size, + const bool enabled, + const SternheimerAbacusRuntime& runtime) +{ + broadcast_periodic_abf_channels(data.densities, grid_size, enabled, runtime); + broadcast_periodic_abf_channels(data.potentials, grid_size, enabled, runtime); +} + +SternheimerPeriodicABFGridData build_abfs_full_coulomb_bloch_grid_channels( + const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + const double gamma_inverse_k2, + const int max_channels_per_atom, + const double pca_threshold, + const SternheimerOrbitalSet* reusable_rpa_abfs, + const std::chrono::steady_clock::time_point& chi0_start_time, + const SternheimerAbacusRuntime& runtime) +{ + const SternheimerABFSInput input + = build_abfs_density_input(ucell, pca_threshold, runtime, reusable_rpa_abfs); + SternheimerPeriodicABFGridData result; + result.potentials + = limit_sternheimer_abf_channels_per_atom(sample_sternheimer_abf_bloch_grid_channels(input.radials_by_type, + input.atom_types, + input.atom_positions, + grid, + qpoint, + -1), + max_channels_per_atom); + append_chi0_progress_event("abfs_grid_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "channels=" + std::to_string(result.potentials.size()), + runtime); + if (sternheimer_grid_coulomb_diagnostic_enabled(static_cast(result.potentials.size()))) + { + result.densities = result.potentials; + } + solve_sternheimer_abf_periodic_full_coulomb_in_place(result.potentials, grid, qpoint, gamma_inverse_k2); + append_chi0_progress_event("abfs_coulomb_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "channels=" + std::to_string(result.potentials.size()), + runtime); + return result; +} + +SternheimerPeriodicABFGridData build_supercell_translation_full_response_abfs_from_input( + const SternheimerABFSInput& input, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerSupercellTranslationSum& translation_sum, + const int max_channels_per_atom) +{ + std::vector supercell_densities + = sample_sternheimer_abf_bloch_grid_channels(input.radials_by_type, + input.atom_types, + input.atom_positions, + grid, + {0.0, 0.0, 0.0}, + -1); + if (max_channels_per_atom > 0) + { + supercell_densities + = limit_sternheimer_abf_channels_per_atom(std::move(supercell_densities), max_channels_per_atom); + } + + SternheimerPeriodicABFGridData result; + result.potentials = combine_all_sternheimer_supercell_translation_channels(supercell_densities, translation_sum); + supercell_densities.clear(); + supercell_densities.shrink_to_fit(); + if (sternheimer_grid_coulomb_diagnostic_enabled(static_cast(result.potentials.size()))) + { + result.densities = result.potentials; + } + solve_sternheimer_abf_periodic_full_coulomb_in_place(result.potentials, grid, {0.0, 0.0, 0.0}, 0.0); + return result; +} + +SternheimerPeriodicABFGridData build_supercell_translation_full_response_abfs( + const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerSupercellTranslationSum& translation_sum, + const int max_channels_per_atom, + const double pca_threshold, + const SternheimerOrbitalSet* reusable_rpa_abfs, + const SternheimerAbacusRuntime& runtime) +{ + return build_supercell_translation_full_response_abfs_from_input( + build_abfs_density_input(ucell, pca_threshold, runtime, reusable_rpa_abfs), + grid, + translation_sum, + max_channels_per_atom); +} + +std::vector find_coulomb_v1_rank_files(const int iq, const int mpi_ranks) +{ + std::vector filenames; + for (int rank = 0; rank != mpi_ranks; ++rank) + { + const std::string filename = "v1_coulomb_full_iq_" + std::to_string(iq) + + "_rank" + std::to_string(rank) + ".dat"; + std::ifstream input(filename.c_str(), std::ios::binary); + if (input.good()) + { + filenames.push_back(filename); + } + } + if (filenames.empty()) + { + throw std::runtime_error("Periodic Gamma Sternheimer requires the full-Coulomb reader-v1 rank files."); + } + return filenames; +} + +std::vector atom_auxiliary_sizes(const std::vector& channels, + const int natom) +{ + std::vector sizes(static_cast(natom), 0); + for (const SternheimerABFBlochGridChannel& channel: channels) + { + if (channel.atom_index < 0 || channel.atom_index >= natom || channel.atom_local_index < 0) + { + throw std::runtime_error("Periodic Gamma Sternheimer found invalid auxiliary channel metadata."); + } + int& size = sizes[static_cast(channel.atom_index)]; + size = std::max(size, channel.atom_local_index + 1); + } + return sizes; +} + +void write_sternheimer_grid_coulomb_diagnostic( + const std::string& filename, + const std::vector& densities, + const std::vector& potentials, + const double volume_element) +{ + if (densities.size() != potentials.size()) + { + throw std::invalid_argument("Sternheimer grid Coulomb density/potential channel counts differ."); + } + std::ofstream out(filename.c_str()); + if (!out) + { + throw std::runtime_error("Failed to open Sternheimer grid Coulomb diagnostic: " + filename); + } + out << std::setprecision(16); + out << "# row col real imag\n"; + out << "naux " << densities.size() << '\n'; + for (std::size_t row = 0; row != densities.size(); ++row) + { + for (std::size_t col = 0; col != potentials.size(); ++col) + { + if (densities[row].potential_r.size() != potentials[col].potential_r.size()) + { + throw std::invalid_argument("Sternheimer grid Coulomb channel sizes differ."); + } + SternheimerRPA::Complex value(0.0, 0.0); + for (std::size_t ir = 0; ir != densities[row].potential_r.size(); ++ir) + { + value += std::conj(densities[row].potential_r[ir]) * potentials[col].potential_r[ir]; + } + value *= volume_element; + out << row << ' ' << col << ' ' << value.real() << ' ' << value.imag() << '\n'; + } + } +} + +std::vector build_lcao_candidate_grid_functions( + const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerAbacusRuntime& runtime, + const LCAO_Orbitals* provided_orbitals = nullptr, + const SternheimerReducedKPoint kpoint = {0.0, 0.0, 0.0}) +{ + LCAO_Orbitals loaded_orbitals; + if (provided_orbitals == nullptr) + { + read_sternheimer_orbitals(ucell, loaded_orbitals, runtime); + provided_orbitals = &loaded_orbitals; + } + const LCAO_Orbitals& orb = *provided_orbitals; + const int grid_size = grid.size(); + constexpr int sample_chunk_size = 8192; + + std::vector candidates; + int atom_index = 0; + for (int type = 0; type != ucell.ntype; ++type) + { + const Atom& unitcell_atom = ucell.atoms[type]; + const Numerical_Orbital& atom_orbitals = orb.Phi[type]; + Atom sampling_atom; + sampling_atom.nwl = atom_orbitals.getLmax(); + sampling_atom.l_nchi.resize(static_cast(sampling_atom.nwl + 1), 0); + for (int angular_momentum = 0; angular_momentum <= sampling_atom.nwl; ++angular_momentum) + { + sampling_atom.l_nchi[static_cast(angular_momentum)] = atom_orbitals.getNchi(angular_momentum); + sampling_atom.nw + += (2 * angular_momentum + 1) * sampling_atom.l_nchi[static_cast(angular_momentum)]; + } + sampling_atom.set_index(); + + for (int ia = 0; ia != unitcell_atom.na; ++ia, ++atom_index) + { + const int orbital_count = sampling_atom.nw; + const std::size_t candidate_begin = candidates.size(); + candidates.resize(candidate_begin + static_cast(orbital_count)); + for (int iw = 0; iw != orbital_count; ++iw) + { + SternheimerDeltaGridFunction& candidate = candidates[candidate_begin + static_cast(iw)]; + candidate.values.assign(static_cast(grid_size), + SternheimerFDHamiltonian::Complex(0.0, 0.0)); + for (SternheimerFDHamiltonian::Vector& gradient: candidate.gradients) + { + gradient.assign(static_cast(grid_size), SternheimerFDHamiltonian::Complex(0.0, 0.0)); + } + } + + ModuleGint::GintAtom sampler(&sampling_atom, + type, + ia, + atom_index, + ModuleGint::Vec3i(0, 0, 0), + ModuleGint::Vec3i(0, 0, 0), + ModuleGint::Vec3d(0.0, 0.0, 0.0), + &orb.Phi[type], + &ucell); + const ModuleBase::Vector3 atom_position = unitcell_atom.tau[ia] * ucell.lat0; + const std::vector> periodic_images + = enumerate_delta_sternheimer_periodic_images(grid, + {atom_position.x, atom_position.y, atom_position.z}, + atom_orbitals.getRcut()); + for (int first = 0; first < grid_size; first += sample_chunk_size) + { + const int chunk_size = std::min(sample_chunk_size, grid_size - first); + std::vector coordinates(static_cast(chunk_size)); + const std::size_t buffer_size + = static_cast(chunk_size) * static_cast(orbital_count); + std::vector values(buffer_size, 0.0); + std::array, 3> gradients{{ + std::vector(buffer_size, 0.0), + std::vector(buffer_size, 0.0), + std::vector(buffer_size, 0.0), + }}; + for (const std::array& image: periodic_images) + { + for (int local = 0; local != chunk_size; ++local) + { + const int linear = first + local; + const int ix = linear / (grid.ny * grid.nz); + const int remainder = linear % (grid.ny * grid.nz); + const int iy = remainder / grid.nz; + const int iz = remainder % grid.nz; + const std::array position + = sternheimer_fd_grid_cartesian_position(grid, ix, iy, iz); + const std::array translation + = sternheimer_fd_grid_lattice_translation(grid, image); + coordinates[static_cast(local)] + = ModuleGint::Vec3d(position[0] - atom_position.x - translation[0], + position[1] - atom_position.y - translation[1], + position[2] - atom_position.z - translation[2]); + } + sampler.set_phi_dphi(coordinates, + orbital_count, + values.data(), + gradients[0].data(), + gradients[1].data(), + gradients[2].data()); + accumulate_delta_sternheimer_bloch_samples(values, + gradients, + chunk_size, + orbital_count, + static_cast(first), + candidate_begin, + kpoint, + image, + candidates); + } + } + } + } + return candidates; +} + +std::vector build_lcao_grid_functions_from_coefficients( + const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const LCAO_Orbitals& orb, + const SternheimerReducedKPoint& kpoint, + const std::vector>& coefficients) +{ + if (coefficients.empty()) + { + return {}; + } + const int grid_size = grid.size(); + constexpr int sample_chunk_size = 8192; + std::vector functions(coefficients.size()); + for (SternheimerDeltaGridFunction& function: functions) + { + function.values.assign(static_cast(grid_size), + SternheimerFDHamiltonian::Complex(0.0, 0.0)); + for (SternheimerFDHamiltonian::Vector& gradient: function.gradients) + { + gradient.assign(static_cast(grid_size), + SternheimerFDHamiltonian::Complex(0.0, 0.0)); + } + } + + std::size_t ao_begin = 0; + int atom_index = 0; + for (int type = 0; type != ucell.ntype; ++type) + { + const Atom& unitcell_atom = ucell.atoms[type]; + const Numerical_Orbital& atom_orbitals = orb.Phi[type]; + Atom sampling_atom; + sampling_atom.nwl = atom_orbitals.getLmax(); + sampling_atom.l_nchi.resize(static_cast(sampling_atom.nwl + 1), 0); + for (int angular_momentum = 0; angular_momentum <= sampling_atom.nwl; ++angular_momentum) + { + sampling_atom.l_nchi[static_cast(angular_momentum)] + = atom_orbitals.getNchi(angular_momentum); + sampling_atom.nw += (2 * angular_momentum + 1) + * sampling_atom.l_nchi[static_cast(angular_momentum)]; + } + sampling_atom.set_index(); + + for (int ia = 0; ia != unitcell_atom.na; ++ia, ++atom_index) + { + const int orbital_count = sampling_atom.nw; + for (const auto& state_coefficients: coefficients) + { + if (ao_begin > state_coefficients.size() + || static_cast(orbital_count) > state_coefficients.size() - ao_begin) + { + throw std::runtime_error( + "Sternheimer direct LCAO sampling coefficient basis is smaller than the sampled AO basis."); + } + } + + ModuleGint::GintAtom sampler(&sampling_atom, + type, + ia, + atom_index, + ModuleGint::Vec3i(0, 0, 0), + ModuleGint::Vec3i(0, 0, 0), + ModuleGint::Vec3d(0.0, 0.0, 0.0), + &orb.Phi[type], + &ucell); + const ModuleBase::Vector3 atom_position = unitcell_atom.tau[ia] * ucell.lat0; + const std::vector> periodic_images + = enumerate_delta_sternheimer_periodic_images(grid, + {atom_position.x, + atom_position.y, + atom_position.z}, + atom_orbitals.getRcut()); + for (int first = 0; first < grid_size; first += sample_chunk_size) + { + const int chunk_size = std::min(sample_chunk_size, grid_size - first); + std::vector coordinates(static_cast(chunk_size)); + const std::size_t buffer_size + = static_cast(chunk_size) * static_cast(orbital_count); + std::vector values(buffer_size, 0.0); + std::array, 3> gradients{{ + std::vector(buffer_size, 0.0), + std::vector(buffer_size, 0.0), + std::vector(buffer_size, 0.0), + }}; + for (const std::array& image: periodic_images) + { + for (int local = 0; local != chunk_size; ++local) + { + const int linear = first + local; + const int ix = linear / (grid.ny * grid.nz); + const int remainder = linear % (grid.ny * grid.nz); + const int iy = remainder / grid.nz; + const int iz = remainder % grid.nz; + const std::array position + = sternheimer_fd_grid_cartesian_position(grid, ix, iy, iz); + const std::array translation + = sternheimer_fd_grid_lattice_translation(grid, image); + coordinates[static_cast(local)] + = ModuleGint::Vec3d(position[0] - atom_position.x - translation[0], + position[1] - atom_position.y - translation[1], + position[2] - atom_position.z - translation[2]); + } + sampler.set_phi_dphi(coordinates, + orbital_count, + values.data(), + gradients[0].data(), + gradients[1].data(), + gradients[2].data()); + accumulate_delta_sternheimer_lcao_state_samples(values, + gradients, + chunk_size, + orbital_count, + static_cast(first), + ao_begin, + coefficients, + kpoint, + image, + functions); + } + } + ao_begin += static_cast(orbital_count); + } + } + for (const auto& state_coefficients: coefficients) + { + if (state_coefficients.size() != ao_begin) + { + throw std::runtime_error( + "Sternheimer direct LCAO sampling coefficient basis exceeds the sampled AO basis."); + } + } + return functions; +} + +struct SternheimerSampledLCAOKPoint +{ + SternheimerFDZeroOrderStates states; + SternheimerFDZeroOrderStates unoccupied_states; + std::vector occupied_functions; + std::vector unoccupied_functions; + std::vector occupied_projector_functions; + double occupied_raw_norm_min = std::numeric_limits::infinity(); + double occupied_raw_norm_max = 0.0; + double unoccupied_raw_norm_min = std::numeric_limits::infinity(); + double unoccupied_raw_norm_max = 0.0; + std::vector occupied_raw_norms; + std::vector unoccupied_raw_norms; +}; + +SternheimerSampledLCAOKPoint sample_sternheimer_lcao_kpoint( + const UnitCell& ucell, + const SternheimerFDHamiltonian::Grid& grid, + const LCAO_Orbitals& orbitals, + const SternheimerLCAOOccupiedKPoint& record, + const int requested_occupied_states, + const bool include_unoccupied, + const int requested_unoccupied_states, + const double volume_element, + const double norm_tolerance) +{ + SternheimerSampledLCAOKPoint sampled; + const std::size_t occupied_count + = sternheimer_sampled_occupied_count(record.coefficients.size(), requested_occupied_states); + const std::size_t sampled_unoccupied_count + = sternheimer_sampled_unoccupied_count(include_unoccupied, + record.unoccupied_coefficients.size(), + requested_unoccupied_states); + std::vector> all_coefficients; + all_coefficients.reserve(occupied_count + sampled_unoccupied_count); + all_coefficients.insert(all_coefficients.end(), + record.coefficients.begin(), + record.coefficients.begin() + static_cast(occupied_count)); + if (sampled_unoccupied_count > 0) + { + all_coefficients.insert(all_coefficients.end(), + record.unoccupied_coefficients.begin(), + record.unoccupied_coefficients.begin() + + static_cast(sampled_unoccupied_count)); + } + std::vector all_functions + = build_lcao_grid_functions_from_coefficients( + ucell, grid, orbitals, sternheimer_lcao_grid_kpoint(record), all_coefficients); + + if (all_functions.size() != occupied_count + sampled_unoccupied_count) + { + throw std::runtime_error("Sternheimer periodic direct LCAO sampling lost selected KS states."); + } + sampled.states.eigenvalues.reserve(occupied_count); + sampled.states.wavefunctions.reserve(occupied_count); + sampled.states.residual_norms.reserve(occupied_count); + sampled.occupied_functions.reserve(occupied_count); + for (std::size_t ib = 0; ib != occupied_count; ++ib) + { + SternheimerDeltaGridFunction occupied_function = std::move(all_functions[ib]); + const double norm = sternheimer_fd_grid_norm(occupied_function.values, volume_element); + if (norm <= norm_tolerance) + { + throw std::runtime_error("Sternheimer periodic sampled occupied function has zero norm."); + } + sampled.occupied_raw_norm_min = std::min(sampled.occupied_raw_norm_min, norm); + sampled.occupied_raw_norm_max = std::max(sampled.occupied_raw_norm_max, norm); + sampled.occupied_raw_norms.push_back(norm); + const SternheimerFDHamiltonian::Complex inverse_norm(1.0 / norm, 0.0); + for (auto& value: occupied_function.values) + { + value *= inverse_norm; + } + for (auto& gradient: occupied_function.gradients) + { + for (auto& value: gradient) + { + value *= inverse_norm; + } + } + sampled.states.eigenvalues.push_back(record.eigenvalues[ib]); + sampled.states.wavefunctions.push_back(occupied_function.values); + sampled.states.residual_norms.push_back(0.0); + sampled.occupied_functions.push_back(std::move(occupied_function)); + } + sampled.unoccupied_states.eigenvalues.reserve(sampled_unoccupied_count); + sampled.unoccupied_functions.reserve(sampled_unoccupied_count); + for (std::size_t ib = 0; ib != sampled_unoccupied_count; ++ib) + { + SternheimerDeltaGridFunction unoccupied_function + = std::move(all_functions[occupied_count + ib]); + const double norm = sternheimer_fd_grid_norm(unoccupied_function.values, volume_element); + if (norm <= norm_tolerance) + { + throw std::runtime_error("Sternheimer periodic sampled unoccupied function has zero norm."); + } + sampled.unoccupied_raw_norm_min = std::min(sampled.unoccupied_raw_norm_min, norm); + sampled.unoccupied_raw_norm_max = std::max(sampled.unoccupied_raw_norm_max, norm); + sampled.unoccupied_raw_norms.push_back(norm); + const SternheimerFDHamiltonian::Complex inverse_norm(1.0 / norm, 0.0); + for (auto& value: unoccupied_function.values) + { + value *= inverse_norm; + } + for (auto& gradient: unoccupied_function.gradients) + { + for (auto& value: gradient) + { + value *= inverse_norm; + } + } + sampled.unoccupied_states.eigenvalues.push_back(record.unoccupied_eigenvalues[ib]); + sampled.unoccupied_functions.push_back(std::move(unoccupied_function)); + } + sampled.occupied_projector_functions = orthonormalize_delta_sternheimer_grid_functions( + sampled.occupied_functions, volume_element, norm_tolerance); + if (sampled.occupied_projector_functions.size() != occupied_count) + { + throw std::runtime_error("Sternheimer periodic occupied projector lost a linearly dependent KS state."); + } + return sampled; +} + +void run_sternheimer_spectrum_audit( + const UnitCell& ucell, + const LCAO_Orbitals& orbitals, + const SternheimerABACUSFDGridData& grid_data, + const std::vector& local_potential, + const SternheimerLCAOOccupiedKPoint& source_record, + const SternheimerLCAOOccupiedKPoint& target_record, + const int source_index, + const int target_index, + const SternheimerAbacusRuntime& runtime) +{ + auto target = sample_sternheimer_lcao_kpoint( + ucell, grid_data.grid, orbitals, target_record, 0, true, 0, + grid_data.volume_element, runtime.input.sternheimer_vnorm_tol); + if (target.unoccupied_functions.empty() || source_record.eigenvalues.empty()) + { + throw std::runtime_error("Spectrum audit requires the complete occupied and virtual KS records."); + } + auto target_grid = grid_data; + target_grid.grid.kpoint = sternheimer_lcao_grid_kpoint(target_record); + auto projectors = make_sternheimer_fd_nonlocal_projector_from_unitcell( + ucell, target_grid.grid, target_grid.volume_element); + const auto hamiltonian = make_sternheimer_fd_hamiltonian_from_local_potential( + target_grid, local_potential, 1.0, std::move(projectors), runtime.input.sternheimer_fd_order); + std::ofstream audit("STERNHEIMER_SPECTRUM_AUDIT.dat"); + if (!audit) + { + throw std::runtime_error("Cannot open spectrum audit output."); + } + audit << std::setprecision(17) + << "status diagnostic_only\n" + << "physical_result no\n" + << "response_equations 0\n" + << "grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' + << grid_data.grid.nz << '\n' + << "source_target " << source_index + 1 << ' ' << target_index + 1 << '\n' + << "occupied_raw_norm_range " << target.occupied_raw_norm_min << ' ' + << target.occupied_raw_norm_max << '\n' + << "virtual_raw_norm_range " << target.unoccupied_raw_norm_min << ' ' + << target.unoccupied_raw_norm_max << '\n'; + std::vector eigenvalues = target_record.eigenvalues; + eigenvalues.insert(eigenvalues.end(), target_record.unoccupied_eigenvalues.begin(), + target_record.unoccupied_eigenvalues.end()); + const std::size_t occupied_count = target.occupied_functions.size(); + std::vector raw_norms = target.occupied_raw_norms; + raw_norms.insert(raw_norms.end(), target.unoccupied_raw_norms.begin(), target.unoccupied_raw_norms.end()); + auto functions = std::move(target.occupied_functions); + functions.insert(functions.end(), std::make_move_iterator(target.unoccupied_functions.begin()), + std::make_move_iterator(target.unoccupied_functions.end())); + target.unoccupied_functions.clear(); + { + const auto matrices = assemble_delta_sternheimer_grid_matrices_fast( + hamiltonian, functions, grid_data.volume_element); + audit << "# state pbe_Ry overlap kinetic_Ry local_Ry nonlocal_Ry expectation_Ry raw_norm\n"; + for (std::size_t state = 0; state < functions.size(); ++state) + { + const std::size_t index = state * (functions.size() + 1); + const double norm2 = matrices.overlap[index].real(); + audit << "state " << state + 1 << ' ' << eigenvalues.at(state) << ' ' + << norm2 << ' ' << matrices.kinetic[index].real() / norm2 << ' ' + << matrices.local_potential[index].real() / norm2 << ' ' + << matrices.nonlocal[index].real() / norm2 << ' ' + << matrices.hamiltonian[index].real() / norm2 << ' ' << raw_norms.at(state) << '\n'; + } + std::ofstream matrix_out("STERNHEIMER_SPECTRUM_MATRICES.dat"); + if (!matrix_out) + { + throw std::runtime_error("Cannot open spectrum matrices output."); + } + matrix_out << std::setprecision(17) << "# row col S T Vloc Vnl H (real imag), column major\n"; + for (std::size_t column = 0; column < functions.size(); ++column) + { + for (std::size_t row = 0; row < functions.size(); ++row) + { + const auto index = row + column * functions.size(); + matrix_out << row + 1 << ' ' << column + 1; + for (const auto* matrix : {&matrices.overlap, &matrices.kinetic, + &matrices.local_potential, &matrices.nonlocal, + &matrices.hamiltonian}) + { + matrix_out << ' ' << (*matrix)[index].real() << ' ' << (*matrix)[index].imag(); + } + matrix_out << '\n'; + } + } + matrix_out.close(); + if (!matrix_out) + { + throw std::runtime_error("Spectrum matrix output did not complete."); + } + } + audit.flush(); + if (!audit) + { + throw std::runtime_error("Spectrum state output did not complete."); + } + functions.erase(functions.begin(), functions.begin() + occupied_count); + SternheimerDeltaSubspaceOptions options; + options.max_virtual_states = 0; + options.norm_tolerance = runtime.input.sternheimer_vnorm_tol; + options.retain_grid_functions = false; + options.evaluate_full_grid_difference = false; + const auto subspace = build_reference_delta_sternheimer_subspace( + hamiltonian, target.occupied_projector_functions, std::move(functions), + grid_data.volume_element, options); + const double source_vbm = *std::max_element(source_record.eigenvalues.begin(), + source_record.eigenvalues.end()); + const double pbe_cbm = *std::min_element(target_record.unoccupied_eigenvalues.begin(), + target_record.unoccupied_eigenvalues.end()); + audit << "source_vbm_Ry " << source_vbm << '\n' + << "target_cbm_Ry " << pbe_cbm << '\n' + << "pbe_transition_min_Ry " << pbe_cbm - source_vbm << '\n'; + double delta_min = std::numeric_limits::infinity(); + for (std::size_t state = 0; state < subspace.virtual_states.size(); ++state) + { + const double energy = subspace.virtual_states[state].eigenvalue; + delta_min = std::min(delta_min, energy); + audit << "delta_state " << state + 1 << ' ' << energy << '\n'; + } + audit << "delta_transition_min_Ry " << delta_min - source_vbm << '\n' + << "audit_complete yes\n"; + audit.close(); + if (!audit) + { + throw std::runtime_error("Spectrum audit output did not complete."); + } +} + +void run_sternheimer_galerkin_spectrum_audit( + const UnitCell& ucell, + const LCAO_Orbitals& orbitals, + SternheimerABACUSFDGridData fine_grid, + SternheimerABACUSFDGridData response_grid, + const std::vector& fine_potential, + const SternheimerLCAOOccupiedKPoint& source_record, + const SternheimerLCAOOccupiedKPoint& target_record, + const int source_index, + const int target_index, + const SternheimerAbacusRuntime& runtime) +{ + fine_grid.grid.kpoint = sternheimer_lcao_grid_kpoint(target_record); + response_grid.grid.kpoint = fine_grid.grid.kpoint; + auto target = sample_sternheimer_lcao_kpoint( + ucell, fine_grid.grid, orbitals, target_record, 0, true, 0, + fine_grid.volume_element, runtime.input.sternheimer_vnorm_tol); + if (target.unoccupied_functions.empty() || source_record.eigenvalues.empty()) + { + throw std::runtime_error("Galerkin spectrum audit requires complete occupied and virtual KS records."); + } + const auto occupied_count = target.occupied_functions.size(); + auto raw_norms = target.occupied_raw_norms; + raw_norms.insert(raw_norms.end(), target.unoccupied_raw_norms.begin(), target.unoccupied_raw_norms.end()); + auto functions = std::move(target.occupied_functions); + functions.insert(functions.end(), std::make_move_iterator(target.unoccupied_functions.begin()), + std::make_move_iterator(target.unoccupied_functions.end())); + target = SternheimerSampledLCAOKPoint(); + std::vector fine_states; + for (auto& function : functions) + { + // This diagnostic compares FullGrid FD8 forms, not the old weak-A block. + for (auto& gradient : function.gradients) + { + SternheimerFDHamiltonian::Vector().swap(gradient); + } + fine_states.push_back(&function.values); + } + auto nonlocal = make_sternheimer_fd_nonlocal_projector_from_unitcell( + ucell, fine_grid.grid, fine_grid.volume_element); + auto fine_hamiltonian = std::make_shared( + make_sternheimer_fd_hamiltonian_from_local_potential( + fine_grid, fine_potential, 1.0, std::move(nonlocal), runtime.input.sternheimer_fd_order)); + trim_sternheimer_process_heap(); + std::ofstream audit("STERNHEIMER_GALERKIN_AUDIT.dat"); + if (!audit) + { + throw std::runtime_error("Cannot open Galerkin spectrum audit."); + } + audit << std::setprecision(17) + << "status diagnostic_only\nphysical_result no\nresponse_equations 0\n" + << "reference FullGrid_FD8_not_weak_A\nfull_pc_positivity_certified no\n" + << "fine_grid " << fine_grid.grid.nx << ' ' << fine_grid.grid.ny << ' ' << fine_grid.grid.nz << '\n' + << "coarse_grid " << response_grid.grid.nx << ' ' << response_grid.grid.ny << ' ' + << response_grid.grid.nz << '\n' + << "source_target " << source_index + 1 << ' ' << target_index + 1 << '\n' + << "nocc " << occupied_count << "\nnvirtual " << functions.size() - occupied_count << '\n' + << "source_vbm_Ry " << *std::max_element(source_record.eigenvalues.begin(), source_record.eigenvalues.end()) + << '\n'; + std::vector pbe_eigenvalues = target_record.eigenvalues; + pbe_eigenvalues.insert(pbe_eigenvalues.end(), target_record.unoccupied_eigenvalues.begin(), + target_record.unoccupied_eigenvalues.end()); + audit << "# state case index pbe_Ry fine_norm2 projected_norm2 reconstruction_error raw_sampled_norm\n"; + constexpr std::size_t audit_workspace_budget = 32ULL * 1024 * 1024 * 1024; + for (const bool identity : {true, false}) + { + const std::string label = identity ? "fine" : "projected"; + const auto matrices = audit_sternheimer_galerkin_matrices( + fine_hamiltonian, fine_states, identity ? fine_grid.grid : response_grid.grid, + audit_workspace_budget); + const auto n = matrices.dimension; + if (n != functions.size()) + { + throw std::runtime_error("Galerkin audit changed the full KS state count."); + } + std::ofstream output("STERNHEIMER_GALERKIN_" + label + "_MATRICES.dat"); + if (!output) + { + throw std::runtime_error("Cannot open Galerkin matrix output."); + } + output << std::setprecision(17) << "# row col S T Vloc Vnl H (real imag), column major\n"; + for (std::size_t column = 0; column != n; ++column) + { + for (std::size_t row = 0; row != n; ++row) + { + const auto index = row + column * n; + output << row + 1 << ' ' << column + 1; + for (const auto* matrix : {&matrices.overlap, &matrices.kinetic, &matrices.local_potential, + &matrices.nonlocal, &matrices.hamiltonian}) + { + output << ' ' << matrix->at(index).real() << ' ' << matrix->at(index).imag(); + } + output << '\n'; + } + } + output.close(); + if (!output) + { + throw std::runtime_error("Galerkin matrix output did not complete."); + } + audit << "case " << label << " workspace_bytes " << matrices.workspace_bytes + << " filtered_identity_max_abs_error " << matrices.filtered_matrix_identity_max_abs_error << '\n'; + for (std::size_t state = 0; state != n; ++state) + { + audit << "state " << label << ' ' << state + 1 << ' ' << pbe_eigenvalues.at(state) << ' ' + << matrices.fine_norm_squared.at(state) << ' ' << matrices.projected_norm_squared.at(state) + << ' ' << matrices.reconstruction_relative_error.at(state) << ' ' << raw_norms.at(state) << '\n'; + } + audit.flush(); + if (!audit) + { + throw std::runtime_error("Galerkin state output did not complete."); + } + } + audit << "audit_complete yes\n"; + audit.close(); + if (!audit) + { + throw std::runtime_error("Galerkin audit output did not complete."); + } +} + +void run_sternheimer_weak_q_unit( + const UnitCell& ucell, + const LCAO_Orbitals& orbitals, + SternheimerABACUSFDGridData fine_grid, + SternheimerABACUSFDGridData coarse_grid, + const std::vector& live_fine_potential, + const std::vector& live_records, + const SternheimerPeriodicResponsePlan& response_plan, + const SternheimerRPA::FrequencyGrid& frequency_grid, + const double pca_threshold, + const SternheimerOrbitalSet* rpa_abfs, + const SternheimerAbacusRuntime& runtime) +{ + using Vector = SternheimerFDHamiltonian::Vector; + using Complex = SternheimerFDHamiltonian::Complex; + using Blocks = SternheimerWeakAugmented; + auto fine_potential = live_fine_potential; + auto records = live_records; + const auto started = std::chrono::steady_clock::now(); + const bool coulomb_only = env_is_true("ABACUS_STERNHEIMER_WEAK_Q_COULOMB_ONLY"); + const bool full_coulomb_matrix = env_is_true("WEAK_Q_FULL_COULOMB_MATRIX"); + const bool gamma_finite_part + = response_plan.qpoint[0] == 0.0 && response_plan.qpoint[1] == 0.0 + && response_plan.qpoint[2] == 0.0; + if (full_coulomb_matrix && !coulomb_only) + throw std::invalid_argument("Full Coulomb matrix audit requires Coulomb-only mode; response unchanged."); + const int nfreq = static_cast(frequency_grid.omega_ha.size()); + if (nfreq <= 0 || frequency_grid.weights_ha.size() != frequency_grid.omega_ha.size() + || response_plan.kq_pairs.size() != records.size() + || response_plan.record_index_by_global_k.size() != records.size() + || fine_potential.size() != static_cast(fine_grid.grid.size()) + || !std::isfinite(fine_grid.volume_element) || fine_grid.volume_element <= 0 + || runtime.input.exx_ewald_dimension != 2) + throw std::invalid_argument("Weak q unit requires full k/frequency/reference data and exact 2D Coulomb."); + const auto finite_complex = [](Complex value) { + return std::isfinite(value.real()) && std::isfinite(value.imag()); + }; + const auto hash_reals = [](siab::Sha256& digest, const std::vector& values) { + hash_u64(digest, values.size()); + for (double value : values) + { + if (!std::isfinite(value)) throw std::invalid_argument("Weak q unit nonfinite reference data."); + hash_double(digest, value); + } + }; + // Separate process shards replay this exact fine potential AND full KS set. + // Seal physical inputs, not live SCF roundoff; never round reference hashes. + const auto input = build_abfs_density_input(ucell, pca_threshold, runtime, rpa_abfs); + siab::Sha256 contract_digest; + const auto contract_text = [&](const std::string& text) { + hash_u64(contract_digest, text.size()); + contract_digest.update(reinterpret_cast(text.data()), text.size()); + }; + if (std::getenv(kOrbitalDirEnv) || std::getenv(kOrbitalFilesEnv)) + throw std::invalid_argument("Weak q sealed references require the actual SCF orbitals, without orbital overrides."); + for (const auto& filename : {runtime.input.input_file, runtime.input.stru_file, runtime.input.kpoint_file}) + contract_text(siab::sha256_file(filename)); + contract_text(siab::sha256_file_manifest(siab::resolve_required_input_files( + orbital_dir_from_env_or_input(runtime), orbital_files_from_env_or_cell(ucell), "initial orbital"))); + contract_text(siab::sha256_file_manifest(siab::resolve_required_input_files( + runtime.input.pseudo_dir, ucell.pseudo_fn, "pseudopotential"))); + hash_reals(contract_digest, cell_vectors_bohr(ucell)); + for (std::size_t atom = 0; atom < input.atom_types.size(); ++atom) + { + hash_int(contract_digest, input.atom_types[atom]); + for (double value : {input.atom_positions.at(atom).x, input.atom_positions.at(atom).y, + input.atom_positions.at(atom).z}) + hash_double(contract_digest, value); + } + for (int value : {fine_grid.grid.nx, fine_grid.grid.ny, fine_grid.grid.nz, + coarse_grid.grid.nx, coarse_grid.grid.ny, coarse_grid.grid.nz, + runtime.nlocal, runtime.input.sternheimer_fd_order, response_plan.iq}) + hash_int(contract_digest, value); + hash_double(contract_digest, fine_grid.volume_element); + hash_double(contract_digest, pca_threshold); + hash_reals(contract_digest, frequency_grid.omega_ha); + hash_reals(contract_digest, frequency_grid.weights_ha); + for (const auto& record : records) + { + hash_int(contract_digest, record.global_k_index); + hash_int(contract_digest, record.spin_index); + for (double v : record.kpoint) hash_double(contract_digest, v); + hash_double(contract_digest, record.kweight); + hash_reals(contract_digest, record.occupations); + hash_u64(contract_digest, record.unoccupied_eigenvalues.size()); + } + for (const auto& radials : input.radials_by_type) + for (const auto& radial : radials) + { + hash_int(contract_digest, radial.type_index); + hash_int(contract_digest, radial.angular_momentum); + hash_int(contract_digest, radial.radial_index); + hash_reals(contract_digest, radial.radial_grid); + hash_reals(contract_digest, radial.radial_values); + } + const auto reference_contract = contract_digest.finish(); + const SternheimerWeakQReferenceDimensions reference_dimensions{ + static_cast(fine_grid.grid.size()), records.size(), + static_cast(runtime.nlocal)}; + const char* reference_import = std::getenv("WEAK_Q_REFERENCE_IMPORT"); + const char* reference_export = std::getenv("WEAK_Q_REFERENCE_EXPORT"); + if (reference_import && reference_export) + throw std::invalid_argument("Weak q reference import and export are mutually exclusive."); + if (reference_import) + { + auto sealed = read_sternheimer_weak_q_reference(reference_import, reference_contract, reference_dimensions); + fine_potential.swap(sealed.fine_potential); + records.swap(sealed.full_records); + } + if (reference_export) + write_sternheimer_weak_q_reference(reference_export, reference_contract, reference_dimensions, + fine_potential, records); + std::vector occupied_counts(records.size()); + siab::Sha256 reference_digest; + hash_reals(reference_digest, cell_vectors_bohr(ucell)); + hash_reals(reference_digest, fine_potential); + std::vector coefficient_hashes(records.size()); + double vbm = -std::numeric_limits::infinity(); + double cbm = std::numeric_limits::infinity(); + for (std::size_t k = 0; k < records.size(); ++k) + { + const auto& record = records.at(response_plan.record_index_by_global_k.at(k)); + const auto nocc = record.eigenvalues.size(); + if (record.global_k_index != static_cast(k) || record.spin_index != 0 + || record.has_grid_kpoint_override || nocc == 0 || record.unoccupied_eigenvalues.empty() + || nocc + record.unoccupied_eigenvalues.size() != static_cast(runtime.nlocal) + || record.coefficients.size() != nocc || record.occupations.size() != nocc + || record.unoccupied_coefficients.size() != record.unoccupied_eigenvalues.size()) + throw std::invalid_argument("Weak q unit requires nbands=nlocal and all full-k occupied/virtual KS records."); + occupied_counts[k] = static_cast(nocc); + hash_int(reference_digest, record.global_k_index); + hash_double(reference_digest, record.kweight); + for (double value : record.kpoint) + { + if (!std::isfinite(value)) throw std::invalid_argument("Weak q unit nonfinite k point."); + hash_double(reference_digest, value); + } + hash_reals(reference_digest, record.eigenvalues); + hash_reals(reference_digest, record.unoccupied_eigenvalues); + hash_reals(reference_digest, record.occupations); + for (std::size_t ib = 0; ib < nocc; ++ib) + { + sternheimer_weak_q_weight(record.kweight, record.occupations[ib]); + vbm = std::max(vbm, record.eigenvalues[ib]); + } + for (double value : record.unoccupied_eigenvalues) cbm = std::min(cbm, value); + siab::Sha256 coefficient_digest; + for (const auto* coefficients : {&record.coefficients, &record.unoccupied_coefficients}) + for (const auto& state : *coefficients) + { + if (state.size() != static_cast(runtime.nlocal)) + throw std::invalid_argument("Weak q unit incomplete KS coefficient vector."); + for (Complex value : state) + { + if (!finite_complex(value)) throw std::invalid_argument("Weak q unit nonfinite KS coefficient."); + hash_double(coefficient_digest, value.real()); + hash_double(coefficient_digest, value.imag()); + hash_double(reference_digest, value.real()); + hash_double(reference_digest, value.imag()); + } + } + coefficient_hashes[k] = coefficient_digest.finish(); + } + if (!(cbm - vbm > 1e-10) || std::abs(response_plan.kweight_sum - 2.0) > 1e-10) + throw std::invalid_argument("Weak q unit requires a positive full-k PBE gap and spin-degenerate k weights summing to two."); + for (int f = 0; f < nfreq; ++f) + { + sternheimer_weak_q_omega_ry(frequency_grid.omega_ha[f]); + if (!std::isfinite(frequency_grid.weights_ha[f]) || frequency_grid.weights_ha[f] <= 0) + throw std::invalid_argument("Weak q unit requires finite positive frequency quadrature weights."); + } + const auto metadata = describe_sternheimer_abf_grid_channels( + input.radials_by_type, input.atom_types, input.atom_positions, -1); + if (metadata.empty() || metadata.size() > static_cast(std::numeric_limits::max())) + throw std::length_error("Weak q unit invalid auxiliary dimension."); + const int channels = static_cast(metadata.size()); + SternheimerWeakQUnit::Raw raw; + raw.source_k = std::getenv("WEAK_Q_SOURCE_K"); + raw.band_begin = std::getenv("WEAK_Q_BAND_BEGIN"); + raw.band_end = std::getenv("WEAK_Q_BAND_END"); + raw.frequency_begin = std::getenv("WEAK_Q_FREQ_BEGIN"); + raw.frequency_end = std::getenv("WEAK_Q_FREQ_END"); + raw.shard_index = std::getenv("WEAK_Q_SHARD_INDEX"); + raw.shard_count = std::getenv("WEAK_Q_SHARD_COUNT"); + const auto unit = SternheimerWeakQUnit::parse(raw, occupied_counts, nfreq, channels); + const bool parallel_benchmark = env_is_true("WEAK_Q_PARALLEL_BENCHMARK"); + const bool use_weak_q_spacegroup = env_is_true("WEAK_Q_USE_SPACEGROUP_SYMMETRY"); + if (use_weak_q_spacegroup && parallel_benchmark) + throw std::invalid_argument("Weak-q space-group reduction is incompatible with the parallel benchmark."); + std::vector weak_symmetry_routes; + SternheimerWeakQSymmetrySource weak_symmetry_source; + int weak_discrete_spatial_order = 1; + int weak_fixed_q_little_group_order = 1; + if (use_weak_q_spacegroup) + { + if (runtime.input.symmetry != "1") + throw std::invalid_argument("Weak-q space-group reduction requires PBE symmetry=1."); + std::vector reduced_rotations; + reduced_rotations.reserve(static_cast(ucell.symm.nrotk)); + for (int isym = 0; isym != ucell.symm.nrotk; ++isym) + { + const auto& rotation = ucell.symm.gmatrix[isym]; + reduced_rotations.push_back({{{rotation.e11, rotation.e12, rotation.e13}, + {rotation.e21, rotation.e22, rotation.e23}, + {rotation.e31, rotation.e32, rotation.e33}}}); + } + const auto fine_grid_operations + = sternheimer_fd_stencil_symmetry_indices(fine_grid.grid, + runtime.input.sternheimer_fd_order, + reduced_rotations); + const auto coarse_grid_operations + = sternheimer_fd_stencil_symmetry_indices(coarse_grid.grid, + runtime.input.sternheimer_fd_order, + reduced_rotations); + std::vector discrete_spatial_operations; + std::copy_if(fine_grid_operations.begin(), + fine_grid_operations.end(), + std::back_inserter(discrete_spatial_operations), + [&](const int isym) { + return std::find(coarse_grid_operations.begin(), + coarse_grid_operations.end(), + isym) + != coarse_grid_operations.end(); + }); + weak_discrete_spatial_order = static_cast(discrete_spatial_operations.size()); + const auto permutations = build_sternheimer_fixed_q_little_group_permutations( + ucell, records, response_plan.qpoint, discrete_spatial_operations); + weak_fixed_q_little_group_order = static_cast(permutations.size()); + std::vector> index_permutations; + index_permutations.reserve(permutations.size()); + for (const auto& permutation : permutations) + index_permutations.push_back(permutation.mapped_index_by_full_k); + const auto orbits = build_sternheimer_fixed_q_k_orbits_from_permutations( + static_cast(records.size()), index_permutations); + for (const auto& orbit : orbits) + { + for (const int member_ik_full : orbit.members) + { + const auto route = std::find_if( + permutations.begin(), permutations.end(), [&](const auto& operation) { + return operation.mapped_index_by_full_k[ + static_cast(member_ik_full)] + == orbit.representative_ik_full; + }); + if (route == permutations.end()) + throw std::runtime_error( + "A weak-q space-group orbit member has no inverse route to its representative."); + weak_symmetry_routes.push_back( + {response_plan.iq, + orbit.representative_ik_full, + member_ik_full, + route->spatial_isym, + route->time_reversal, + route->fold_G_by_full_k[static_cast(member_ik_full)]}); + } + } + weak_symmetry_source = select_sternheimer_weak_q_symmetry_source( + response_plan.iq, unit.source_k, orbits, weak_symmetry_routes); + validate_sternheimer_weak_q_symmetry_band_coverage( + unit.band_begin, unit.band_end, occupied_counts.at(static_cast(unit.source_k - 1))); + if (!weak_symmetry_source.source_is_representative) + { + const std::string skipped_stem = unit.stem(response_plan.iq); + std::ofstream skipped(skipped_stem + "_unit.dat"); + if (!skipped) + throw std::runtime_error("Cannot open weak-q nonrepresentative skip audit."); + skipped << "format_version 1\nstatus diagnostic_only\nphysical_result no\n" + << "full_q_response no\nreader_v1 no\nsymmetry_skipped_nonrepresentative yes\n" + << "iq " << response_plan.iq << "\nsource_k " << unit.source_k + << "\nrepresentative_k " << weak_symmetry_source.representative_ik_full + 1 + << "\norbit_size " << weak_symmetry_source.orbit_size + << "\ndiscrete_spatial_group_order " << weak_discrete_spatial_order + << "\nfixed_q_little_group_order " << weak_fixed_q_little_group_order + << "\nresponse_equations 0\nunit_complete yes\n"; + for (const auto& route : weak_symmetry_source.inverse_routes) + skipped << "fixed_q_route " << route.iq << ' ' << route.representative_ik_full << ' ' + << route.member_ik_full << ' ' << route.spatial_isym << ' ' + << static_cast(route.time_reversal) << ' ' << route.fold_G[0] << ' ' + << route.fold_G[1] << ' ' << route.fold_G[2] << '\n'; + skipped.close(); + return; + } + } + auto owned_columns = unit.columns(); + if (parallel_benchmark) + { + if (owned_columns.size() < 48 || unit.band_end <= unit.band_begin + || unit.frequency_end <= unit.frequency_begin) + throw std::invalid_argument("Weak q benchmark requires 48 columns and distinct band/frequency endpoints."); + const auto all_columns = owned_columns; + owned_columns.clear(); + for (std::size_t i = 0; i < 48; ++i) + owned_columns.push_back(all_columns[i * (all_columns.size() - 1) / 47]); + } + int allocated_threads = 1; +#ifdef _OPENMP + allocated_threads = omp_get_max_threads(); +#endif + allocated_threads = positive_int_from_env("SLURM_CPUS_PER_TASK", allocated_threads); + const SternheimerWeakQLayout layout{ + positive_int_from_env("WEAK_Q_WORKERS", 1), + positive_int_from_env("WEAK_Q_INNER_THREADS", allocated_threads), allocated_threads}; + layout.validate(); + const auto weak_preconditioner_mode = sternheimer_weak_preconditioner_mode(); + const double weak_residual_tolerance = sternheimer_weak_residual_tolerance(); + const auto& pair = response_plan.kq_pairs.at(unit.source_k - 1); + const int coulomb_reader_iq + = sternheimer_coulomb_reader_q_index_one_based(records, response_plan.iq); + if (pair.source_index != unit.source_k - 1) + throw std::invalid_argument("Weak q unit full-k source does not match the response plan."); + const auto& source_record = records.at(response_plan.record_index_by_global_k.at(pair.source_index)); + const auto& target_record = records.at(response_plan.record_index_by_global_k.at(pair.target_index)); + validate_sternheimer_weak_q_fold(sternheimer_lcao_grid_kpoint(source_record), response_plan.qpoint, + sternheimer_lcao_grid_kpoint(target_record), pair.reciprocal_shift); + // Fields are full Bloch values. Canonical target E admits integer folds without an extra exp(iG.r). + fine_grid.grid.kpoint = sternheimer_lcao_grid_kpoint(target_record); + coarse_grid.grid.kpoint = fine_grid.grid.kpoint; + const std::string stem = unit.stem(response_plan.iq) + (coulomb_only ? "_coulomb_only" : "") + + (parallel_benchmark ? "_benchmark_w" + std::to_string(layout.workers) + + "_t" + std::to_string(layout.inner_threads) : ""); + const auto output = [](const std::string& path) { + std::ofstream stream(path, std::ios::out | std::ios::trunc); + stream.exceptions(std::ios::failbit | std::ios::badbit); + stream << std::setprecision(17); + return stream; + }; + const auto limits = [](std::ostream& out) { + out << "format_version 1\nfull_q_response no\nphysical_result no\nreader_v1 no\n" + << "response_symmetry_restoration no\nresponse_representation raw_potential_potential\n" + << "occupation_weighted yes\nfrequency_weight_applied no\nqweight_applied no\n" + << "signed_branches both\nsecond_hermitian_completion no\n"; + }; + const auto identity = [&](std::ostream& out) { + limits(out); + out << "iq " << response_plan.iq << "\nsource_k " << unit.source_k + << "\ntarget_k " << pair.target_index + 1 + << "\nband_begin " << unit.band_begin << "\nband_end " << unit.band_end + << "\nfrequency_begin " << unit.frequency_begin << "\nfrequency_end " << unit.frequency_end + << "\nshard_index " << unit.shard.index << "\nshard_count " << unit.shard.count + << "\nauxiliary_channels " << channels << "\nowned_columns " << owned_columns.size() + << "\nall_source_bands " << (!parallel_benchmark && unit.all_source_bands(occupied_counts[unit.source_k - 1]) ? "yes" : "no") + << "\nall_frequencies " << (!parallel_benchmark && unit.all_frequencies(nfreq) ? "yes" : "no") + << "\nqpoint " << response_plan.qpoint[0] << ' ' << response_plan.qpoint[1] << ' ' << response_plan.qpoint[2] + << "\nreciprocal_shift " << pair.reciprocal_shift[0] << ' ' << pair.reciprocal_shift[1] + << ' ' << pair.reciprocal_shift[2] + << "\ncoulomb_zero_mode " << (gamma_finite_part ? "gamma_finite_part" : "none") + << "\ncoulomb_singular_component " + << (gamma_finite_part ? "removed_2pi_over_q_planar_constant" : "none") + << "\nanalytic_gamma_head_required " << (gamma_finite_part ? "yes" : "no") << '\n'; + if (use_weak_q_spacegroup) + { + out << "weak_q_spacegroup yes\ndiscrete_spatial_group_order " + << weak_discrete_spatial_order << "\nfixed_q_little_group_order " + << weak_fixed_q_little_group_order << "\nsource_k_is_representative " + << (weak_symmetry_source.source_is_representative ? "yes" : "no") + << "\nrepresentative_k " << weak_symmetry_source.representative_ik_full + 1 + << "\norbit_size " << weak_symmetry_source.orbit_size << '\n'; + for (const auto& route : weak_symmetry_source.inverse_routes) + out << "fixed_q_route " << route.iq << ' ' << route.representative_ik_full << ' ' + << route.member_ik_full << ' ' << route.spatial_isym << ' ' + << static_cast(route.time_reversal) << ' ' << route.fold_G[0] << ' ' + << route.fold_G[1] << ' ' << route.fold_G[2] << '\n'; + } + }; + const auto reference_sha256 = reference_digest.finish(); + siab::Sha256 auxiliary_digest; + for (const auto& radials : input.radials_by_type) + { + hash_u64(auxiliary_digest, radials.size()); + for (const auto& radial : radials) + { + hash_int(auxiliary_digest, radial.type_index); + hash_int(auxiliary_digest, radial.angular_momentum); + hash_int(auxiliary_digest, radial.radial_index); + hash_reals(auxiliary_digest, radial.radial_grid); + hash_reals(auxiliary_digest, radial.radial_values); + } + } + for (std::size_t atom = 0; atom < input.atom_types.size(); ++atom) + { + hash_int(auxiliary_digest, input.atom_types[atom]); + for (double value : {input.atom_positions.at(atom).x, input.atom_positions.at(atom).y, + input.atom_positions.at(atom).z}) + { + if (!std::isfinite(value)) throw std::invalid_argument("Weak q unit nonfinite atom position."); + hash_double(auxiliary_digest, value); + } + } + const auto auxiliary_sha256 = auxiliary_digest.finish(); + const auto lattice = sternheimer_fd_grid_lattice_vectors(fine_grid.grid); + const double slab_height = std::abs(lattice[2][2]); + const double normal_sign = lattice[2][2] >= 0 ? 1.0 : -1.0; + bool z_support_contained = true; + std::ostringstream support_report; + support_report << std::setprecision(17); + for (std::size_t atom = 0; atom < input.atom_types.size(); ++atom) + for (const auto& radial : input.radials_by_type.at(input.atom_types[atom])) + { + if (radial.continue_coulomb_tail) + throw std::invalid_argument("Weak q unit requires compact densities, not Coulomb-tail radial functions."); + const double radius = sternheimer_weak_q_radial_support(radial.radial_grid, radial.radial_values); + const double center = normal_sign * input.atom_positions.at(atom).z; + const bool contained = sternheimer_weak_q_z_support_contained(center, radius, slab_height); + z_support_contained = z_support_contained && contained; + support_report << "abf_z_support " << atom << ' ' << radial.angular_momentum << ' ' + << radial.radial_index << ' ' << center << ' ' << radius << ' ' + << center - radius << ' ' << slab_height - center - radius << ' ' + << (contained ? "yes" : "no") << '\n'; + } + auto manifest = output(stem + "_input.dat"); + manifest << "format_version 1\niq " << response_plan.iq << "\nfull_kpoints " << records.size() + << "\nqpoint " << response_plan.qpoint[0] << ' ' << response_plan.qpoint[1] << ' ' << response_plan.qpoint[2] + << "\nnlocal " << runtime.nlocal << "\npbe_symmetry " << runtime.input.symmetry + << "\nreference_sha256 " << reference_sha256 << "\nauxiliary_sha256 " << auxiliary_sha256 + << "\ncompiled_commit " << compiled_commit_metadata() + << "\nexecutable_sha256 " << siab::sha256_file(siab::resolve_executable_path()) + << "\norbital_sha256 " << siab::sha256_file_manifest(siab::resolve_required_input_files( + orbital_dir_from_env_or_input(runtime), orbital_files_from_env_or_cell(ucell), "initial orbital")) + << "\npseudopotential_sha256 " << siab::sha256_file_manifest(siab::resolve_required_input_files( + runtime.input.pseudo_dir, ucell.pseudo_fn, "pseudopotential")) + << "\nfine_grid " << fine_grid.grid.nx << ' ' << fine_grid.grid.ny << ' ' << fine_grid.grid.nz + << "\ncoarse_grid " << coarse_grid.grid.nx << ' ' << coarse_grid.grid.ny << ' ' << coarse_grid.grid.nz + << "\nfine_dv " << fine_grid.volume_element << "\npca_threshold " << pca_threshold + << "\ncoulomb_kernel strict2d\ncoulomb_zero_mode " + << (gamma_finite_part ? "gamma_finite_part" : "none") + << "\ncoulomb_singular_component " + << (gamma_finite_part ? "removed_2pi_over_q_planar_constant" : "none") + << "\nanalytic_gamma_head_required " << (gamma_finite_part ? "yes" : "no") + << "\nkweight_sum " << response_plan.kweight_sum + << "\nsource_reference original_pbe_band_order\n"; + manifest << "reference_contract_sha256 " << reference_contract << '\n'; + manifest << "slab_z_length_bohr " << slab_height << "\nz_support_contained " + << (z_support_contained ? "yes" : "no") << "\nz_support_treatment no_atom_rewrapping\n" + << support_report.str(); + manifest << "coulomb_reader_iq " << coulomb_reader_iq << '\n'; + for (const auto& generated_path : find_coulomb_v1_rank_files(coulomb_reader_iq, runtime.nproc)) + { + std::string path = generated_path; + if (const char* directory = std::getenv("WEAK_Q_COULOMB_REFERENCE_DIR")) + path = std::string(directory) + "/" + generated_path.substr(generated_path.find_last_of('/') + 1); + manifest << "coulomb_reference_file " << path << ' ' << siab::sha256_file(path) << '\n'; + } + for (std::size_t k = 0; k < records.size(); ++k) + { + const auto& record = records.at(response_plan.record_index_by_global_k.at(k)); + manifest << "kpoint " << k + 1 << ' ' << record.kpoint[0] << ' ' << record.kpoint[1] << ' ' + << record.kpoint[2] << ' ' << record.kweight << ' ' << record.eigenvalues.size() << ' ' + << record.unoccupied_eigenvalues.size() << '\n' + << "coefficients_sha256 " << k + 1 << ' ' << coefficient_hashes[k] << '\n'; + for (std::size_t ib = 0; ib < record.eigenvalues.size(); ++ib) + manifest << "source_band " << k + 1 << ' ' << ib + 1 << ' ' << record.eigenvalues[ib] << ' ' + << record.occupations[ib] << ' ' << sternheimer_weak_q_weight(record.kweight, record.occupations[ib]) << '\n'; + for (std::size_t ib = 0; ib < record.unoccupied_eigenvalues.size(); ++ib) + manifest << "target_virtual " << k + 1 << ' ' << record.eigenvalues.size() + ib + 1 << ' ' + << record.unoccupied_eigenvalues[ib] << '\n'; + } + for (const auto& entry : response_plan.kq_pairs) + { + const auto& source = records.at(response_plan.record_index_by_global_k.at(entry.source_index)); + const auto& target = records.at(response_plan.record_index_by_global_k.at(entry.target_index)); + validate_sternheimer_weak_q_fold(source.kpoint, response_plan.qpoint, target.kpoint, entry.reciprocal_shift); + manifest << "kpair " << entry.source_index + 1 << ' ' << entry.target_index + 1 << ' ' + << entry.reciprocal_shift[0] << ' ' << entry.reciprocal_shift[1] << ' ' << entry.reciprocal_shift[2] << '\n'; + } + manifest << "frequency_table " << nfreq << "\nfrequency_grid_file " + << std::quoted(runtime.input.sternheimer_freq_file) << '\n'; + for (int f = 0; f < nfreq; ++f) + manifest << "frequency " << f + 1 << ' ' << frequency_grid.omega_ha[f] << ' ' << frequency_grid.weights_ha[f] << '\n'; + manifest << "auxiliary_channels " << channels << '\n'; + for (int j = 0; j < channels; ++j) + { + const auto& channel = metadata[j]; + if (channel.channel_index != j) throw std::invalid_argument("Weak q unit auxiliary channel IDs are not contiguous."); + manifest << "auxiliary_channel " << j + 1 << " atom_index " << channel.atom_index + << " atom_local_index " << channel.atom_local_index << " type_index " << channel.type_index + << " L " << channel.angular_momentum << " M " << channel.magnetic_index + << " radial_index " << channel.radial_index << " label " << std::quoted(channel.label) << '\n'; + } + manifest << "manifest_complete yes\n"; + manifest.close(); + const auto input_manifest_sha256 = siab::sha256_file(stem + "_input.dat"); + auto audit = output(stem + "_unit.dat"); + identity(audit); + audit << "input_manifest " << stem << "_input.dat\ninput_manifest_sha256 " << input_manifest_sha256 + << "\nreference_sha256 " << reference_sha256 << "\nauxiliary_sha256 " << auxiliary_sha256 + << "\nexpected_equations " << (coulomb_only ? 0 : unit.expected_equations()) + << "\nmode " << (coulomb_only ? "coulomb_only" : "response_unit") << '\n'; + audit << "parallel_workers " << layout.workers << "\ninner_threads " << layout.inner_threads + << "\nallocated_threads " << layout.allocated_threads + << "\nparallel_benchmark " << (parallel_benchmark ? "yes" : "no") << '\n'; + const auto stage = [&](const std::string& name) { + const auto memory = detect_sternheimer_memory_snapshot(); + audit << "stage " << name << " elapsed_s " << elapsed_seconds_since(started) + << " memory_current_bytes " << memory.current_bytes << '\n'; + audit.flush(); + }; + const auto sample_density = [&](int j) { + auto single = sample_sternheimer_abf_bloch_grid_channels( + input.radials_by_type, input.atom_types, input.atom_positions, fine_grid.grid, response_plan.qpoint, 1, j); + if (single.size() != 1 || single.front().channel_index != j + || single.front().potential_r.size() != fine_potential.size() + || !std::all_of(single.front().potential_r.begin(), single.front().potential_r.end(), finite_complex)) + throw std::runtime_error("Weak q unit failed to sample finite full-Bloch auxiliary density."); + return single; + }; + const auto sample_potential = [&](int j) { + auto single = sample_density(j); + if (gamma_finite_part) + solve_sternheimer_abf_strict2d_coulomb_finite_part_in_place( + single, fine_grid.grid, response_plan.qpoint); + else + solve_sternheimer_abf_strict2d_coulomb_in_place(single, fine_grid.grid, response_plan.qpoint); + if (single.front().potential_r.size() != fine_potential.size() + || !std::all_of(single.front().potential_r.begin(), single.front().potential_r.end(), finite_complex)) + throw std::runtime_error("Weak q unit nonfinite strict2D potential."); + return std::move(single.front().potential_r); + }; + // Coulomb-only mode stops here, before NAO blocks or any response equation. + auto coulomb = output(stem + "_coulomb.dat"); + identity(coulomb); + coulomb << "coulomb_reader_iq " << coulomb_reader_iq + << "\ninput_manifest_sha256 " << input_manifest_sha256 + << "\nintegral_representation density_potential_Ha\n" + << "integral_occupation_weighted no\nkernel_matched_to_ewald no\nz_support_contained " + << (z_support_contained ? "yes" : "no") << '\n'; + std::vector samples{0, std::min(1, channels - 1), channels / 2, channels - 1, + owned_columns.front(), owned_columns.back()}; + std::sort(samples.begin(), samples.end()); + samples.erase(std::unique(samples.begin(), samples.end()), samples.end()); + coulomb << "coulomb_sample_channels " << samples.size() << '\n'; + for (int j : samples) + { + const Vector v = sample_potential(j); + std::vector rows{0, std::min(1, channels - 1), j}; + std::sort(rows.begin(), rows.end()); + rows.erase(std::unique(rows.begin(), rows.end()), rows.end()); + for (int i : rows) + { + const auto density = sample_density(i); + const Complex value = sternheimer_fd_grid_dot(density.front().potential_r, v, fine_grid.volume_element); + if (!finite_complex(value)) throw std::runtime_error("Weak q unit nonfinite density-potential integral."); + coulomb << "coulomb_integral " << i + 1 << ' ' << j + 1 << ' ' << value.real() << ' ' << value.imag() << '\n'; + } + } + coulomb << "coulomb_samples_complete yes\n"; + coulomb.close(); + audit << "coulomb_file " << stem << "_coulomb.dat\nkernel_matched_to_ewald no\n"; + stage("coulomb_samples_complete"); + if (full_coulomb_matrix) + { + if (!z_support_contained) + throw std::runtime_error("Full Coulomb audit requires contained radial z supports."); + const std::size_t ng = fine_potential.size(); + const std::size_t nc = static_cast(channels); + constexpr std::size_t panel_width = 16; + constexpr std::size_t workspace_limit = std::size_t(96)*1024*1024*1024; + if (nc == 0 || ng > workspace_limit/sizeof(Complex)/(nc+panel_width)) + throw std::length_error("Full Coulomb audit exceeds 96 GiB field workspace budget."); + Vector densities(ng*nc); + for (int i = 0; i < channels; ++i) + { + const auto sampled = sample_density(i); + std::copy(sampled.front().potential_r.begin(), sampled.front().potential_r.end(), + densities.begin()+ng*static_cast(i)); + if ((i+1)%32 == 0 || i+1 == channels) stage("full_coulomb_density_"+std::to_string(i+1)); + } + auto full = output(stem+"_full_coulomb.dat"); + identity(full); + full << "input_manifest_sha256 " << input_manifest_sha256 + << "\nintegral_representation density_potential_Ha\nintegral_occupation_weighted no\nfull_matrix_rows " << channels + << "\nfull_matrix_columns " << channels << "\nhermitian_completion no\n"; + for (std::size_t begin = 0; begin < nc; begin += panel_width) + { + const std::size_t count = std::min(panel_width, nc-begin); + Vector potentials(ng*count); + std::exception_ptr sampling_error; +#ifdef _OPENMP +#pragma omp parallel for schedule(dynamic) num_threads(static_cast(count)) +#endif + for (int jj = 0; jj < static_cast(count); ++jj) + { + try + { + std::vector single(1); + const std::size_t j = begin+static_cast(jj); + single[0].potential_r.assign(densities.begin()+ng*j, densities.begin()+ng*(j+1)); + if (gamma_finite_part) + solve_sternheimer_abf_strict2d_coulomb_finite_part_in_place( + single, fine_grid.grid, response_plan.qpoint); + else + solve_sternheimer_abf_strict2d_coulomb_in_place( + single, fine_grid.grid, response_plan.qpoint); + std::copy(single[0].potential_r.begin(), single[0].potential_r.end(), potentials.begin()+ng*jj); + } + catch (...) + { +#ifdef _OPENMP +#pragma omp critical(sternheimer_full_coulomb_error) +#endif + { if (!sampling_error) sampling_error = std::current_exception(); } + } + } + if (sampling_error) std::rethrow_exception(sampling_error); + const auto matrix = sternheimer_abf_coulomb_panel(densities, potentials, ng, nc, count, + fine_grid.volume_element); + for (std::size_t j = 0; j < count; ++j) + for (std::size_t i = 0; i < nc; ++i) + { + const Complex value = matrix[i+nc*j]; + full << "coulomb_integral " << i+1 << ' ' << begin+j+1 << ' ' + << value.real() << ' ' << value.imag() << '\n'; + } + full.flush(); + if (!full) throw std::runtime_error("Full Coulomb audit output write failed."); + stage("full_coulomb_column_"+std::to_string(begin+count)); + } + full << "full_matrix_complete yes\n"; + full.close(); + if (!full) throw std::runtime_error("Full Coulomb audit output close failed."); + audit << "full_coulomb_file " << stem << "_full_coulomb.dat\n"; + stage("full_coulomb_complete"); + } + if (coulomb_only) + { + audit << "response_equations 0\nunit_complete yes\n"; + audit.close(); + return; + } + if (!z_support_contained) + throw std::runtime_error("Weak q unit ABF z support crosses the open slab window; Coulomb samples are diagnostic only. Recenter/extend the common reference window before response admission."); + auto target = sample_sternheimer_lcao_kpoint( + ucell, fine_grid.grid, orbitals, target_record, 0, true, 0, + fine_grid.volume_element, runtime.input.sternheimer_vnorm_tol); + const int nocc = static_cast(target.occupied_functions.size()); + const int nvirtual = static_cast(target.unoccupied_functions.size()); + if (nocc != static_cast(target_record.eigenvalues.size()) + || nvirtual != static_cast(target_record.unoccupied_eigenvalues.size())) + throw std::runtime_error("Weak q unit lost complete target KS states."); + auto functions = std::move(target.occupied_functions); + functions.insert(functions.end(), std::make_move_iterator(target.unoccupied_functions.begin()), + std::make_move_iterator(target.unoccupied_functions.end())); + target = SternheimerSampledLCAOKPoint(); + const double metric_error = orthonormalize_sternheimer_weak_states_in_place(functions, fine_grid.volume_element); + if (!std::isfinite(metric_error) || metric_error > 1e-8) + throw std::runtime_error("Weak q unit target metric gate failed."); + auto nonlocal = make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, fine_grid.grid, fine_grid.volume_element); + auto h = std::make_shared(make_sternheimer_fd_hamiltonian_from_local_potential( + fine_grid, fine_potential, 1.0, std::move(nonlocal), runtime.input.sternheimer_fd_order)); + auto op = std::make_shared(h, coarse_grid.grid); + auto assembled = op->assemble_blocks(functions); + Blocks::Data data; + data.nocc = nocc; data.nvirtual = nvirtual; data.ncoarse = coarse_grid.grid.size(); + data.hu = std::move(assembled.state_hamiltonian); + data.l = std::move(assembled.state_coarse_overlap); + data.k = std::move(assembled.state_coarse_hamiltonian); + auto blocks = std::make_shared(std::move(data)); + audit << "nocc " << nocc << "\nnvirtual " << nvirtual << "\northonormalization_max_error " << metric_error + << "\ncomplement_metric_min " << blocks->minimum_metric_eigenvalue() << '\n'; + for (auto& function : functions) + for (auto& gradient : function.gradients) Vector().swap(gradient); + trim_sternheimer_process_heap(); + stage("weak_blocks_ready"); + if (env_is_true("ABACUS_STERNHEIMER_WEAK_EXACT_CACHE")) + { + Vector probe(blocks->ncoarse()), uncached, cached; + for (std::size_t i = 0; i < probe.size(); ++i) + probe[i] = Complex(std::sin(0.31 * (i + 1)), std::cos(0.17 * (i + 1))); + const auto before = std::chrono::steady_clock::now(); + op->apply(probe, uncached); + const double uncached_s = elapsed_seconds_since(before); + const auto enable = std::chrono::steady_clock::now(); + op->enable_exact_apply_cache(true, true); + const double enable_s = elapsed_seconds_since(enable); + const auto after = std::chrono::steady_clock::now(); + op->apply(probe, cached); + const double cached_s = elapsed_seconds_since(after); + if (uncached.size() != probe.size() || cached.size() != probe.size()) + throw std::runtime_error("Weak q cache probe returned an invalid dimension."); + double norm_reference = 0, norm_difference = 0; + for (std::size_t i = 0; i < probe.size(); ++i) + { + if (!finite_complex(uncached[i]) || !finite_complex(cached[i])) + throw std::runtime_error("Weak q cache probe returned nonfinite values."); + norm_reference = std::hypot(norm_reference, std::abs(uncached[i])); + norm_difference = std::hypot(norm_difference, std::abs(cached[i] - uncached[i])); + } + const double cache_error = norm_reference > 0 ? norm_difference / norm_reference : norm_difference; + audit << "cache_probe_relative_error " << cache_error << "\ncache_uncached_apply_s " << uncached_s + << "\ncache_enable_s " << enable_s << "\ncache_cached_apply_s " << cached_s + << "\ncache_storage_bytes " << op->exact_cache_storage_bytes() + << "\ncache_nonlocal " << op->has_exact_nonlocal_cache() << "\ncache_local " << op->has_exact_local_cache() + << "\ncache_probe_is_not_operator_proof yes\n"; + audit.flush(); + if (!std::isfinite(cache_error) || cache_error > 1e-10) + throw std::runtime_error("Weak q exact cache probe failed 1e-10 gate."); + } + const auto elements = [](std::size_t rows, std::size_t columns) { + if (columns && rows > Vector().max_size() / columns) + throw std::length_error("Weak q unit matrix size overflow."); + return rows * columns; + }; + // FFTW captures its thread count when each plan is created. Build private + // operators serially; do not hold the plan lock across a constructor. + const auto plan_threads = [](int threads) { +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) + { fftw_plan_with_nthreads(threads); } +#else + (void)threads; +#endif + }; + struct FFTWidthRestore + { + int threads; + ~FFTWidthRestore() + { +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) + { fftw_plan_with_nthreads(threads); } +#endif + } + }; + std::vector> worker_ops; + const auto operators_started = std::chrono::steady_clock::now(); + plan_threads(layout.inner_threads); + try + { + sternheimer_weak_q_detail::InnerThreadsGuard guard(layout.inner_threads); + for (int worker_index = 0; worker_index < layout.workers; ++worker_index) + { + auto worker_op = std::make_shared(h, coarse_grid.grid); + if (env_is_true("ABACUS_STERNHEIMER_WEAK_EXACT_CACHE")) + worker_op->enable_exact_apply_cache(true, true); + worker_ops.push_back(std::move(worker_op)); + } + } + catch (...) + { + plan_threads(allocated_threads); + throw; + } + plan_threads(allocated_threads); + audit << "parallel_operator_setup_s " << elapsed_seconds_since(operators_started) << '\n'; + stage("parallel_operators_ready"); + std::vector partials(static_cast(unit.frequency_end - unit.frequency_begin + 1), + Vector(elements(channels, owned_columns.size()), Complex(0))); + SternheimerRPA::SolverOptions options; + options.max_iter = positive_int_from_env("ABACUS_STERNHEIMER_WEAK_AUDIT_MAX_ITER", 300); + options.residual_tol = weak_residual_tolerance; + options.use_fd_spectral_preconditioner = false; + std::uint64_t equations = 0; + std::uint64_t fine_checked_equations = 0; + std::uint64_t fine_vertex_checks = 0; + audit << "fine_check_policy first_last_owned_columns_each_band_frequency_sign\n" + << "fine_check_tolerance 1e-9\nweak_preconditioner " + << sternheimer_weak_preconditioner_name(weak_preconditioner_mode) + << "\nweak_preconditioner_regularization_Ry 0\noriginal_residual_tolerance " + << options.residual_tol << '\n'; + for (int ib = unit.band_begin; ib <= unit.band_end; ++ib) + { + if (parallel_benchmark && ib != unit.band_begin && ib != unit.band_end) continue; + // Preserve each original source band and its energy; only normalize its fine-grid norm. + auto sampled = build_lcao_grid_functions_from_coefficients(ucell, fine_grid.grid, orbitals, + sternheimer_lcao_grid_kpoint(source_record), {source_record.coefficients.at(ib - 1)}); + Vector source_values = std::move(sampled.at(0).values); + sampled.clear(); + const double source_norm = sternheimer_fd_grid_norm(source_values, fine_grid.volume_element); + if (!std::isfinite(source_norm) || source_norm <= runtime.input.sternheimer_vnorm_tol) + throw std::runtime_error("Weak q unit invalid source norm."); + for (auto& value : source_values) value /= source_norm; + const double epsilon = source_record.eigenvalues.at(ib - 1); + const double occupation = sternheimer_weak_q_weight(source_record.kweight, source_record.occupations.at(ib - 1)); + audit << "source_band " << ib << " epsilon_Ry " << epsilon << " source_norm " << source_norm + << " weighted_occupation " << occupation << '\n'; + const auto sample_fine_vertex = [&](int j) { + Vector g = sample_potential(j); + for (std::size_t ir = 0; ir < g.size(); ++ir) + { + g[ir] *= 2.0 * source_values[ir]; + if (!finite_complex(g[ir])) throw std::runtime_error("Weak q unit nonfinite fine vertex."); + } + return g; + }; + const int ncoarse = blocks->ncoarse(); + Blocks::Vertices projected_vertices; + projected_vertices.f.resize(elements(nvirtual, channels)); + projected_vertices.w.resize(elements(ncoarse, channels)); + const Vector first_row_vertex = sample_fine_vertex(0); + for (int j = 0; j < channels; ++j) + { + const Vector sampled_vertex = j == 0 ? Vector() : sample_fine_vertex(j); + const Vector& g = j == 0 ? first_row_vertex : sampled_vertex; + Vector gu(functions.size()), ge; + for (std::size_t u = 0; u < functions.size(); ++u) + gu[u] = sternheimer_fd_grid_dot(functions[u].values, g, fine_grid.volume_element); + op->project(g, ge); + const auto vertex = blocks->project_vertices(gu, ge); + std::copy(vertex.f.begin(), vertex.f.end(), projected_vertices.f.begin() + std::size_t(j) * nvirtual); + std::copy(vertex.w.begin(), vertex.w.end(), projected_vertices.w.begin() + std::size_t(j) * ncoarse); + if ((j + 1) % 32 == 0 || j + 1 == channels) stage("vertices_projected_" + std::to_string(j + 1)); + } + stage("source_vertices_ready"); + for (int ifrequency = unit.frequency_begin; ifrequency <= unit.frequency_end; ++ifrequency) + { + if (parallel_benchmark && ifrequency != unit.frequency_begin && ifrequency != unit.frequency_end) continue; + auto& partial = partials.at(ifrequency - unit.frequency_begin); + for (int sign : {1, -1}) + { + std::mutex audit_mutex; + std::mutex fine_check_mutex; + const auto batch_started = std::chrono::steady_clock::now(); + FFTWidthRestore restore_width{allocated_threads}; + plan_threads(layout.inner_threads); + const double signed_omega = sign * sternheimer_weak_q_omega_ry( + frequency_grid.omega_ha.at(ifrequency - 1)); + const auto task_counts = run_sternheimer_weak_q_tasks(owned_columns.size(), layout, + [&](int worker_index) { + auto worker_op = worker_ops.at(worker_index); + Blocks::Apply apply_preconditioner; + if (weak_preconditioner_mode + == SternheimerWeakPreconditionerMode::Spectral) + { + auto preconditioner = std::make_shared< + SternheimerWeakSpectralPreconditioner>( + coarse_grid.grid, h->kinetic_prefactor(), epsilon, + signed_omega, 0.0); + apply_preconditioner + = [preconditioner](const Vector& x, Vector& y) { + preconditioner->apply(x, y); + }; + } + return std::unique_ptr(new Blocks::Worker(blocks, + [worker_op](const Vector& x, Vector& y) { worker_op->apply(x, y); }, + epsilon, signed_omega, 512ULL * 1024 * 1024, + std::move(apply_preconditioner))); + }, [&](std::size_t owned, std::unique_ptr& worker, int worker_index) + { + const int j = owned_columns[owned]; + const auto f = projected_vertices.f.begin() + std::size_t(j) * nvirtual; + const auto w = projected_vertices.w.begin() + std::size_t(j) * ncoarse; + const Blocks::Vertices vertex{Vector(f, f + nvirtual), Vector(w, w + ncoarse)}; + const auto solve_start = std::chrono::steady_clock::now(); + const auto result = worker->solve(vertex, options); + const double solve_seconds = elapsed_seconds_since(solve_start); + std::uint64_t equation_id; + { + std::lock_guard lock(audit_mutex); + equation_id = ++equations; + audit << "equation " << equation_id << " band " << ib << " ifrequency " << ifrequency + << " column " << j + 1 << " sign " << sign << " converged " << (result.converged ? "yes" : "no") + << " iterations " << result.schur.iterations << " relative_residual " << result.relative_residual + << " absolute_residual " << result.absolute_residual << " solve_s " << solve_seconds + << " worker " << worker_index << '\n'; + audit.flush(); + } + validate_sternheimer_weak_q_residual(result.converged, + result.relative_residual, + result.absolute_residual, + options.residual_tol); + if (result.coefficients.size() != static_cast(nvirtual) + ncoarse + || !std::all_of(result.coefficients.begin(), result.coefficients.end(), finite_complex)) + throw std::runtime_error("Weak q unit nonfinite or incomplete response coefficients."); + Vector column(channels, Complex(0)); + const Complex left_factor(sternheimer_weak_q_left_vertex_factor()); + BlasConnector::gemv('C', nvirtual, channels, left_factor, projected_vertices.f.data(), nvirtual, + result.coefficients.data(), 1, Complex(0), column.data(), 1); + BlasConnector::gemv('C', ncoarse, channels, left_factor, projected_vertices.w.data(), ncoarse, + result.coefficients.data() + nvirtual, 1, Complex(1), column.data(), 1); + if (owned == 0 || owned + 1 == owned_columns.size()) + { + std::lock_guard check_lock(fine_check_mutex); + std::ostringstream check_log; + check_log << std::setprecision(17); + ++fine_checked_equations; + const auto fine_check_started = std::chrono::steady_clock::now(); + const auto expansion = blocks->expand_coordinates(result.coefficients); + Vector fine_response; + worker_ops.at(worker_index)->lift(expansion.e, fine_response); + for (std::size_t u = 0; u < functions.size(); ++u) + { +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir < fine_response.size(); ++ir) + fine_response[ir] += expansion.u[u] * functions[u].values[ir]; + } + if (!std::all_of(fine_response.begin(), fine_response.end(), finite_complex)) + throw std::runtime_error("Weak q unit nonfinite fine response check."); + const Vector diagonal_vertex = j == 0 ? Vector() : sample_fine_vertex(j); + for (int i : {0, j}) + { + const Complex fine_value = sternheimer_weak_q_left_vertex_factor() * sternheimer_fd_grid_dot( + i == 0 ? first_row_vertex : diagonal_vertex, fine_response, fine_grid.volume_element); + const double error = std::abs(fine_value - column[i]) / std::max(1.0, std::abs(fine_value)); + check_log << "vertex_check " << equation_id << ' ' << i + 1 << ' ' << error << '\n'; + ++fine_vertex_checks; + if (!finite_complex(fine_value) || !std::isfinite(error) || error > 1e-9) + throw std::runtime_error("Weak q unit fine/block left-right vertex gate failed."); + if (j == 0) break; + } + check_log << "fine_check_seconds " << equation_id << ' ' << elapsed_seconds_since(fine_check_started) << '\n'; + std::lock_guard log_lock(audit_mutex); + audit << check_log.str(); + } + accumulate_sternheimer_weak_q_column(partial, column, channels, owned, occupation); + }); + plan_threads(allocated_threads); + audit << "parallel_batch band " << ib << " ifrequency " << ifrequency << " sign " << sign + << " equations " << owned_columns.size() << " wall_s " << elapsed_seconds_since(batch_started) << '\n'; + for (std::size_t w = 0; w < task_counts.size(); ++w) + audit << "worker_tasks " << w << ' ' << task_counts[w] << '\n'; + audit.flush(); + } + stage("source_frequency_complete_" + std::to_string(ifrequency)); + } + } + const auto expected_equations = parallel_benchmark ? std::uint64_t(48 * 2 * 2 * 2) : unit.expected_equations(); + if (equations != expected_equations) throw std::runtime_error("Weak q unit equation coverage is incomplete."); + for (int ifrequency = unit.frequency_begin; ifrequency <= unit.frequency_end; ++ifrequency) + { + if (parallel_benchmark && ifrequency != unit.frequency_begin && ifrequency != unit.frequency_end) continue; + const auto& partial = partials.at(ifrequency - unit.frequency_begin); + const auto filename = stem + "_columns_ifreq_" + std::to_string(ifrequency) + ".dat"; + auto columns = output(filename); + identity(columns); + columns << "input_manifest_sha256 " << input_manifest_sha256 << "\nifrequency " << ifrequency + << "\nomega_Ha " << frequency_grid.omega_ha.at(ifrequency - 1) + << "\nweight_Ha " << frequency_grid.weights_ha.at(ifrequency - 1) + << "\nlayout column_major_owned_columns\n"; + for (std::size_t owned = 0; owned < owned_columns.size(); ++owned) + { + columns << "owned_column " << owned_columns[owned] + 1 << '\n'; + for (int i = 0; i < channels; ++i) + { + const Complex value = partial[std::size_t(i) + std::size_t(channels) * owned]; + if (!finite_complex(value)) throw std::runtime_error("Weak q unit nonfinite final signed weighted response."); + columns << "response " << i + 1 << ' ' << owned_columns[owned] + 1 << ' ' + << value.real() << ' ' << value.imag() << '\n'; + } + } + columns << "columns_complete yes\n"; + columns.close(); + audit << "column_file " << ifrequency << ' ' << filename << '\n'; + } + stage("response_unit_complete"); + audit << "response_equations " << equations << "\nfine_checked_equations " << fine_checked_equations + << "\nfine_vertex_checks " << fine_vertex_checks + << (parallel_benchmark ? "\nbenchmark_complete yes\nunit_complete no\n" : "\nunit_complete yes\n"); + audit.close(); +} + +void run_sternheimer_weak_response_audit( + const UnitCell& ucell, + const LCAO_Orbitals& orbitals, + SternheimerABACUSFDGridData fine_grid, + SternheimerABACUSFDGridData coarse_grid, + const std::vector& fine_potential, + const SternheimerLCAOOccupiedKPoint& source_record, + const SternheimerLCAOOccupiedKPoint& target_record, + const SternheimerReducedKPoint& qpoint, + const double omega_ha, + const double pca_threshold, + const SternheimerOrbitalSet* rpa_abfs, + const int source_index, + const int target_index, + const SternheimerAbacusRuntime& runtime) +{ + using Vector = SternheimerFDHamiltonian::Vector; + using Complex = SternheimerFDHamiltonian::Complex; + using Blocks = SternheimerWeakAugmented; + const bool matrix_audit = env_is_true("ABACUS_STERNHEIMER_WEAK_MATRIX_AUDIT"); + const auto started = std::chrono::steady_clock::now(); + std::ofstream audit("STERNHEIMER_WEAK_RESPONSE_AUDIT.dat"); + if (!audit) throw std::runtime_error("Cannot open fine-weak response audit."); + audit << std::setprecision(17) + << "status diagnostic_only\nphysical_result no\nfull_q_response no\n" + << "reference fine_analytic_weak_form\nsource_band_selection VBM_only\n" + << (matrix_audit ? "auxiliary_channel_selection all\nsymmetry_restoration no\n" + : "auxiliary_channel_selection first_two_only\nsymmetry_restoration no\n") + << "fine_grid " << fine_grid.grid.nx << ' ' << fine_grid.grid.ny << ' ' << fine_grid.grid.nz << '\n' + << "coarse_grid " << coarse_grid.grid.nx << ' ' << coarse_grid.grid.ny << ' ' << coarse_grid.grid.nz << '\n' + << "source_target " << source_index + 1 << ' ' << target_index + 1 << '\n' + << "omega_Ha " << omega_ha << '\n'; + if (matrix_audit) + audit << "matrix_audit yes\nresponse_matrix partial\n" + << "global_matrix_checks deferred_external_merge\n" + << "qpoint " << qpoint[0] << ' ' << qpoint[1] << ' ' << qpoint[2] << '\n'; + const auto stage = [&](const std::string& name, const double solve_s = -1.0, + const double contraction_s = -1.0, const double finecheck_s = -1.0) { + const auto memory = detect_sternheimer_memory_snapshot(); + audit << "stage " << name << " elapsed_s " << elapsed_seconds_since(started) + << " memory_current_bytes " << memory.current_bytes; + if (solve_s >= 0.0) + audit << " solve_s " << solve_s << " contraction_s " << contraction_s << " finecheck_s " << finecheck_s; + audit << '\n'; + audit.flush(); + if (!audit) throw std::runtime_error("Fine-weak response audit write failed."); + }; + const auto source_k = sternheimer_lcao_grid_kpoint(source_record); + fine_grid.grid.kpoint = sternheimer_lcao_grid_kpoint(target_record); + coarse_grid.grid.kpoint = fine_grid.grid.kpoint; + for (int a = 0; a < 3; ++a) + if (std::abs(fine_grid.grid.kpoint[a] - source_k[a] - qpoint[a]) > 1e-12) + throw std::runtime_error("Fine-weak audit does not yet admit BZ-folded k pairs."); + if (source_record.eigenvalues.empty() || target_record.unoccupied_eigenvalues.empty() + || !(omega_ha > 0.0) || !std::isfinite(omega_ha)) + throw std::runtime_error("Fine-weak audit requires complete KS records and positive frequency."); + if (target_record.eigenvalues.size() + target_record.unoccupied_eigenvalues.size() + != static_cast(runtime.nlocal) + || target_record.coefficients.size() != target_record.eigenvalues.size() + || target_record.unoccupied_coefficients.size() != target_record.unoccupied_eigenvalues.size()) + throw std::runtime_error("Fine-weak audit requires nbands=nlocal and every target coefficient vector."); + if (matrix_audit) + { + if (source_record.coefficients.size() != source_record.eigenvalues.size() + || source_record.occupations.size() != source_record.eigenvalues.size() + || target_record.eigenvalues.empty() + || target_record.occupations.size() != target_record.eigenvalues.size() + || fine_potential.size() != static_cast(fine_grid.grid.size())) + throw std::runtime_error("Weak matrix audit requires complete source/target KS records and full fine potential."); + const auto finite_real = [](const double value) { return std::isfinite(value); }; + for (const auto* values : {&source_record.eigenvalues, &target_record.eigenvalues, + &target_record.unoccupied_eigenvalues, &fine_potential}) + if (!std::all_of(values->begin(), values->end(), finite_real)) + throw std::runtime_error("Weak matrix audit input contains nonfinite real values."); + for (const auto* coefficients : {&source_record.coefficients, &target_record.coefficients, + &target_record.unoccupied_coefficients}) + for (const auto& state : *coefficients) + if (state.size() != static_cast(runtime.nlocal) + || !std::all_of(state.begin(), state.end(), [](const Complex value) { + return std::isfinite(value.real()) && std::isfinite(value.imag()); + })) + throw std::runtime_error("Weak matrix audit requires complete finite KS coefficient vectors."); + for (int a = 0; a < 3; ++a) + if (!std::isfinite(source_k[a]) || !std::isfinite(fine_grid.grid.kpoint[a]) || !std::isfinite(qpoint[a])) + throw std::runtime_error("Weak matrix audit requires finite k/q coordinates."); + } + auto target = sample_sternheimer_lcao_kpoint( + ucell, fine_grid.grid, orbitals, target_record, 0, true, 0, + fine_grid.volume_element, runtime.input.sternheimer_vnorm_tol); + const int nocc = static_cast(target.occupied_functions.size()); + const int nvirtual = static_cast(target.unoccupied_functions.size()); + if (nocc != static_cast(target_record.eigenvalues.size()) + || nvirtual != static_cast(target_record.unoccupied_eigenvalues.size())) + throw std::runtime_error("Fine-weak audit lost target KS states."); + auto functions = std::move(target.occupied_functions); + functions.insert(functions.end(), std::make_move_iterator(target.unoccupied_functions.begin()), + std::make_move_iterator(target.unoccupied_functions.end())); + target = SternheimerSampledLCAOKPoint(); + stage("fine_states_sampled"); + const double orthogonality_error = orthonormalize_sternheimer_weak_states_in_place(functions, fine_grid.volume_element); + audit << "orthonormalization_max_error " << orthogonality_error << '\n'; + if (orthogonality_error > 1e-8) + throw std::runtime_error("Fine-weak state orthonormalization failed its numerical gate."); + stage("fine_states_orthonormal"); + auto nonlocal = make_sternheimer_fd_nonlocal_projector_from_unitcell( + ucell, fine_grid.grid, fine_grid.volume_element); + auto h = std::make_shared( + make_sternheimer_fd_hamiltonian_from_local_potential( + fine_grid, fine_potential, 1.0, std::move(nonlocal), runtime.input.sternheimer_fd_order)); + auto op = std::make_shared(h, coarse_grid.grid); + auto assembled = op->assemble_blocks(functions); + const double metric_error = assembled.maximum_metric_error; + std::ofstream hu_output("STERNHEIMER_WEAK_HU.dat"); + hu_output << std::setprecision(17) << "# nocc nvirtual, then row col H_Ry_real H_Ry_imag\n" + << nocc << ' ' << nvirtual << '\n'; + for (std::size_t j = 0; j < functions.size(); ++j) + for (std::size_t i = 0; i < functions.size(); ++i) + { + const auto value = assembled.state_hamiltonian[i + functions.size() * j]; + hu_output << i + 1 << ' ' << j + 1 << ' ' << value.real() << ' ' << value.imag() << '\n'; + } + hu_output.close(); + if (!hu_output) throw std::runtime_error("Fine-weak Hamiltonian audit write failed."); + Blocks::Data data; + data.nocc = nocc; data.nvirtual = nvirtual; data.ncoarse = coarse_grid.grid.size(); + data.hu = std::move(assembled.state_hamiltonian); + data.l = std::move(assembled.state_coarse_overlap); + data.k = std::move(assembled.state_coarse_hamiltonian); + auto blocks = std::make_shared(std::move(data)); + audit << "nocc " << nocc << "\nnvirtual " << nvirtual + << "\nfine_state_metric_error " << metric_error + << "\ncomplement_metric_min " << blocks->minimum_metric_eigenvalue() << '\n'; + // Couplings retain every analytic gradient contribution; only now release gradients. + for (auto& function : functions) + for (auto& gradient : function.gradients) Vector().swap(gradient); + trim_sternheimer_process_heap(); + stage("weak_blocks_ready"); + auto source_grid = fine_grid.grid; + source_grid.kpoint = source_k; + auto source = sample_sternheimer_lcao_kpoint( + ucell, source_grid, orbitals, source_record, 0, false, 0, + fine_grid.volume_element, runtime.input.sternheimer_vnorm_tol); + const auto vbm = std::max_element(source_record.eigenvalues.begin(), source_record.eigenvalues.end()); + const auto band = std::size_t(vbm - source_record.eigenvalues.begin()); + const double epsilon = *vbm; + Vector source_values = std::move(source.occupied_functions.at(band).values); + source = SternheimerSampledLCAOKPoint(); + audit << "source_band " << band + 1 << "\nsource_epsilon_Ry " << epsilon << '\n'; + const auto input = build_abfs_density_input(ucell, pca_threshold, runtime, rpa_abfs); + if (matrix_audit) + { + const auto metadata = describe_sternheimer_abf_grid_channels( + input.radials_by_type, input.atom_types, input.atom_positions, -1); + if (metadata.size() > static_cast(std::numeric_limits::max())) + throw std::length_error("Weak matrix audit channel count exceeds BLAS integer range."); + const int channel_count = static_cast(metadata.size()); + const auto shard = SternheimerWeakAuditShard::parse( + std::getenv("WEAK_AUDIT_SHARD_INDEX"), std::getenv("WEAK_AUDIT_SHARD_COUNT"), channel_count); + const auto owned_columns = shard.columns(); + audit << "shard_index " << shard.index << "\nshard_count " << shard.count + << "\nauxiliary_channels " << channel_count << '\n' + << "# auxiliary_channel IDs are 1-based; named metadata indices are 0-based; M is real-Ylm index\n"; + for (int j = 0; j < channel_count; ++j) + { + const auto& channel = metadata[j]; + if (channel.channel_index != j) + throw std::runtime_error("Weak matrix audit requires contiguous global auxiliary channel IDs."); + audit << "auxiliary_channel " << j + 1 << " atom_index " << channel.atom_index + << " atom_local_index " << channel.atom_local_index << " type_index " << channel.type_index + << " L " << channel.angular_momentum << " M " << channel.magnetic_index + << " radial_index " << channel.radial_index << " label " << std::quoted(channel.label) << '\n'; + } + stage("auxiliary_metadata_ready"); + const auto sample_fine_vertex = [&](const int j) { + auto single = sample_sternheimer_abf_bloch_grid_channels( + input.radials_by_type, input.atom_types, input.atom_positions, fine_grid.grid, qpoint, 1, j); + if (single.size() != 1 || single.front().channel_index != j) + throw std::runtime_error("Weak matrix audit failed to sample the requested global auxiliary channel."); + solve_sternheimer_abf_periodic_full_coulomb_in_place(single, fine_grid.grid, qpoint, 0.0); + Vector g = std::move(single.front().potential_r); + if (g.size() != source_values.size()) + throw std::runtime_error("Weak matrix audit fine vertex dimension mismatch."); + for (std::size_t ir = 0; ir < g.size(); ++ir) + { + // In-place Ha -> Ry multiplication avoids a second fine vertex allocation. + g[ir] *= 2.0 * source_values[ir]; + if (!std::isfinite(g[ir].real()) || !std::isfinite(g[ir].imag())) + throw std::runtime_error("Weak matrix audit fine vertex is nonfinite."); + } + return g; + }; + const int ncoarse = blocks->ncoarse(); + const auto checked_elements = [](const std::size_t rows, const std::size_t columns) { + if (columns != 0 && rows > Vector().max_size() / columns) + throw std::length_error("Weak matrix audit projected storage size overflow."); + return rows * columns; + }; + // The all-channel cache contains only gF/gW, packed column-major for BLAS. + Blocks::Vertices projected_vertices; + projected_vertices.f.resize(checked_elements(nvirtual, channel_count)); + projected_vertices.w.resize(checked_elements(ncoarse, channel_count)); + audit << "projected_f_bytes " << projected_vertices.f.size() * sizeof(Complex) + << "\nprojected_w_bytes " << projected_vertices.w.size() * sizeof(Complex) + << "\nfine_vertex_cache_channels 1\n"; + const Vector first_row_vertex = sample_fine_vertex(0); + for (int j = 0; j < channel_count; ++j) + { + { + const Vector sampled = j != 0 ? sample_fine_vertex(j) : Vector(); + const Vector& g = j == 0 ? first_row_vertex : sampled; + Vector gu(functions.size()), ge; + for (std::size_t u = 0; u < functions.size(); ++u) + gu[u] = sternheimer_fd_grid_dot(functions[u].values, g, fine_grid.volume_element); + op->project(g, ge); + const auto vertex = blocks->project_vertices(gu, ge); + std::copy(vertex.f.begin(), vertex.f.end(), + projected_vertices.f.begin() + std::size_t(j) * nvirtual); + std::copy(vertex.w.begin(), vertex.w.end(), + projected_vertices.w.begin() + std::size_t(j) * ncoarse); + } + // Only the first-row fine vertex survives for the repeated selected-row check. + if ((j + 1) % 32 == 0 || j + 1 == channel_count) + { + stage("vertices_projected_" + std::to_string(j + 1)); + } + } + trim_sternheimer_process_heap(); + stage("fine_vertices_ready"); + SternheimerRPA::SolverOptions options; + options.max_iter = positive_int_from_env("ABACUS_STERNHEIMER_WEAK_AUDIT_MAX_ITER", 300); + options.residual_tol = 1e-8; + options.use_fd_spectral_preconditioner = false; + audit << "coordinate_transform complement_inverse_sqrt\npreconditioner none\nmax_iter " + << options.max_iter << "\nresidual_tolerance " << options.residual_tol << '\n'; + Vector partial_response(checked_elements(channel_count, owned_columns.size()), Complex(0.0)); + std::size_t equations = 0; + for (int sign : {1, -1}) + { + Blocks::Worker worker(blocks, [op](const Vector& x, Vector& result) { op->apply(x, result); }, + epsilon, sign * 2.0 * omega_ha); + for (std::size_t owned = 0; owned < owned_columns.size(); ++owned) + { + const int j = owned_columns[owned]; + const auto f_begin = projected_vertices.f.begin() + std::size_t(j) * nvirtual; + const auto w_begin = projected_vertices.w.begin() + std::size_t(j) * ncoarse; + const Blocks::Vertices vertex{Vector(f_begin, f_begin + nvirtual), Vector(w_begin, w_begin + ncoarse)}; + const auto solve_started = std::chrono::steady_clock::now(); + const auto result = worker.solve(vertex, options); + const double solve_s = elapsed_seconds_since(solve_started); + audit << "equation " << ++equations << " channel " << j + 1 << " sign " << sign + << " converged " << (result.converged ? "yes" : "no") << " iterations " << result.schur.iterations + << " full_residual " << result.relative_residual << " elapsed_s " << elapsed_seconds_since(started) << '\n'; + audit.flush(); + if (!audit) throw std::runtime_error("Weak matrix audit equation output failed."); + if (!result.converged) throw std::runtime_error("Fine-weak response audit equation did not converge."); + const auto contraction_started = std::chrono::steady_clock::now(); + Vector block_column(channel_count, Complex(0.0)); + BlasConnector::gemv('C', nvirtual, channel_count, Complex(0.5), projected_vertices.f.data(), nvirtual, + result.coefficients.data(), 1, Complex(0.0), block_column.data(), 1); + BlasConnector::gemv('C', ncoarse, channel_count, Complex(0.5), projected_vertices.w.data(), ncoarse, + result.coefficients.data() + nvirtual, 1, Complex(1.0), block_column.data(), 1); + const double contraction_s = elapsed_seconds_since(contraction_started); + const auto finecheck_started = std::chrono::steady_clock::now(); + const auto expansion = blocks->expand_coordinates(result.coefficients); + Vector fine_response; + op->lift(expansion.e, fine_response); + for (std::size_t u = 0; u < functions.size(); ++u) + { +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir < fine_response.size(); ++ir) + fine_response[ir] += expansion.u[u] * functions[u].values[ir]; + } + const Vector diagonal_vertex = j != 0 ? sample_fine_vertex(j) : Vector(); + for (int i = 0; i < channel_count; ++i) + { + const Complex block_value = block_column[i]; + if (!std::isfinite(block_value.real()) || !std::isfinite(block_value.imag())) + throw std::runtime_error("Weak matrix audit projected response is nonfinite."); + if (i == j || i == 0) + { + // The first row is cached; only the solved column is regenerated. + const Complex fine_value = 0.5 * sternheimer_fd_grid_dot( + i == 0 ? first_row_vertex : diagonal_vertex, fine_response, fine_grid.volume_element); + const double error = std::abs(fine_value - block_value) / std::max(1.0, std::abs(fine_value)); + audit << "vertex_check " << equations << ' ' << i + 1 << ' ' << error << '\n'; + if (!std::isfinite(error) || error > 1e-9) + throw std::runtime_error("Fine-weak left/right vertex consistency failed."); + } + partial_response[std::size_t(i) + std::size_t(channel_count) * owned] += block_value; + } + stage("columns_complete_" + std::to_string(equations), solve_s, contraction_s, + elapsed_seconds_since(finecheck_started)); + } + } + if (equations != 2 * owned_columns.size()) + throw std::runtime_error("Weak matrix audit did not solve both signs for every owned column."); + for (std::size_t owned = 0; owned < owned_columns.size(); ++owned) + for (int i = 0; i < channel_count; ++i) + { + const auto value = partial_response[std::size_t(i) + std::size_t(channel_count) * owned]; + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + throw std::runtime_error("Weak matrix audit signed response sum is nonfinite."); + audit << "response " << i + 1 << ' ' << owned_columns[owned] + 1 << ' ' + << value.real() << ' ' << value.imag() << '\n'; + } + audit << "audit_complete yes\nresponse_equations " << equations << '\n'; + stage("complete"); + return; + } + auto channels = sample_sternheimer_abf_bloch_grid_channels( + input.radials_by_type, input.atom_types, input.atom_positions, fine_grid.grid, qpoint, 2); + if (channels.size() != 2) throw std::runtime_error("Fine-weak audit requires two real ABFS channels."); + solve_sternheimer_abf_periodic_full_coulomb_in_place(channels, fine_grid.grid, qpoint, 0.0); + std::array fine_vertices; + std::array vertices; + for (std::size_t j = 0; j < 2; ++j) + { + auto& g = fine_vertices[j]; + g.resize(source_values.size()); + // The real ABFS potentials are Ha; the weak Hamiltonian and RHS are Ry. + for (std::size_t ir = 0; ir < g.size(); ++ir) g[ir] = 2.0 * channels[j].potential_r[ir] * source_values[ir]; + Vector gu(functions.size()), ge; + for (std::size_t u = 0; u < functions.size(); ++u) + gu[u] = sternheimer_fd_grid_dot(functions[u].values, g, fine_grid.volume_element); + op->project(g, ge); + vertices[j] = blocks->project_vertices(gu, ge); + } + stage("fine_vertices_ready"); + SternheimerRPA::SolverOptions options; + options.max_iter = positive_int_from_env("ABACUS_STERNHEIMER_WEAK_AUDIT_MAX_ITER", 300); + options.residual_tol = 1e-8; + options.use_fd_spectral_preconditioner = false; + audit << "coordinate_transform complement_inverse_sqrt\npreconditioner none\nmax_iter " + << options.max_iter << "\nresidual_tolerance " << options.residual_tol << '\n'; + std::array response{}; + int equations = 0; + for (int sign : {1, -1}) + { + Blocks::Worker worker(blocks, [op](const Vector& x, Vector& result) { op->apply(x, result); }, + epsilon, sign * 2.0 * omega_ha); + for (std::size_t j = 0; j < 2; ++j) + { + const auto result = worker.solve(vertices[j], options); + audit << "equation " << ++equations << " channel " << j + 1 << " sign " << sign + << " converged " << (result.converged ? "yes" : "no") << " iterations " << result.schur.iterations + << " full_residual " << result.relative_residual << " elapsed_s " << elapsed_seconds_since(started) << '\n'; + audit.flush(); + if (!result.converged) throw std::runtime_error("Fine-weak response audit equation did not converge."); + const auto expansion = blocks->expand_coordinates(result.coefficients); + Vector fine_response; + op->lift(expansion.e, fine_response); + for (std::size_t u = 0; u < functions.size(); ++u) + { +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir < fine_response.size(); ++ir) + fine_response[ir] += expansion.u[u] * functions[u].values[ir]; + } + const Vector y(result.coefficients.begin(), result.coefficients.begin() + nvirtual); + const Vector x(result.coefficients.begin() + nvirtual, result.coefficients.end()); + for (std::size_t i = 0; i < 2; ++i) + { + const Complex fine_value = 0.5 * sternheimer_fd_grid_dot(fine_vertices[i], fine_response, fine_grid.volume_element); + const Complex block_value = 0.5 * (sternheimer_fd_grid_dot(vertices[i].f, y, 1.0) + + sternheimer_fd_grid_dot(vertices[i].w, x, 1.0)); + const double error = std::abs(fine_value - block_value) / std::max(1.0, std::abs(fine_value)); + audit << "vertex_check " << equations << ' ' << i + 1 << ' ' << error << '\n'; + if (!std::isfinite(error) || error > 1e-9) + throw std::runtime_error("Fine-weak left/right vertex consistency failed."); + response[i + 2 * j] += fine_value; + } + } + } + for (int j = 0; j < 2; ++j) + for (int i = 0; i < 2; ++i) + audit << "response " << i + 1 << ' ' << j + 1 << ' ' << response[i + 2 * j].real() + << ' ' << response[i + 2 * j].imag() << '\n'; + double hermitian_error = 0.0; + for (int j = 0; j < 2; ++j) + for (int i = 0; i < 2; ++i) + hermitian_error = std::max(hermitian_error, std::abs(response[i + 2 * j] - std::conj(response[j + 2 * i]))); + const auto off_diagonal = 0.5 * (response[1] + std::conj(response[2])); + const double maximum_eigenvalue = 0.5 * (response[0].real() + response[3].real()) + + std::hypot(0.5 * (response[0].real() - response[3].real()), std::abs(off_diagonal)); + audit << "response_hermitian_error " << hermitian_error + << "\nresponse_max_eigenvalue " << maximum_eigenvalue << '\n'; + audit.flush(); + if (!std::isfinite(maximum_eigenvalue) || hermitian_error > 1e-7 || maximum_eigenvalue > 1e-8) + throw std::runtime_error("Fine-weak diagnostic response sign/Hermiticity failed."); + audit << "audit_complete yes\nresponse_equations " << equations << '\n'; + stage("complete"); +} + +std::vector> collect_channel_potentials(const std::vector& channels) +{ + std::vector> potentials; + potentials.reserve(channels.size()); + for (const SternheimerABFGridChannel& channel: channels) + { + potentials.push_back(channel.potential_r); + } + return potentials; +} + +std::vector collect_channel_potentials( + const std::vector& channels) +{ + std::vector potentials; + potentials.reserve(channels.size()); + for (const SternheimerABFBlochGridChannel& channel: channels) + { + potentials.push_back(channel.potential_r); + } + return potentials; +} + +std::vector scale_potential(const std::vector& potential, const double factor) +{ + std::vector scaled = potential; + for (double& value: scaled) + { + value *= factor; + } + return scaled; +} + +template +std::vector> scale_potentials(const std::vector>& potentials, + const double factor) +{ + std::vector> scaled = potentials; + for (std::vector& potential: scaled) + { + for (Scalar& value: potential) + { + value *= factor; + } + } + return scaled; +} + +template +void write_abfs_channel_diagnostic(const std::string& filename, const std::vector& channels) +{ + std::ofstream out(filename.c_str(), std::ios::out | std::ios::trunc); + if (!out) + { + throw std::runtime_error("Failed to open Sternheimer ABFS channel diagnostic file: " + filename); + } + out << std::setprecision(16); + out << "# ABACUS Sternheimer ABFS channel diagnostic\n"; + out << "# channel atom atom_local type l radial m label max_abs\n"; + for (const Channel& channel: channels) + { + out << channel.channel_index << ' ' << channel.atom_index << ' ' << channel.atom_local_index << ' ' + << channel.type_index << ' ' << channel.angular_momentum << ' ' << channel.radial_index << ' ' + << channel.magnetic_index << ' ' << channel.label << ' ' << channel.max_abs << '\n'; + } +} + +void write_coulomb_whitening_diagnostic(const std::string& filename, + const SternheimerCoulombWhitening& whitening) +{ + std::ofstream out(filename.c_str(), std::ios::out | std::ios::trunc); + if (!out) + { + throw std::runtime_error("Failed to open Sternheimer Coulomb whitening diagnostic file: " + filename); + } + out << std::setprecision(16); + out << "# ABACUS Sternheimer global full-Coulomb whitening\n"; + out << "raw_auxiliary_dimension " << whitening.raw_dimension << '\n'; + out << "whitened_auxiliary_rank " << whitening.retained_rank << '\n'; + out << "discarded_auxiliary_rank " << whitening.discarded_rank << '\n'; + out << "relative_threshold " << whitening.relative_threshold << '\n'; + out << "largest_eigenvalue " << whitening.largest_eigenvalue << '\n'; + out << "smallest_retained_eigenvalue " << whitening.smallest_retained_eigenvalue << '\n'; + out << "max_orthonormality_error " << whitening.max_orthonormality_error << '\n'; + out << "transform_sha256 " << hash_coulomb_whitening_transform(whitening) << '\n'; + out << "# index eigenvalue retained\n"; + const int first_retained = whitening.discarded_rank; + for (int index = 0; index != whitening.raw_dimension; ++index) + { + out << index << ' ' << whitening.eigenvalues[static_cast(index)] << ' ' + << (index >= first_retained ? "yes" : "no") << '\n'; + } +} + +template +SternheimerRPA::Chi0V1Metadata make_chi0_v1_metadata(const UnitCell& ucell, + const std::vector& channels, + const int iq, + const int ifrequency, + const double omega_ha, + const double weight_ha, + const int output_atom_count = -1) +{ + SternheimerRPA::Chi0V1Metadata metadata; + metadata.iq = iq; + metadata.ifrequency = ifrequency; + metadata.omega = omega_ha; + metadata.weight = weight_ha; + const int atom_count = output_atom_count > 0 ? output_atom_count : ucell.nat; + metadata.atom_naux.assign(static_cast(atom_count), 0); + for (const Channel& channel: channels) + { + if (channel.atom_index < 0 || channel.atom_index >= atom_count) + { + throw std::runtime_error("Sternheimer chi0 found an ABFS channel with invalid atom index."); + } + if (channel.atom_local_index < 0) + { + throw std::runtime_error("Sternheimer chi0 found an ABFS channel with invalid local index."); + } + int& atom_naux = metadata.atom_naux[static_cast(channel.atom_index)]; + atom_naux = std::max(atom_naux, channel.atom_local_index + 1); + } + return metadata; +} + +template +std::vector make_chi0_auxiliary_channels(const std::vector& channels) +{ + std::vector auxiliary_channels; + auxiliary_channels.reserve(channels.size()); + for (const Channel& channel: channels) + { + SternheimerRPA::AuxiliaryChannel auxiliary_channel; + auxiliary_channel.channel_index = channel.channel_index; + auxiliary_channel.atom_index = channel.atom_index; + auxiliary_channel.atom_local_index = channel.atom_local_index; + auxiliary_channels.push_back(auxiliary_channel); + } + return auxiliary_channels; +} + +void write_chi0_index_file(const std::string& filename, + const std::vector>& entries) +{ + std::ofstream out(filename.c_str(), std::ios::out | std::ios::trunc); + if (!out) + { + throw std::runtime_error("Failed to open Sternheimer chi0 index file: " + filename); + } + out << std::setprecision(16); + out << "# ABACUS Sternheimer chi0 v1 index\n"; + out << "format_version 1\n"; + out << "marker " << SternheimerRPA::chi0_v1_marker() << '\n'; + out << "nq 1\n"; + out << "nfreq " << entries.size() << '\n'; + out << "iq ifrequency omega_Ha weight_Ha filename\n"; + for (const auto& entry: entries) + { + const SternheimerRPA::Chi0V1Metadata& metadata = entry.second; + out << metadata.iq << ' ' << metadata.ifrequency << ' ' << metadata.omega << ' ' << metadata.weight << ' ' + << entry.first << '\n'; + } +} + +SternheimerFDZeroOrderStates solve_fd_zero_order_auto(const SternheimerFDHamiltonian& hamiltonian, + const int num_bands, + const double volume_element, + const int max_dense_size, + const int lanczos_max_subspace_size) +{ + if (hamiltonian.grid().size() <= max_dense_size) + { + return solve_sternheimer_fd_zero_order_dense(hamiltonian, num_bands, volume_element, max_dense_size); + } + + SternheimerFDLanczosOptions options; + options.max_subspace_size = lanczos_max_subspace_size; + options.residual_tolerance = 1.0e-8; + options.initial_seed = 1; + return solve_sternheimer_fd_zero_order_lanczos(hamiltonian, num_bands, volume_element, options); +} + +void broadcast_zero_order_states(SternheimerFDZeroOrderStates& states, + const int grid_size, + const bool enabled, + const SternheimerAbacusRuntime& runtime) +{ +#ifdef __MPI + if (!enabled || runtime.nproc <= 1) + { + return; + } + + int num_states = runtime.rank == 0 ? static_cast(states.wavefunctions.size()) : 0; + MPI_Bcast(&num_states, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (num_states < 0) + { + throw std::runtime_error("Invalid Sternheimer zero-order state count broadcast."); + } + + if (runtime.rank != 0) + { + states.eigenvalues.assign(static_cast(num_states), 0.0); + states.residual_norms.assign(static_cast(num_states), 0.0); + states.wavefunctions.assign(static_cast(num_states), + SternheimerFDHamiltonian::Vector(static_cast(grid_size), + SternheimerFDHamiltonian::Complex(0.0, 0.0))); + } + + if (num_states > 0) + { + MPI_Bcast(states.eigenvalues.data(), num_states, MPI_DOUBLE, 0, MPI_COMM_WORLD); + MPI_Bcast(states.residual_norms.data(), num_states, MPI_DOUBLE, 0, MPI_COMM_WORLD); + } + for (int istate = 0; istate != num_states; ++istate) + { + if (static_cast(states.wavefunctions[static_cast(istate)].size()) != grid_size) + { + throw std::runtime_error("Sternheimer zero-order wavefunction size does not match the full grid."); + } + MPI_Bcast(reinterpret_cast(states.wavefunctions[static_cast(istate)].data()), + 2 * grid_size, + MPI_DOUBLE, + 0, + MPI_COMM_WORLD); + } +#else + (void)states; + (void)grid_size; + (void)enabled; +#endif +} + +void reduce_chi0_output_stats(bool& all_converged, + int& solved_equations, + double& max_solver_relative_residual, + double& max_equation_residual_norm, + const bool enabled, + const SternheimerAbacusRuntime& runtime) +{ +#ifdef __MPI + if (!enabled || runtime.nproc <= 1) + { + return; + } + + int converged_flag = all_converged ? 1 : 0; + MPI_Allreduce(MPI_IN_PLACE, &converged_flag, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD); + all_converged = converged_flag != 0; + + MPI_Allreduce(MPI_IN_PLACE, &solved_equations, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD); + + double residuals[2] = {max_solver_relative_residual, max_equation_residual_norm}; + MPI_Allreduce(MPI_IN_PLACE, residuals, 2, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD); + max_solver_relative_residual = residuals[0]; + max_equation_residual_norm = residuals[1]; +#else + (void)all_converged; + (void)solved_equations; + (void)max_solver_relative_residual; + (void)max_equation_residual_norm; + (void)enabled; +#endif +} + +void reduce_kpoint_parallel_complex_vector(std::vector& values, + const bool enabled, + const SternheimerAbacusRuntime& runtime) +{ +#ifdef __MPI + if (!enabled || runtime.nproc <= 1 || values.empty()) + { + return; + } + MPI_Allreduce(MPI_IN_PLACE, + values.data(), + static_cast(values.size()), + MPI_DOUBLE_COMPLEX, + MPI_SUM, + MPI_COMM_WORLD); +#else + (void)values; + (void)enabled; +#endif +} + +void reduce_kpoint_parallel_int_vector(std::vector& values, + const bool enabled, + const SternheimerAbacusRuntime& runtime) +{ +#ifdef __MPI + if (!enabled || runtime.nproc <= 1 || values.empty()) + { + return; + } + MPI_Allreduce(MPI_IN_PLACE, + values.data(), + static_cast(values.size()), + MPI_INT, + MPI_SUM, + MPI_COMM_WORLD); +#else + (void)values; + (void)enabled; +#endif +} + +void reduce_kpoint_parallel_double_vector(std::vector& values, + const bool enabled, + const SternheimerAbacusRuntime& runtime) +{ +#ifdef __MPI + if (!enabled || runtime.nproc <= 1 || values.empty()) + { + return; + } + MPI_Allreduce(MPI_IN_PLACE, + values.data(), + static_cast(values.size()), + MPI_DOUBLE, + MPI_SUM, + MPI_COMM_WORLD); +#else + (void)values; + (void)enabled; +#endif +} + +std::vector occupied_wavefunctions_from_states( + const SternheimerFDZeroOrderStates& states, + const elecstate::ElecState& elec_state, + const int k_index) +{ + std::vector occupied; + for (int ib = 0; ib != static_cast(states.wavefunctions.size()); ++ib) + { + if (elec_state.wg(k_index, ib) > 1.0e-8) + { + occupied.push_back(states.wavefunctions[ib]); + } + } + return occupied; +} + +void run_sternheimer_periodic_lcao_chi0_output(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const LCAO_Orbitals& orbitals, + const std::vector& occupied_kpoints, + const std::array& kmesh, + std::ofstream& out, + const bool use_frequency_mpi, + const int kpoint_groups, + const std::chrono::steady_clock::time_point& chi0_start_time, + const SternheimerOrbitalSet* reusable_rpa_abfs, + const SternheimerAbacusRuntime& runtime) +{ + const auto output_file = [&runtime](const std::string& filename) { + return join_output_path(runtime.input.rpa_outdir, filename); + }; + const bool use_weak_q_unit = env_is_true("ABACUS_STERNHEIMER_WEAK_Q_UNIT"); + if (use_weak_q_unit + && (runtime.nproc != 1 || use_frequency_mpi || kpoint_groups != 1 || runtime.input.sternheimer_channel_mpi + || !runtime.input.sternheimer_delta || runtime.input.sternheimer_virt_max != 0 + || runtime.input.sternheimer_virt_src != "ks_bands" + || (runtime.input.sternheimer_freq_file.empty() + && !env_is_true("ABACUS_STERNHEIMER_WEAK_Q_COULOMB_ONLY")) + || env_is_true("ABACUS_STERNHEIMER_WEAK_RESPONSE_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_WEAK_MATRIX_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_SPECTRUM_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_GALERKIN_AUDIT"))) + throw std::invalid_argument("Weak q unit requires one rank, complete KS target states, a fixed frequency file except for Coulomb-only bootstrap, and no other audit route."); + if (runtime.input.nspin != 1) + { + throw std::runtime_error("The first periodic Sternheimer driver supports only nspin=1 insulators."); + } + if (runtime.input.symmetry != "-1" && runtime.input.symmetry != "1") + { + throw std::runtime_error("Periodic Sternheimer output requires symmetry=-1 or symmetry=1."); + } + + const char* supercell_translation_sum_raw = std::getenv(kSupercellTranslationSumEnv); + const bool use_supercell_translation_sum + = supercell_translation_sum_raw != nullptr && supercell_translation_sum_raw[0] != '\0'; + if (use_weak_q_unit && use_supercell_translation_sum) + throw std::invalid_argument("Weak q unit does not support supercell translation reconstruction."); + const bool full_supercell_response + = use_supercell_translation_sum && env_is_true(kSupercellFullResponseEnv); + if (env_is_true(kSupercellFullResponseEnv) && !use_supercell_translation_sum) + { + throw std::runtime_error( + "Full supercell Sternheimer response requires a translation-sum specification."); + } + if (full_supercell_response && runtime.input.symmetry != "-1") + { + throw std::runtime_error( + "The first full supercell Sternheimer response gate requires symmetry=-1."); + } + + SternheimerSupercellTranslationSum supercell_translation_sum; + std::vector recovered_supercell_kpoints; + int supercell_sector_dimension = 0; + double supercell_sector_kweight = 0.0; + double supercell_sector_max_orthonormality_error = 0.0; + double supercell_sector_max_full_space_residual = 0.0; + if (use_supercell_translation_sum) + { + supercell_translation_sum + = parse_sternheimer_supercell_translation_sum(supercell_translation_sum_raw); + const int expected_atoms + = sternheimer_supercell_primitive_cell_count(supercell_translation_sum) + * supercell_translation_sum.atoms_per_primitive; + constexpr double gamma_tolerance = 1.0e-10; + if (ucell.nat != expected_atoms || occupied_kpoints.size() != 1 + || std::any_of(occupied_kpoints.front().kpoint.begin(), + occupied_kpoints.front().kpoint.end(), + [gamma_tolerance](const double coordinate) { + return std::abs(coordinate) > gamma_tolerance; + })) + { + throw std::runtime_error( + "Supercell translation response requires one Gamma record and a matching translation-major atom list."); + } + if (full_supercell_response) + { + const auto& gamma_record = occupied_kpoints.front(); + std::vector complete_eigenvalues = gamma_record.eigenvalues; + complete_eigenvalues.insert(complete_eigenvalues.end(), + gamma_record.unoccupied_eigenvalues.begin(), + gamma_record.unoccupied_eigenvalues.end()); + std::vector> complete_coefficients + = gamma_record.coefficients; + complete_coefficients.insert(complete_coefficients.end(), + gamma_record.unoccupied_coefficients.begin(), + gamma_record.unoccupied_coefficients.end()); + const auto sectors = recover_all_sternheimer_supercell_sectors( + complete_eigenvalues, + complete_coefficients, + supercell_translation_sum.repeats); + for (const auto& sector: sectors) + { + supercell_sector_dimension + = std::max(supercell_sector_dimension, + static_cast(sector.sector.eigenvalues.size())); + supercell_sector_max_orthonormality_error + = std::max(supercell_sector_max_orthonormality_error, + sector.sector.max_orthonormality_error); + supercell_sector_max_full_space_residual + = std::max(supercell_sector_max_full_space_residual, + sector.sector.max_full_space_residual); + } + recovered_supercell_kpoints + = build_sternheimer_supercell_full_kpoint_records(gamma_record, sectors); + supercell_sector_kweight = recovered_supercell_kpoints.front().kweight; + } + } + + const auto& response_kpoints + = full_supercell_response ? recovered_supercell_kpoints : occupied_kpoints; + const int supercell_primitive_cell_count + = use_supercell_translation_sum + ? sternheimer_supercell_primitive_cell_count(supercell_translation_sum) + : 1; + const double response_matrix_scale + = sternheimer_supercell_response_matrix_scale(full_supercell_response, + supercell_primitive_cell_count); + const std::array response_kmesh + = full_supercell_response ? supercell_translation_sum.repeats : kmesh; + const int response_q_index + = full_supercell_response + ? sternheimer_find_kpoint_one_based(response_kpoints, + supercell_translation_sum.primitive_qpoint) + : runtime.input.sternheimer_q_index; + const SternheimerPeriodicResponsePlan response_plan + = build_sternheimer_periodic_response_plan( + response_kpoints, + response_q_index, + use_supercell_translation_sum && !full_supercell_response); + if (use_weak_q_unit && std::abs(response_plan.qpoint[2]) > 1e-12) + throw std::invalid_argument("Weak q unit requires an in-plane q point."); + if (runtime.input.sternheimer_q_index <= 0) + { + throw std::runtime_error("Internal error: the periodic Sternheimer path requires a positive q index."); + } + validate_sternheimer_periodic_kmesh(response_kmesh, + static_cast(response_kpoints.size())); + const bool use_delta_sternheimer = runtime.input.sternheimer_delta; + const std::array requested_response_dimensions = {runtime.input.sternheimer_response_nx, + runtime.input.sternheimer_response_ny, + runtime.input.sternheimer_response_nz}; + const bool explicit_response_dimensions_requested + = std::any_of(requested_response_dimensions.begin(), + requested_response_dimensions.end(), + [](const int value) { return value != 0; }); + if ((runtime.input.sternheimer_response_ecutwfc > 0.0 || explicit_response_dimensions_requested) + && !use_delta_sternheimer) + { + throw std::runtime_error("An independent Sternheimer response grid requires sternheimer_delta=true."); + } + SternheimerResponseGrid response_grid = make_sternheimer_response_grid(pw_basis, + runtime.input.ecutwfc, + runtime.input.sternheimer_response_ecutwfc, + runtime.input.fft_mode, + requested_response_dimensions, + runtime.input.sternheimer_fd_order); + const char* response_grid_source = sternheimer_response_grid_source_name(response_grid.source); + const double response_grid_ecutwfc + = response_grid.source == SternheimerResponseGridSource::Cutoff + ? runtime.input.sternheimer_response_ecutwfc + : (response_grid.source == SternheimerResponseGridSource::Pbe ? runtime.input.ecutwfc : 0.0); + const ModulePW::PW_Basis& response_pw_basis = *response_grid.basis; + const bool use_kpoint_mpi = kpoint_groups > 1; + const bool use_parallel_grid_mpi = use_frequency_mpi || use_kpoint_mpi; + const SternheimerABACUSFDGridData grid_data + = use_parallel_grid_mpi ? make_sternheimer_fd_full_grid(response_pw_basis) + : make_sternheimer_fd_grid(response_pw_basis); + // Keep PBE/KS symmetry independent from response-equation symmetry. This + // switch makes A/B validation compare the same KS reference on both arms; + // it is also useful when an external driver must disable only response + // representative reduction without changing the SCF calculation. + const bool response_symmetry_disabled = env_is_true("ABACUS_STERNHEIMER_DISABLE_RESPONSE_SYMMETRY"); + const bool use_symmetry_partial_response + = runtime.input.symmetry == "1" && !use_weak_q_unit && !response_symmetry_disabled; + const bool write_kresolved_diagnostic + = !use_weak_q_unit && !use_symmetry_partial_response && env_is_true(kKResolvedDiagnosticEnv); + const bool write_partial_kresolved = use_symmetry_partial_response || write_kresolved_diagnostic; + std::vector canonical_q_indices + = sternheimer_canonical_q_indices_one_based(response_kpoints); + std::vector fixed_q_orbits; + std::vector fixed_q_routes; + std::vector qstar_routes; + int fixed_q_little_group_order = 1; + int fixed_q_discrete_spatial_order = 1; + std::vector fixed_q_representative(response_kpoints.size(), true); + if (use_symmetry_partial_response) + { + std::vector reduced_rotations; + reduced_rotations.reserve(static_cast(ucell.symm.nrotk)); + for (int isym = 0; isym != ucell.symm.nrotk; ++isym) + { + const auto& rotation = ucell.symm.gmatrix[isym]; + reduced_rotations.push_back({{{rotation.e11, rotation.e12, rotation.e13}, + {rotation.e21, rotation.e22, rotation.e23}, + {rotation.e31, rotation.e32, rotation.e33}}}); + } + const auto discrete_spatial_operations + = sternheimer_fd_stencil_symmetry_indices(grid_data.grid, + runtime.input.sternheimer_fd_order, + reduced_rotations); + fixed_q_discrete_spatial_order = static_cast(discrete_spatial_operations.size()); + const auto qstar_permutations = build_sternheimer_discrete_qstar_permutations( + ucell, response_kpoints, discrete_spatial_operations); + qstar_routes = build_sternheimer_qstar_routes_from_permutations( + static_cast(response_kpoints.size()), qstar_permutations); + canonical_q_indices.clear(); + for (const auto& route: qstar_routes) + { + if (route.representative_iq == route.member_iq) + { + canonical_q_indices.push_back(route.representative_iq); + } + } + const auto selected_q_route = std::find_if( + qstar_routes.begin(), qstar_routes.end(), [&](const auto& route) { + return route.member_iq == response_plan.iq; + }); + if (selected_q_route == qstar_routes.end() + || selected_q_route->representative_iq != response_plan.iq) + { + throw std::runtime_error( + "Sternheimer symmetry output requires sternheimer_q_index to select a discrete q-star representative."); + } + const auto permutations = build_sternheimer_fixed_q_little_group_permutations( + ucell, response_kpoints, response_plan.qpoint, discrete_spatial_operations); + fixed_q_little_group_order = static_cast(permutations.size()); + std::vector> index_permutations; + index_permutations.reserve(permutations.size()); + for (const auto& permutation: permutations) + { + index_permutations.push_back(permutation.mapped_index_by_full_k); + } + fixed_q_orbits = build_sternheimer_fixed_q_k_orbits_from_permutations( + static_cast(response_kpoints.size()), index_permutations); + std::fill(fixed_q_representative.begin(), fixed_q_representative.end(), false); + for (const auto& orbit: fixed_q_orbits) + { + fixed_q_representative[static_cast(orbit.representative_ik_full)] = true; + for (const int member_ik_full: orbit.members) + { + const auto route = std::find_if( + permutations.begin(), + permutations.end(), + [&](const auto& operation) { + return operation.mapped_index_by_full_k[ + static_cast(member_ik_full)] + == orbit.representative_ik_full; + }); + if (route == permutations.end()) + { + throw std::runtime_error( + "A Sternheimer fixed-q orbit member has no inverse route to its representative."); + } + fixed_q_routes.push_back( + {response_plan.iq, + orbit.representative_ik_full, + member_ik_full, + route->spatial_isym, + route->time_reversal, + route->fold_G_by_full_k[static_cast(member_ik_full)]}); + } + } + } + const SternheimerKPointSchedule kpoint_schedule(fixed_q_representative, + use_kpoint_mpi ? kpoint_groups : 1); + constexpr double q_tolerance = 1.0e-10; + const bool gamma_qpoint + = std::all_of(response_plan.qpoint.begin(), response_plan.qpoint.end(), [q_tolerance](const double coordinate) { + return std::abs(coordinate) <= q_tolerance; + }); + const double massidda_chi = !gamma_qpoint + ? 0.0 + : (sternheimer_periodic_gamma_uses_2d_massidda( + response_plan.qpoint, runtime.input.exx_ewald_dimension) + ? Singular_Value::cal_massidda_2d(ucell, response_kmesh, 1.0, 5, 1.0e-4) + : Singular_Value::cal_massidda( + ucell, response_kmesh, 2, 1.0, 5, 1.0e-4)); + const double gamma_inverse_k2 + = sternheimer_periodic_gamma_inverse_k2(response_plan.qpoint, + runtime.input.exx_singularity_correction, + massidda_chi); + append_chi0_progress_event("periodic_plan_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "kq_pairs=" + std::to_string(response_plan.kq_pairs.size()), + runtime); + + const double solver_tolerance = positive_double_from_env(kSolverToleranceEnv, 1.0e-8); + const int solver_max_iter = positive_int_from_env(kSolverMaxIterEnv, 300); + const double pca_threshold = nonnegative_double_from_env(kPCAThresholdEnv, runtime.input.exx_pca_threshold); + const double threshold_scale = std::max(1.0, std::abs(runtime.input.exx_pca_threshold)); + const bool reuse_rpa_abfs = reusable_rpa_abfs != nullptr && !reusable_rpa_abfs->empty() + && std::abs(pca_threshold - runtime.input.exx_pca_threshold) <= 1.0e-14 * threshold_scale; + const SternheimerOrbitalSet* periodic_abfs_orbitals = reuse_rpa_abfs ? reusable_rpa_abfs : nullptr; + const double ccp_rmesh_times + = positive_double_from_env(kCCPRmeshTimesEnv, runtime.input.rpa_ccp_rmesh_times); + const int max_channels = positive_int_from_env(kChannelsEnv, -1); + const int max_bands = positive_int_from_env(kBandsEnv, -1); + const int channel_threads = positive_int_from_env(kChannelThreadsEnv, 0); + const int nfreq = runtime.input.sternheimer_nfreq; + const bool use_channel_mpi = runtime.input.sternheimer_channel_mpi; + const bool use_nested_response_mpi = use_frequency_mpi && use_kpoint_mpi; + const SternheimerNestedMPIReplicaLayout nested_replica_layout + = use_nested_response_mpi + ? sternheimer_nested_mpi_replica_layout(kpoint_groups, + nfreq, + runtime.nproc, + runtime.rank, + use_channel_mpi) + : SternheimerNestedMPIReplicaLayout(); + const int response_replicas + = use_nested_response_mpi + ? nested_replica_layout.replicas_per_slot + : (use_frequency_mpi && use_channel_mpi ? runtime.nproc / nfreq : 1); + const int local_channel_replica + = use_nested_response_mpi + ? nested_replica_layout.local_replica + : (use_frequency_mpi && use_channel_mpi ? runtime.rank % response_replicas : 0); + const int local_kpoint_group + = use_kpoint_mpi + ? (use_nested_response_mpi + ? nested_replica_layout.local_response_slot / nfreq + : runtime.rank) + : 0; + const int local_frequency_slot + = use_nested_response_mpi ? nested_replica_layout.local_response_slot % nfreq : 0; + const int default_frequency_rank_shift = use_frequency_mpi && runtime.nproc > 1 ? 1 : 0; + const int frequency_rank_shift = int_from_env(kFrequencyRankShiftEnv, default_frequency_rank_shift); + const SternheimerDeltaABlockMode delta_a_block_mode = delta_a_block_mode_from_env(); + const bool write_delta_components = use_delta_sternheimer && env_is_true(kDeltaComponentDiagnosticEnv); + const bool write_lcao_sos = env_is_true(kLCAOSOSDiagnosticEnv); + const char* wavefunction_diagnostic_raw = std::getenv(kWavefunctionDiagnosticEnv); + const bool write_wavefunction_diagnostic + = wavefunction_diagnostic_raw != nullptr && wavefunction_diagnostic_raw[0] != '\0'; + SternheimerWavefunctionDiagnostic::Configuration wavefunction_diagnostic_config; + if (write_wavefunction_diagnostic) + { + wavefunction_diagnostic_config + = SternheimerWavefunctionDiagnostic::parse_configuration(wavefunction_diagnostic_raw); + } + const bool bands_are_truncated + = max_bands > 0 + && std::any_of(response_kpoints.begin(), + response_kpoints.end(), + [max_bands](const SternheimerLCAOOccupiedKPoint& record) { + return static_cast(record.coefficients.size()) > max_bands; + }); + const bool write_truncated_v1_diagnostic + = bands_are_truncated && env_is_true(kTruncatedV1DiagnosticEnv); + const bool write_periodic_v1 + = sternheimer_write_periodic_v1(use_supercell_translation_sum, + bands_are_truncated, + full_supercell_response, + write_truncated_v1_diagnostic); + validate_sternheimer_periodic_output_mode(write_periodic_v1, write_partial_kresolved); + if (use_symmetry_partial_response + && (write_delta_components || write_lcao_sos || write_wavefunction_diagnostic)) + { + throw std::runtime_error( + "Symmetry-reduced Sternheimer partial output does not yet support component diagnostics."); + } + if (write_lcao_sos) + { + for (const SternheimerLCAOOccupiedKPoint& record: response_kpoints) + { + if (record.unoccupied_coefficients.empty()) + { + throw std::runtime_error( + "Periodic direct LCAO-SOS diagnostic requires gathered unoccupied LCAO states."); + } + } + } + const std::string frequency_grid_file = runtime.input.sternheimer_freq_file; + const SternheimerRPA::TransitionEnergyWindow transition_window + = full_supercell_response + ? transition_window_from_recovered_kpoints(response_kpoints) + : transition_window_from_all_kpoints(elec_state, response_kpoints); + const bool use_frequency_grid_file = !frequency_grid_file.empty(); + const std::string frequency_grid_source = use_frequency_grid_file ? "file" : "greenx_minimax"; + const SternheimerRPA::FrequencyGrid frequency_grid + = use_frequency_grid_file + ? SternheimerRPA::read_frequency_grid_file(frequency_grid_file, nfreq) + : SternheimerRPA::generate_greenx_minimax_frequency_grid(nfreq, + transition_window.emin_ha, + transition_window.emax_ha); + append_chi0_progress_event("frequency_grid_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "nfreq=" + std::to_string(nfreq), + runtime); + + const auto restrict_response_field = [&](const std::vector& fine_values) { + return response_grid.source == SternheimerResponseGridSource::Explicit + ? restrict_sternheimer_real_field_rectangular(*response_grid.serial_fine_basis, + *response_grid.serial_response_basis, + fine_values) + : restrict_sternheimer_real_field(*response_grid.serial_fine_basis, + *response_grid.serial_response_basis, + fine_values); + }; + const std::vector independent_response_full_potential + = response_grid.independent + ? restrict_response_field(copy_sternheimer_full_local_potential(potential, pw_basis, 0)) + : std::vector(); + const std::vector kpoint_parallel_full_potential + = use_kpoint_mpi + ? (response_grid.independent + ? independent_response_full_potential + : copy_sternheimer_full_local_potential(potential, pw_basis, 0)) + : std::vector(); + const std::vector diagnostic_fixed_local_potential + = write_wavefunction_diagnostic + ? (response_grid.independent + ? restrict_response_field(copy_sternheimer_full_fixed_local_potential(potential, pw_basis)) + : (use_kpoint_mpi + ? copy_sternheimer_full_fixed_local_potential(potential, pw_basis) + : copy_sternheimer_fixed_local_potential(potential, pw_basis))) + : std::vector(); + append_chi0_progress_event("full_grid_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "pbe_grid=" + std::to_string(pw_basis.nx) + "x" + + std::to_string(pw_basis.ny) + "x" + std::to_string(pw_basis.nz) + + ",response_grid=" + std::to_string(grid_data.grid.nx) + "x" + + std::to_string(grid_data.grid.ny) + "x" + + std::to_string(grid_data.grid.nz) + ",grid_size=" + + std::to_string(grid_data.grid.size()), + runtime); + if (use_weak_q_unit) + { + run_sternheimer_weak_q_unit(ucell, orbitals, make_sternheimer_fd_full_grid(pw_basis), grid_data, + copy_sternheimer_full_local_potential(potential, pw_basis, 0), response_kpoints, + response_plan, frequency_grid, pca_threshold, periodic_abfs_orbitals, runtime); + out << "status diagnostic_only\nphysical_result no\nfull_q_response no\nreader_v1 no\n" + << (env_is_true("WEAK_Q_PARALLEL_BENCHMARK") + ? "weak_q_benchmark_complete yes\nweak_q_unit_complete no\n" : "weak_q_unit_complete yes\n"); + return; + } + if (env_is_true("ABACUS_STERNHEIMER_WEAK_RESPONSE_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_WEAK_MATRIX_AUDIT")) + { + if (runtime.nproc != 1 || use_supercell_translation_sum || !use_delta_sternheimer + || runtime.input.sternheimer_virt_max != 0 || gamma_qpoint) + throw std::runtime_error("Fine-weak response audit requires one rank, full states, and nonzero q."); + const auto& pair = response_plan.kq_pairs.at(0); + const auto& source = response_kpoints.at(response_plan.record_index_by_global_k.at(pair.source_index)); + const auto& target = response_kpoints.at(response_plan.record_index_by_global_k.at(pair.target_index)); + run_sternheimer_weak_response_audit( + ucell, orbitals, make_sternheimer_fd_full_grid(pw_basis), grid_data, + copy_sternheimer_full_local_potential(potential, pw_basis, 0), source, target, + response_plan.qpoint, frequency_grid.omega_ha.at(0), pca_threshold, periodic_abfs_orbitals, + pair.source_index, pair.target_index, runtime); + out << "status diagnostic_only\nphysical_result no\nweak_response_audit_complete yes\n"; + return; + } + if (env_is_true("ABACUS_STERNHEIMER_SPECTRUM_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_GALERKIN_AUDIT")) + { + if (runtime.nproc != 1 || use_supercell_translation_sum || !use_delta_sternheimer) + { + throw std::runtime_error("Spectrum-only audit requires one MPI rank and the direct periodic Delta route."); + } + const auto& pair = response_plan.kq_pairs.at(0); + const auto& source = response_kpoints.at(response_plan.record_index_by_global_k.at(pair.source_index)); + const auto& target = response_kpoints.at(response_plan.record_index_by_global_k.at(pair.target_index)); + if (env_is_true("ABACUS_STERNHEIMER_GALERKIN_AUDIT")) + { + run_sternheimer_galerkin_spectrum_audit( + ucell, orbitals, make_sternheimer_fd_full_grid(pw_basis), grid_data, + copy_sternheimer_full_local_potential(potential, pw_basis, 0), source, target, + pair.source_index, pair.target_index, runtime); + } + else + { + const auto audit_potential = response_grid.independent + ? independent_response_full_potential + : copy_sternheimer_full_local_potential(potential, pw_basis, 0); + run_sternheimer_spectrum_audit(ucell, orbitals, grid_data, audit_potential, source, target, + pair.source_index, pair.target_index, runtime); + } + out << "status diagnostic_only\n" << "physical_result no\n" + << "response_equations 0\n" << "spectrum_audit_complete yes\n"; + return; + } + append_chi0_progress_event("abfs_source_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + std::string("source=") + (reuse_rpa_abfs ? "rpa_handoff" : "rebuilt"), + runtime); + SternheimerPeriodicABFGridData periodic_abfs; + if (full_supercell_response) + { + if (use_kpoint_mpi) + { + const SternheimerABFSInput collective_abfs_input + = build_abfs_density_input(ucell, pca_threshold, runtime, periodic_abfs_orbitals); + if (runtime.rank == 0) + { + periodic_abfs = build_supercell_translation_full_response_abfs_from_input( + collective_abfs_input, + grid_data.grid, + supercell_translation_sum, + max_channels); + } + } + else + { + periodic_abfs = build_supercell_translation_full_response_abfs(ucell, + grid_data.grid, + supercell_translation_sum, + max_channels, + pca_threshold, + periodic_abfs_orbitals, + runtime); + } + broadcast_periodic_abfs(periodic_abfs, grid_data.grid.size(), use_kpoint_mpi, runtime); + } + else + { + periodic_abfs = build_abfs_full_coulomb_bloch_grid_channels(ucell, + grid_data.grid, + response_plan.qpoint, + gamma_inverse_k2, + max_channels, + pca_threshold, + periodic_abfs_orbitals, + chi0_start_time, + runtime); + } + if (use_supercell_translation_sum && !full_supercell_response) + { + if (!periodic_abfs.densities.empty()) + { + periodic_abfs.densities = { + combine_sternheimer_supercell_translation_channel(periodic_abfs.densities, supercell_translation_sum)}; + } + periodic_abfs.potentials = {combine_sternheimer_supercell_translation_channel( + periodic_abfs.potentials, supercell_translation_sum)}; + } + if (periodic_abfs.potentials.empty()) + { + throw std::runtime_error("No periodic ABFS full-Coulomb perturbation channels were generated."); + } + const int num_channels = static_cast(periodic_abfs.potentials.size()); + const int channel_batch_width = use_delta_sternheimer ? sternheimer_channel_batch_width() : 1; + const SternheimerMemorySnapshot channel_memory = detect_sternheimer_memory_snapshot(); + SternheimerChannelWorkerPlan channel_worker_plan + = plan_sternheimer_channel_workers(num_channels, + sternheimer_channel_openmp_threads(), + grid_data.grid.size(), + channel_threads, + channel_memory, + channel_batch_width); + append_chi0_progress_event( + "channel_workers_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "global_channels=" + std::to_string(num_channels) + + " local_channels=" + std::to_string(num_channels) + " " + + format_sternheimer_channel_worker_diagnostic(channel_memory, + channel_worker_plan, + grid_data.grid.size(), + channel_threads), + runtime); + const int output_atom_count + = full_supercell_response ? supercell_translation_sum.atoms_per_primitive : ucell.nat; + double gamma_projection_relative_error = -1.0; + const bool evaluate_gamma_projection + = gamma_qpoint && !use_supercell_translation_sum && !periodic_abfs.densities.empty(); + if (evaluate_gamma_projection) + { + const auto target = SternheimerRPA::read_coulomb_v1_files( + find_coulomb_v1_rank_files(response_plan.iq, runtime.nproc)); + if (target.iq != response_plan.iq + || target.atom_naux != atom_auxiliary_sizes(periodic_abfs.potentials, ucell.nat)) + { + throw std::runtime_error("Periodic Gamma Sternheimer full-Coulomb v1 metadata do not match the grid ABFS."); + } + gamma_projection_relative_error + = compare_sternheimer_periodic_coulomb_projection(periodic_abfs.densities, + periodic_abfs.potentials, + target.values, + grid_data.volume_element) + .relative_error; + } + const std::vector& channels = periodic_abfs.potentials; + append_chi0_progress_event("channels_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "channels=" + std::to_string(num_channels), + runtime); + if (runtime.rank == 0) + { + write_abfs_channel_diagnostic("STERNHEIMER_ABFS_CHANNELS.dat", channels); + if (sternheimer_grid_coulomb_diagnostic_enabled(num_channels)) + { + write_sternheimer_grid_coulomb_diagnostic("STERNHEIMER_GRID_COULOMB.dat", + periodic_abfs.densities, + periodic_abfs.potentials, + grid_data.volume_element); + if (gamma_qpoint && !use_supercell_translation_sum) + { + const std::vector body_potentials + = solve_sternheimer_abf_periodic_full_coulomb( + periodic_abfs.densities, grid_data.grid, response_plan.qpoint, 0.0); + write_sternheimer_grid_coulomb_diagnostic("STERNHEIMER_GRID_COULOMB_BODY.dat", + periodic_abfs.densities, + body_potentials, + grid_data.volume_element); + } + } + if (sternheimer_abfs_diag_only_enabled()) + { + out << "status abfs_diag_only\n"; + out << "sternheimer_q_index " << response_plan.iq << '\n'; + out << "sternheimer_qpoint " << response_plan.qpoint[0] << ' ' << response_plan.qpoint[1] << ' ' + << response_plan.qpoint[2] << '\n'; + out << "grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' << grid_data.grid.nz + << " size " << grid_data.grid.size() << " dV " << grid_data.volume_element << '\n'; + out << "pbe_ecutwfc_Ry " << runtime.input.ecutwfc << '\n'; + out << "sternheimer_response_ecutwfc_requested_Ry " + << runtime.input.sternheimer_response_ecutwfc << '\n'; + out << "sternheimer_response_ecutwfc_Ry " << response_grid_ecutwfc << '\n'; + out << "sternheimer_response_grid_requested " << response_grid.requested_dimensions[0] << ' ' + << response_grid.requested_dimensions[1] << ' ' << response_grid.requested_dimensions[2] << '\n'; + out << "sternheimer_response_grid_source " << response_grid_source << '\n'; + out << "pbe_grid " << pw_basis.nx << ' ' << pw_basis.ny << ' ' << pw_basis.nz << " size " + << pw_basis.nxyz << '\n'; + out << "response_grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' + << grid_data.grid.nz << " size " << grid_data.grid.size() << '\n'; + out << "abfs_channels " << num_channels << '\n'; + out << "perturbation_coulomb_kernel full_periodic_poisson\n"; + out << "periodic_kmesh " << response_kmesh[0] << ' ' << response_kmesh[1] << ' ' + << response_kmesh[2] << '\n'; + out << "periodic_gamma_massidda_chi " << massidda_chi << '\n'; + out << "periodic_gamma_coulomb_projection " + << (use_supercell_translation_sum ? "skipped_supercell_translation_sum" + : (evaluate_gamma_projection ? "diagnostic_only_physical_poisson" + : "skipped_large_auxiliary_dimension")) + << '\n'; + out << "periodic_gamma_projection_relative_error " << gamma_projection_relative_error << '\n'; + out << "periodic_gamma_limit constant_mode_only_no_headwing\n"; + out << "perturbation_ccp_rmesh_times_used no\n"; + } + } + if (sternheimer_abfs_diag_only_enabled()) + { + return; + } + + const std::vector auxiliary_channels + = make_chi0_auxiliary_channels(channels); + + const auto response_owner_rank = [&](const int source_kpoint_index, + const int ifrequency) { + if (use_nested_response_mpi) + { + const auto assignment = kpoint_schedule.assignment( + source_kpoint_index, + ifrequency, + nfreq, + kpoint_groups * nfreq, + frequency_rank_shift); + return assignment.owner_rank * response_replicas; + } + if (use_kpoint_mpi) + { + return kpoint_schedule.owner_group(source_kpoint_index); + } + if (use_frequency_mpi && use_channel_mpi) + { + return SternheimerRPA::frequency_mpi_assignment(ifrequency, + nfreq, + runtime.nproc, + runtime.rank, + frequency_rank_shift, + true) + .frequency_leader_rank; + } + return SternheimerRPA::frequency_owner_rank(ifrequency, + runtime.nproc, + frequency_rank_shift); + }; + const auto response_owned_by_rank = [&](const int source_kpoint_index, + const int ifrequency) { + const int leader = response_owner_rank(source_kpoint_index, ifrequency); + return runtime.rank >= leader && runtime.rank < leader + response_replicas; + }; + +#ifdef __MPI + MPI_Comm response_group_comm = MPI_COMM_NULL; + if (response_replicas > 1) + { + const int response_group_color = runtime.rank / response_replicas; + MPI_Comm_split(MPI_COMM_WORLD, + response_group_color, + local_channel_replica, + &response_group_comm); + } +#endif + + std::vector frequency_owners(static_cast(nfreq), 0); + std::vector> chi0_branches(static_cast(nfreq)); + std::vector> delta_sos_branches(static_cast(nfreq)); + std::vector> delta_pulay_branches(static_cast(nfreq)); + std::vector> delta_out_branches(static_cast(nfreq)); + std::vector> lcao_sos_branches(static_cast(nfreq)); + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const int owner_rank = use_frequency_mpi && !use_kpoint_mpi + ? response_owner_rank(0, ifrequency) + : 0; + frequency_owners[static_cast(ifrequency)] = owner_rank; + if (use_kpoint_mpi || response_owned_by_rank(0, ifrequency)) + { + chi0_branches[static_cast(ifrequency)].assign( + static_cast(num_channels) * static_cast(num_channels), + SternheimerRPA::Complex(0.0, 0.0)); + if (write_delta_components) + { + delta_sos_branches[static_cast(ifrequency)] + = chi0_branches[static_cast(ifrequency)]; + delta_pulay_branches[static_cast(ifrequency)] + = chi0_branches[static_cast(ifrequency)]; + delta_out_branches[static_cast(ifrequency)] + = chi0_branches[static_cast(ifrequency)]; + } + if (write_lcao_sos) + { + lcao_sos_branches[static_cast(ifrequency)] + = chi0_branches[static_cast(ifrequency)]; + } + } + } + SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = solver_max_iter; + solver_options.residual_tol = solver_tolerance; + solver_options.use_fd_spectral_preconditioner + = sternheimer_environment_flag(kSpectralPreconditionerEnv, true); + solver_options.fd_spectral_preconditioner_regularization + = nonnegative_double_from_env(kSpectralPreconditionerRegularizationEnv, 0.0); + SternheimerDeltaSubspaceOptions delta_options; + delta_options.max_virtual_states = runtime.input.sternheimer_virt_max; + delta_options.norm_tolerance = runtime.input.sternheimer_vnorm_tol; + + bool all_converged = true; + int solved_equations = 0; + double max_solver_relative_residual = 0.0; + double max_equation_residual_norm = 0.0; + double max_full_grid_equation_residual_norm = 0.0; + std::vector target_projector_dimensions(response_plan.kq_pairs.size(), 0); + std::vector target_delta_dimensions(response_plan.kq_pairs.size(), 0); + std::vector target_delta_eigenvalue_min(response_plan.kq_pairs.size(), 0.0); + std::vector target_delta_eigenvalue_max(response_plan.kq_pairs.size(), 0.0); + std::vector target_delta_grid_hamiltonian_relative_difference(response_plan.kq_pairs.size(), 0.0); + std::vector target_delta_grid_hamiltonian_max_abs_difference(response_plan.kq_pairs.size(), 0.0); + std::vector target_lcao_unoccupied_min(response_plan.kq_pairs.size(), 0.0); + std::vector target_lcao_unoccupied_max(response_plan.kq_pairs.size(), 0.0); + std::vector target_lcao_occupied_raw_norm_min(response_plan.kq_pairs.size(), 0.0); + std::vector target_lcao_occupied_raw_norm_max(response_plan.kq_pairs.size(), 0.0); + std::vector target_lcao_unoccupied_raw_norm_min(response_plan.kq_pairs.size(), 0.0); + std::vector target_lcao_unoccupied_raw_norm_max(response_plan.kq_pairs.size(), 0.0); + std::vector target_lcao_occ_unocc_overlap_max(response_plan.kq_pairs.size(), 0.0); + int local_wavefunction_diagnostic_count = 0; + + const std::vector owned_pair_indices + = kpoint_schedule.owned_pair_indices(response_plan, use_kpoint_mpi ? local_kpoint_group : 0); + std::vector local_partial_records; + for (const std::size_t pair_index: owned_pair_indices) + { + const SternheimerKQPair& pair = response_plan.kq_pairs[pair_index]; + const int source_record_index + = response_plan.record_index_by_global_k[static_cast(pair.source_index)]; + const int target_record_index + = response_plan.record_index_by_global_k[static_cast(pair.target_index)]; + const SternheimerLCAOOccupiedKPoint& full_source_record + = response_kpoints[static_cast(source_record_index)]; + const SternheimerLCAOOccupiedKPoint& full_target_record + = response_kpoints[static_cast(target_record_index)]; + SternheimerLCAOOccupiedKPoint source_sector_record; + SternheimerLCAOOccupiedKPoint target_sector_record; + const SternheimerLCAOOccupiedKPoint* source_record_pointer = &full_source_record; + const SternheimerLCAOOccupiedKPoint* target_record_pointer = &full_target_record; + if (use_supercell_translation_sum && !full_supercell_response) + { + const int cell_count + = sternheimer_supercell_primitive_cell_count(supercell_translation_sum); + if (full_source_record.coefficients.size() % static_cast(cell_count) != 0 + || full_target_record.coefficients.size() % static_cast(cell_count) != 0) + { + throw std::runtime_error( + "Supercell translation-sector occupied bands do not divide evenly over primitive cells."); + } + const int source_occupied_count + = static_cast(full_source_record.coefficients.size()) / cell_count; + const int target_occupied_count + = static_cast(full_target_record.coefficients.size()) / cell_count; + auto recover_record + = [&](const SternheimerLCAOOccupiedKPoint& full_record, + const SternheimerReducedKPoint& primitive_kpoint, + const int occupied_count, + SternheimerLCAOOccupiedKPoint& sector_record) { + std::vector complete_eigenvalues = full_record.eigenvalues; + complete_eigenvalues.insert(complete_eigenvalues.end(), + full_record.unoccupied_eigenvalues.begin(), + full_record.unoccupied_eigenvalues.end()); + std::vector> complete_coefficients + = full_record.coefficients; + complete_coefficients.insert(complete_coefficients.end(), + full_record.unoccupied_coefficients.begin(), + full_record.unoccupied_coefficients.end()); + const SternheimerSupercellSector sector + = recover_sternheimer_supercell_sector(complete_eigenvalues, + complete_coefficients, + supercell_translation_sum.repeats, + primitive_kpoint); + supercell_sector_dimension = static_cast(sector.eigenvalues.size()); + supercell_sector_max_orthonormality_error + = std::max(supercell_sector_max_orthonormality_error, + sector.max_orthonormality_error); + supercell_sector_max_full_space_residual + = std::max(supercell_sector_max_full_space_residual, + sector.max_full_space_residual); + if (occupied_count <= 0 + || occupied_count >= static_cast(sector.eigenvalues.size()) + || full_record.occupations.empty()) + { + throw std::runtime_error( + "Supercell translation-sector recovery found an invalid occupied dimension."); + } + validate_sternheimer_supercell_sector_occupations( + full_record.occupations, + static_cast(full_record.coefficients.size())); + sector_record = full_record; + sector_record.kweight + = sternheimer_supercell_sector_kweight(full_record.kweight, cell_count); + supercell_sector_kweight = sector_record.kweight; + sector_record.eigenvalues.assign(sector.eigenvalues.begin(), + sector.eigenvalues.begin() + occupied_count); + sector_record.coefficients.assign(sector.coefficients.begin(), + sector.coefficients.begin() + occupied_count); + sector_record.occupations.assign( + static_cast(occupied_count), + full_record.occupations.front()); + sector_record.unoccupied_eigenvalues.assign( + sector.eigenvalues.begin() + occupied_count, + sector.eigenvalues.end()); + sector_record.unoccupied_coefficients.assign( + sector.coefficients.begin() + occupied_count, + sector.coefficients.end()); + append_chi0_progress_event( + "supercell_sector_ready", + 0, + -1, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "primitive_k=" + std::to_string(primitive_kpoint[0]) + ":" + + std::to_string(primitive_kpoint[1]) + ":" + + std::to_string(primitive_kpoint[2]) + + " dimension=" + std::to_string(sector.eigenvalues.size()) + + " orth_error=" + + precise_double_string(sector.max_orthonormality_error) + + " residual=" + + precise_double_string(sector.max_full_space_residual), runtime); + }; + recover_record(full_source_record, + SternheimerReducedKPoint{0.0, 0.0, 0.0}, + source_occupied_count, + source_sector_record); + recover_record(full_target_record, + supercell_translation_sum.primitive_qpoint, + target_occupied_count, + target_sector_record); + source_record_pointer = &source_sector_record; + target_record_pointer = &target_sector_record; + } + const SternheimerLCAOOccupiedKPoint& source_record = *source_record_pointer; + const SternheimerLCAOOccupiedKPoint& target_record = *target_record_pointer; + const SternheimerLCAOSamplingPlan sampling_plan = sternheimer_lcao_sampling_plan( + use_delta_sternheimer, + write_lcao_sos, + !target_record.unoccupied_coefficients.empty()); + + std::vector> partial_pair_branches; + if (write_partial_kresolved) + { + partial_pair_branches.resize(static_cast(nfreq)); + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + if (response_owned_by_rank(pair.source_index, ifrequency)) + { + partial_pair_branches[static_cast(ifrequency)].assign( + static_cast(num_channels) * num_channels, + SternheimerRPA::Complex(0.0, 0.0)); + } + } + } + + append_chi0_progress_event("kpair_start", + 0, + -1, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "source_k=" + std::to_string(pair.source_index + 1) + + ",target_k=" + std::to_string(pair.target_index + 1), runtime); + + const bool reuse_target_sampling + = can_reuse_sternheimer_target_lcao_sampling(source_record_pointer == target_record_pointer, sampling_plan); + std::unique_ptr sampled_source; + if (!reuse_target_sampling) + { + sampled_source = std::make_unique( + sample_sternheimer_lcao_kpoint(ucell, + grid_data.grid, + orbitals, + source_record, + max_bands, + sampling_plan.sample_source_unoccupied, + write_lcao_sos ? 0 : delta_options.max_virtual_states, + grid_data.volume_element, + runtime.input.sternheimer_vnorm_tol)); + } + SternheimerSampledLCAOKPoint target + = sample_sternheimer_lcao_kpoint(ucell, + grid_data.grid, + orbitals, + target_record, + 0, + sampling_plan.sample_target_unoccupied, + write_lcao_sos ? 0 : delta_options.max_virtual_states, + grid_data.volume_element, + runtime.input.sternheimer_vnorm_tol); + const SternheimerSampledLCAOKPoint& source = reuse_target_sampling ? target : *sampled_source; + append_chi0_progress_event("lcao_sampling_ready", + 0, + -1, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + std::string("source_reused=") + (reuse_target_sampling ? "yes" : "no") + + " target_unoccupied=" + + std::to_string(target.unoccupied_functions.size()), runtime); + const SternheimerFDHamiltonian hamiltonian = [&]() { + if (response_grid.independent || use_kpoint_mpi) + { + SternheimerABACUSFDGridData target_grid_data = grid_data; + target_grid_data.grid.kpoint = sternheimer_lcao_grid_kpoint(target_record); + auto nonlocal_projector = make_sternheimer_fd_nonlocal_projector_from_unitcell( + ucell, target_grid_data.grid, target_grid_data.volume_element); + return make_sternheimer_fd_hamiltonian_from_local_potential(target_grid_data, + response_grid.independent + ? independent_response_full_potential + : kpoint_parallel_full_potential, + 1.0, + std::move(nonlocal_projector), + runtime.input.sternheimer_fd_order); + } + return use_frequency_mpi + ? make_sternheimer_fd_full_hamiltonian( + potential, + pw_basis, + ucell, + 0, + 1.0, + sternheimer_lcao_grid_kpoint(target_record), + runtime.input.sternheimer_fd_order) + : make_sternheimer_fd_hamiltonian( + potential, + pw_basis, + ucell, + 0, + 1.0, + sternheimer_lcao_grid_kpoint(target_record), + runtime.input.sternheimer_fd_order); + }(); + + std::vector target_occupied_projector; + target_occupied_projector.reserve(target.occupied_projector_functions.size()); + for (const SternheimerDeltaGridFunction& function: target.occupied_projector_functions) + { + target_occupied_projector.push_back(function.values); + } + SternheimerDeltaSubspace delta_subspace; + std::unique_ptr pair_projectors; + SternheimerChannelWorkerPlan pair_channel_worker_plan = channel_worker_plan; + target_projector_dimensions[pair_index] = static_cast(target_occupied_projector.size()); + if (use_delta_sternheimer) + { + std::vector delta_candidates; + int candidate_occupied_count = 0; + if (sampling_plan.build_target_ao_candidates) + { + delta_candidates = build_lcao_candidate_grid_functions( + ucell, + grid_data.grid, + runtime, + &orbitals, + sternheimer_lcao_grid_kpoint(target_record)); + candidate_occupied_count = static_cast(target_occupied_projector.size()); + } + else if (write_lcao_sos) + { + delta_candidates = target.unoccupied_functions; + } + else + { + delta_candidates = std::move(target.unoccupied_functions); + } + SternheimerDeltaSubspaceOptions pair_delta_options = delta_options; + pair_delta_options.max_virtual_states = sternheimer_delta_virtual_state_limit( + delta_options.max_virtual_states, + static_cast(delta_candidates.size()), + candidate_occupied_count); + pair_delta_options.retain_grid_functions = false; + pair_delta_options.evaluate_full_grid_difference = false; + delta_subspace = build_delta_sternheimer_subspace_by_mode(hamiltonian, + target.occupied_projector_functions, + std::move(delta_candidates), + grid_data.volume_element, + pair_delta_options, + delta_a_block_mode); + std::ostringstream excitation_context; + excitation_context << std::setprecision(17) + << "q_index=" << response_plan.iq + << " source_k=" << pair.source_index + 1 + << " target_k=" << pair.target_index + 1 + << " rank=" << runtime.rank + << " response_grid=" << grid_data.grid.nx << 'x' << grid_data.grid.ny + << 'x' << grid_data.grid.nz + << " pbe_grid=" << pw_basis.nx << 'x' << pw_basis.ny << 'x' << pw_basis.nz + << " a_block=" << sternheimer_delta_a_block_mode_name(delta_a_block_mode); + for (const auto* record : {&source_record, &target_record}) + { + const auto& k = sternheimer_lcao_grid_kpoint(*record); + excitation_context << " k_reduced=" << k[0] << ',' << k[1] << ',' << k[2]; + } + require_positive_periodic_delta_excitations( + delta_subspace.virtual_states, source_record.eigenvalues, excitation_context.str()); + target_delta_dimensions[pair_index] = static_cast(delta_subspace.virtual_states.size()); + const auto delta_minmax = std::minmax_element( + delta_subspace.virtual_states.begin(), + delta_subspace.virtual_states.end(), + [](const SternheimerDeltaVirtualState& lhs, const SternheimerDeltaVirtualState& rhs) { + return lhs.eigenvalue < rhs.eigenvalue; + }); + target_delta_eigenvalue_min[pair_index] = delta_minmax.first->eigenvalue; + target_delta_eigenvalue_max[pair_index] = delta_minmax.second->eigenvalue; + target_delta_grid_hamiltonian_relative_difference[pair_index] + = delta_subspace.full_grid_hamiltonian_relative_difference; + target_delta_grid_hamiltonian_max_abs_difference[pair_index] + = delta_subspace.full_grid_hamiltonian_max_abs_difference; + + append_chi0_progress_event("delta_subspace_ready", + 0, + -1, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "nvirtual=" + + std::to_string(delta_subspace.virtual_states.size()), runtime); + + std::vector().swap(target.occupied_functions); + std::vector().swap(target.occupied_projector_functions); + if (!write_lcao_sos) + { + target.unoccupied_states = SternheimerFDZeroOrderStates(); + } + const auto memory_before_trim = detect_sternheimer_memory_snapshot(); + const auto trim_status = trim_sternheimer_process_heap(); + const auto memory_after_trim = detect_sternheimer_memory_snapshot(); + bool pack_projectors = channel_batch_width > 1; + std::uint64_t shared_projector_bytes + = SternheimerPeriodicResponseProjectors::estimated_storage_bytes( + grid_data.grid.size(), target_occupied_projector.size(), delta_subspace.virtual_states.size(), + pack_projectors); + append_chi0_progress_event( + "delta_memory_reclaimed", 0, -1, -1, -1, solved_equations, nullptr, -1.0, + elapsed_seconds_since(chi0_start_time), + "resource_source=" + memory_after_trim.source + + " accounting_mode=" + sternheimer_memory_accounting_mode_name(memory_after_trim.mode) + + " memory_before_trim_bytes=" + std::to_string(memory_before_trim.current_bytes) + + " memory_after_trim_bytes=" + std::to_string(memory_after_trim.current_bytes) + + " node_memory_limit_bytes=" + std::to_string(memory_after_trim.limit_bytes) + + " trim_status=" + std::to_string(static_cast(trim_status)) + + " shared_projector_bytes=" + std::to_string(shared_projector_bytes), runtime); + // Admit the persistent packed projectors before allocating them, then + // measure them as baseline rather than duplicating them in each worker. + if (pack_projectors) + { + try + { + const auto admission = plan_sternheimer_channel_workers( + num_channels, sternheimer_channel_openmp_threads(), grid_data.grid.size(), channel_threads, + reserve_sternheimer_shared_memory(memory_after_trim, shared_projector_bytes), channel_batch_width); + pack_projectors = admission.channel_batch_width > 1; + } + catch (const std::runtime_error&) + { + // A scalar solve borrows the same complete basis without packing it. + // It still has to pass the unchanged admission check below. + pack_projectors = false; + } + } + shared_projector_bytes = SternheimerPeriodicResponseProjectors::estimated_storage_bytes( + grid_data.grid.size(), target_occupied_projector.size(), delta_subspace.virtual_states.size(), + pack_projectors); + const int admitted_batch_width = pack_projectors ? channel_batch_width : 1; + plan_sternheimer_channel_workers( + num_channels, sternheimer_channel_openmp_threads(), grid_data.grid.size(), channel_threads, + reserve_sternheimer_shared_memory(memory_after_trim, shared_projector_bytes), admitted_batch_width); + pair_projectors = std::make_unique( + target_occupied_projector, delta_subspace.virtual_states, grid_data.volume_element, pack_projectors); + trim_sternheimer_process_heap(); + SternheimerMemorySnapshot pair_channel_memory = detect_sternheimer_memory_snapshot(); + append_chi0_progress_event( + "shared_projectors_ready", 0, -1, -1, -1, solved_equations, nullptr, -1.0, + elapsed_seconds_since(chi0_start_time), + "memory_current_bytes=" + std::to_string(pair_channel_memory.current_bytes) + + " node_memory_limit_bytes=" + std::to_string(pair_channel_memory.limit_bytes) + + " shared_projector_bytes=" + std::to_string(shared_projector_bytes) + + " owned_projector_bytes=" + + std::to_string(pair_projectors->occupied.storage_bytes() + pair_projectors->fixed.storage_bytes()), runtime); + pair_channel_worker_plan + = plan_sternheimer_channel_workers(num_channels, + sternheimer_channel_openmp_threads(), + grid_data.grid.size(), + channel_threads, + pair_channel_memory, + admitted_batch_width); + if (pack_projectors && pair_channel_worker_plan.channel_batch_width == 1) + { + pair_projectors.reset(); + trim_sternheimer_process_heap(); + pair_projectors = std::make_unique( + target_occupied_projector, delta_subspace.virtual_states, grid_data.volume_element, false); + pair_channel_memory = detect_sternheimer_memory_snapshot(); + pair_channel_worker_plan = plan_sternheimer_channel_workers( + num_channels, sternheimer_channel_openmp_threads(), grid_data.grid.size(), channel_threads, + pair_channel_memory, 1); + } + channel_worker_plan = pair_channel_worker_plan; + append_chi0_progress_event( + "equation_workers_ready", + 0, + -1, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + format_sternheimer_channel_worker_diagnostic(pair_channel_memory, + pair_channel_worker_plan, + grid_data.grid.size(), + channel_threads), runtime); + } + const int pair_channel_batch_width = pair_channel_worker_plan.channel_batch_width; + + if (!target_record.unoccupied_eigenvalues.empty()) + { + const auto lcao_unoccupied_minmax + = std::minmax_element(target_record.unoccupied_eigenvalues.begin(), + target_record.unoccupied_eigenvalues.end()); + target_lcao_unoccupied_min[pair_index] = *lcao_unoccupied_minmax.first; + target_lcao_unoccupied_max[pair_index] = *lcao_unoccupied_minmax.second; + } + else + { + const int target_occupied_count = static_cast(target.states.eigenvalues.size()); + if (target_occupied_count >= elec_state.ekb.nc) + { + throw std::runtime_error("Periodic Sternheimer target k point has no LCAO unoccupied states."); + } + double lcao_unoccupied_min = std::numeric_limits::infinity(); + double lcao_unoccupied_max = -std::numeric_limits::infinity(); + for (int ib = target_occupied_count; ib != elec_state.ekb.nc; ++ib) + { + const double eigenvalue = elec_state.ekb(target_record.zero_order_k_index, ib); + lcao_unoccupied_min = std::min(lcao_unoccupied_min, eigenvalue); + lcao_unoccupied_max = std::max(lcao_unoccupied_max, eigenvalue); + } + target_lcao_unoccupied_min[pair_index] = lcao_unoccupied_min; + target_lcao_unoccupied_max[pair_index] = lcao_unoccupied_max; + } + target_lcao_occupied_raw_norm_min[pair_index] = target.occupied_raw_norm_min; + target_lcao_occupied_raw_norm_max[pair_index] = target.occupied_raw_norm_max; + + std::vector lcao_virtual_states; + if (write_lcao_sos) + { + lcao_virtual_states.reserve(target.unoccupied_functions.size()); + for (std::size_t ia = 0; ia != target.unoccupied_functions.size(); ++ia) + { + SternheimerDeltaVirtualState state; + state.orbital = target.unoccupied_functions[ia].values; + state.residual.assign(state.orbital.size(), SternheimerFDHamiltonian::Complex(0.0, 0.0)); + state.eigenvalue = target.unoccupied_states.eigenvalues[ia]; + lcao_virtual_states.push_back(std::move(state)); + } + target_lcao_unoccupied_raw_norm_min[pair_index] = target.unoccupied_raw_norm_min; + target_lcao_unoccupied_raw_norm_max[pair_index] = target.unoccupied_raw_norm_max; + double max_overlap = 0.0; + for (const auto& occupied: target.states.wavefunctions) + { + for (const auto& virtual_state: lcao_virtual_states) + { + max_overlap = std::max( + max_overlap, + std::abs(sternheimer_fd_grid_dot(occupied, + virtual_state.orbital, + grid_data.volume_element))); + } + } + target_lcao_occ_unocc_overlap_max[pair_index] = max_overlap; + } + + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const int owner_rank = response_owner_rank(pair.source_index, ifrequency); + if (!response_owned_by_rank(pair.source_index, ifrequency)) + { + continue; + } + const double omega_ry = 2.0 * frequency_grid.omega_ha[static_cast(ifrequency)]; + std::vector& chi0_branch + = write_partial_kresolved + ? partial_pair_branches[static_cast(ifrequency)] + : chi0_branches[static_cast(ifrequency)]; + std::vector* delta_sos_branch + = write_delta_components ? &delta_sos_branches[static_cast(ifrequency)] : nullptr; + std::vector* delta_pulay_branch + = write_delta_components ? &delta_pulay_branches[static_cast(ifrequency)] : nullptr; + std::vector* delta_out_branch + = write_delta_components ? &delta_out_branches[static_cast(ifrequency)] : nullptr; + std::vector* lcao_sos_branch + = write_lcao_sos ? &lcao_sos_branches[static_cast(ifrequency)] : nullptr; + + const int source_band_count = sternheimer_periodic_band_count( + static_cast(source.states.wavefunctions.size()), max_bands); + for (int ib = 0; ib != source_band_count; ++ib) + { + const double occupation = sternheimer_lcao_weighted_occupation(source_record, ib); + const double matrix_occupation = response_matrix_scale * occupation; + struct PeriodicChannelEquationResult + { + int channel_index = -1; + SternheimerRPA::SolverResult solver; + double equation_residual_norm = 0.0; + double full_grid_equation_residual_norm = 0.0; + bool has_wavefunction_diagnostic = false; + SternheimerWavefunctionDiagnostic::Record wavefunction_diagnostic; + }; + const std::vector channel_batches + = make_sternheimer_owned_channel_batches(num_channels, channel_batch_width, + pair_channel_batch_width, response_replicas, + local_channel_replica, ib); + const int solved_equations_before_band = solved_equations; + std::atomic completed_batch_equations{0}; + std::vector> grouped_channel_results + = run_sternheimer_channel_tasks>( + static_cast(channel_batches.size()), + [&](const int batch_task) { + const SternheimerChannelBatch batch + = channel_batches[static_cast(batch_task)]; + SternheimerFDHamiltonian::Matrix rhs_batch(static_cast(batch.size)); + std::vector batch_perturbations_ry( + static_cast(batch.size)); + std::vector> + perturbation_matrix_elements(static_cast(batch.size)); + for (int offset = 0; offset != batch.size; ++offset) + { + const int ichannel = batch.begin + offset; + const std::size_t channel_index = static_cast(ichannel); + auto& perturbation_ry = batch_perturbations_ry[static_cast(offset)]; + perturbation_ry = channels[channel_index].potential_r; + for (auto& value: perturbation_ry) + { + value *= kHartreeToRydberg; + } + SternheimerRPA::build_rhs_from_hartree_perturbation( + perturbation_ry, + source.states.wavefunctions[ib], + rhs_batch[static_cast(offset)]); + if (use_delta_sternheimer) + { + perturbation_matrix_elements[static_cast(offset)] + = delta_sternheimer_perturbation_matrix_elements( + delta_subspace.virtual_states, + perturbation_ry, + source.states.wavefunctions[ib], + grid_data.volume_element); + } + } + std::vector responses; + if (pair_channel_batch_width == 1 || !use_delta_sternheimer) + { + responses.push_back(solve_sternheimer_periodic_linear_response( + use_delta_sternheimer, + hamiltonian, + target_occupied_projector, + source.states.eigenvalues[ib], + rhs_batch.front(), + delta_subspace.virtual_states, + perturbation_matrix_elements.front(), + omega_ry, + grid_data.volume_element, + solver_options, + pair_projectors.get())); + } + else + { + responses = solve_sternheimer_periodic_linear_response_batch( + true, + hamiltonian, + target_occupied_projector, + source.states.eigenvalues[ib], + rhs_batch, + delta_subspace.virtual_states, + perturbation_matrix_elements, + omega_ry, + grid_data.volume_element, + solver_options, + pair_projectors.get()); + } + std::vector response_views; + response_views.reserve(responses.size()); + for (const auto& response: responses) + { + response_views.push_back(&response.wavefunction); + } + SternheimerRPA::accumulate_chi0_branch_columns(channels, + source.states.wavefunctions[ib], + response_views, + grid_data.volume_element, + matrix_occupation, + batch.begin, + chi0_branch); + std::vector batch_results; + batch_results.reserve(static_cast(batch.size)); + for (int offset = 0; offset != batch.size; ++offset) + { + const int ichannel = batch.begin + offset; + const std::size_t channel_index = static_cast(ichannel); + const SternheimerPeriodicLinearResponse& response + = responses[static_cast(offset)]; + if (write_delta_components) + { + SternheimerRPA::accumulate_chi0_branch_column( + channels, + source.states.wavefunctions[ib], + response.delta_components.in_sos_wavefunction, + grid_data.volume_element, + matrix_occupation, + ichannel, + *delta_sos_branch); + SternheimerRPA::accumulate_chi0_branch_column( + channels, + source.states.wavefunctions[ib], + response.delta_components.in_pulay_wavefunction, + grid_data.volume_element, + matrix_occupation, + ichannel, + *delta_pulay_branch); + SternheimerRPA::accumulate_chi0_branch_column( + channels, + source.states.wavefunctions[ib], + response.delta_components.out_wavefunction, + grid_data.volume_element, + matrix_occupation, + ichannel, + *delta_out_branch); + } + SternheimerFDHamiltonian::Vector lcao_response; + if (write_lcao_sos) + { + const auto lcao_matrix_elements = delta_sternheimer_perturbation_matrix_elements( + lcao_virtual_states, + batch_perturbations_ry[static_cast(offset)], + source.states.wavefunctions[ib], + grid_data.volume_element); + lcao_response = build_delta_sternheimer_sos_wavefunction( + lcao_virtual_states, + lcao_matrix_elements, + source.states.eigenvalues[ib], + omega_ry); + SternheimerRPA::accumulate_chi0_branch_column(channels, + source.states.wavefunctions[ib], + lcao_response, + grid_data.volume_element, + matrix_occupation, + ichannel, + *lcao_sos_branch); + } + PeriodicChannelEquationResult result; + result.channel_index = ichannel; + result.solver = response.solver; + result.equation_residual_norm = response.residual_norm; + result.full_grid_equation_residual_norm + = response.full_grid_equation_residual_norm; + if (write_wavefunction_diagnostic + && wavefunction_diagnostic_config.selector.matches(response_plan.iq, + pair.source_index, + ib, + ifrequency + 1, + ichannel)) + { + result.has_wavefunction_diagnostic = true; + auto& diagnostic = result.wavefunction_diagnostic; + diagnostic.metadata.nx = grid_data.grid.nx; + diagnostic.metadata.ny = grid_data.grid.ny; + diagnostic.metadata.nz = grid_data.grid.nz; + diagnostic.metadata.iq = response_plan.iq; + diagnostic.metadata.ik_full = pair.source_index; + diagnostic.metadata.ib = ib; + diagnostic.metadata.ifrequency = ifrequency + 1; + diagnostic.metadata.channel = ichannel; + const auto lattice = sternheimer_fd_grid_lattice_vectors(grid_data.grid); + for (int i = 0; i != 3; ++i) + { + for (int j = 0; j != 3; ++j) + { + diagnostic.metadata.lattice[static_cast(3 * i + j)] + = lattice[static_cast(i)][static_cast(j)]; + } + } + diagnostic.metadata.qpoint + = use_supercell_translation_sum + ? supercell_translation_sum.primitive_qpoint + : response_plan.qpoint; + diagnostic.metadata.source_kpoint = source_record.kpoint; + diagnostic.metadata.target_kpoint = target_record.kpoint; + diagnostic.metadata.omega_ha + = frequency_grid.omega_ha[static_cast(ifrequency)]; + diagnostic.metadata.omega_ry = omega_ry; + diagnostic.metadata.volume_element = grid_data.volume_element; + diagnostic.metadata.reference_eigenvalue_ry = source.states.eigenvalues[ib]; + diagnostic.metadata.weighted_occupation = occupation; + diagnostic.metadata.rhs_norm + = sternheimer_fd_grid_norm(response.projected_rhs, + grid_data.volume_element); + diagnostic.metadata.solver_relative_residual = response.solver.relative_residual; + diagnostic.metadata.equation_relative_residual + = diagnostic.metadata.rhs_norm > 0.0 + ? response.full_grid_equation_residual_norm + / diagnostic.metadata.rhs_norm + : response.full_grid_equation_residual_norm; + diagnostic.metadata.diagonal_branch_element + = matrix_occupation + * SternheimerRPA::accumulate_polarizability_grid_element( + channels[channel_index].potential_r, + source.states.wavefunctions[ib], + response.wavefunction, + grid_data.volume_element); + diagnostic.vectors.push_back( + {"psi0", source.states.wavefunctions[ib]}); + diagnostic.vectors.push_back( + {"hartree_potential_ha", channels[channel_index].potential_r}); + diagnostic.vectors.push_back( + {"perturbation_ry", batch_perturbations_ry[static_cast(offset)]}); + diagnostic.vectors.push_back({"rhs_ry", response.projected_rhs}); + diagnostic.vectors.push_back({"delta_psi", response.wavefunction}); + if (response.has_delta_components) + { + diagnostic.vectors.push_back( + {"delta_in_sos", response.delta_components.in_sos_wavefunction}); + diagnostic.vectors.push_back( + {"delta_in_pulay", response.delta_components.in_pulay_wavefunction}); + diagnostic.vectors.push_back( + {"delta_out_grid", response.delta_components.out_wavefunction}); + const SternheimerDeltaPulayOperatorComponents pulay_terms + = decompose_delta_sternheimer_pulay_operator_terms( + hamiltonian, + diagnostic_fixed_local_potential, + target_occupied_projector, + delta_subspace.virtual_states, + response.delta_components.out_wavefunction, + source.states.eigenvalues[ib], + omega_ry, + grid_data.volume_element); + diagnostic.vectors.push_back( + {"delta_in_pulay_kinetic", pulay_terms.kinetic}); + diagnostic.vectors.push_back( + {"delta_in_pulay_fixed_local", pulay_terms.fixed_local}); + diagnostic.vectors.push_back( + {"delta_in_pulay_hxc_local", pulay_terms.hxc_local}); + diagnostic.vectors.push_back( + {"delta_in_pulay_nonlocal", pulay_terms.nonlocal}); + diagnostic.vectors.push_back( + {"delta_in_pulay_eigenvalue", pulay_terms.eigenvalue}); + diagnostic.vectors.push_back( + {"delta_in_pulay_operator_total", pulay_terms.total}); + } + if (!lcao_response.empty()) + { + diagnostic.vectors.push_back({"lcao_sos", std::move(lcao_response)}); + } + } + batch_results.push_back(std::move(result)); + } + SternheimerRPA::SolverResult batch_solver; + batch_solver.converged = true; + double batch_equation_residual_norm = 0.0; + for (const PeriodicChannelEquationResult& result: batch_results) + { + batch_solver.converged = batch_solver.converged && result.solver.converged; + batch_solver.iterations = std::max(batch_solver.iterations, + result.solver.iterations); + batch_solver.relative_residual = std::max(batch_solver.relative_residual, + result.solver.relative_residual); + batch_solver.absolute_residual = std::max(batch_solver.absolute_residual, + result.solver.absolute_residual); + batch_equation_residual_norm + = std::max(batch_equation_residual_norm, + result.equation_residual_norm); + } + const int batch_completed + = completed_batch_equations.fetch_add(batch.size) + batch.size; + append_chi0_progress_event( + "equation_batch", + ifrequency + 1, + owner_rank, + ib, + batch.begin, + solved_equations_before_band + batch_completed, + &batch_solver, + batch_equation_residual_norm, + elapsed_seconds_since(chi0_start_time), + "batch_size=" + std::to_string(batch.size) + + ",source_k=" + std::to_string(pair.source_index + 1) + + ",target_k=" + std::to_string(pair.target_index + 1), runtime); + return batch_results; + }, + pair_channel_worker_plan.effective_workers); + std::vector channel_results; + channel_results.reserve(static_cast(num_channels / response_replicas + 1)); + for (std::vector& group: grouped_channel_results) + { + for (PeriodicChannelEquationResult& result: group) + { + channel_results.push_back(std::move(result)); + } + } + + for (PeriodicChannelEquationResult& result: channel_results) + { + all_converged = all_converged && result.solver.converged; + ++solved_equations; + max_solver_relative_residual + = std::max(max_solver_relative_residual, result.solver.relative_residual); + max_equation_residual_norm + = std::max(max_equation_residual_norm, result.equation_residual_norm); + max_full_grid_equation_residual_norm + = std::max(max_full_grid_equation_residual_norm, + result.full_grid_equation_residual_norm); + if (result.has_wavefunction_diagnostic) + { + SternheimerWavefunctionDiagnostic::write( + wavefunction_diagnostic_config.output_filename, + result.wavefunction_diagnostic); + ++local_wavefunction_diagnostic_count; + runtime.log << " Sternheimer wavefunction diagnostic: " + << wavefunction_diagnostic_config.output_filename << std::endl; + } + append_chi0_progress_event("equation", + ifrequency + 1, + owner_rank, + ib, + result.channel_index, + solved_equations, + &result.solver, + result.equation_residual_norm, + elapsed_seconds_since(chi0_start_time), + "source_k=" + std::to_string(pair.source_index + 1) + + ",target_k=" + std::to_string(pair.target_index + 1), runtime); + } + } + } + if (write_partial_kresolved) + { +#ifdef __MPI + if (response_replicas > 1) + { + for (auto& branch: partial_pair_branches) + { + if (!branch.empty()) + { + MPI_Allreduce(MPI_IN_PLACE, + branch.data(), + static_cast(branch.size()), + MPI_DOUBLE_COMPLEX, + MPI_SUM, + response_group_comm); + } + } + } +#endif + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const auto& branch = partial_pair_branches[static_cast(ifrequency)]; + if (branch.empty() || local_channel_replica != 0) + { + continue; + } + SternheimerPartialResponseRecord partial + = make_sternheimer_partial_response_record(response_plan.iq, + pair.source_index, + ifrequency + 1, + branch, + num_channels); + const SternheimerRPA::Chi0V1Metadata metadata + = make_chi0_v1_metadata(ucell, + channels, + response_plan.iq, + ifrequency + 1, + frequency_grid.omega_ha[static_cast(ifrequency)], + frequency_grid.weights_ha[static_cast(ifrequency)], + output_atom_count); + const std::string data_file = output_file(partial.filename); + SternheimerRPA::write_chi0_v1_file( + data_file, metadata, auxiliary_channels, partial.matrix); + runtime.log << " Sternheimer periodic partial response: " + << data_file << std::endl; + local_partial_records.push_back(std::move(partial)); + + if (write_kresolved_diagnostic) + { + auto& aggregate = chi0_branches[static_cast(ifrequency)]; + for (std::size_t entry = 0; entry != branch.size(); ++entry) + { + aggregate[entry] += branch[entry]; + } + } + } + } + append_chi0_progress_event("kpair_finish", + 0, + -1, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + std::string("mode=") + (use_delta_sternheimer ? "delta" : "standard") + + ",delta_dim=" + std::to_string(delta_subspace.virtual_states.size()), runtime); + } + +#ifdef __MPI + if (response_replicas > 1 && !write_partial_kresolved) + { + const auto reduce_response_replica_branches + = [&](std::vector>& branches) { + for (auto& branch: branches) + { + if (branch.empty()) + { + continue; + } + MPI_Allreduce(MPI_IN_PLACE, + branch.data(), + static_cast(branch.size()), + MPI_DOUBLE_COMPLEX, + MPI_SUM, + response_group_comm); + if (local_channel_replica != 0) + { + std::fill(branch.begin(), branch.end(), SternheimerRPA::Complex(0.0, 0.0)); + } + } + }; + reduce_response_replica_branches(chi0_branches); + if (write_delta_components) + { + reduce_response_replica_branches(delta_sos_branches); + reduce_response_replica_branches(delta_pulay_branches); + reduce_response_replica_branches(delta_out_branches); + } + if (write_lcao_sos) + { + reduce_response_replica_branches(lcao_sos_branches); + } + } + if (response_group_comm != MPI_COMM_NULL) + { + MPI_Comm_free(&response_group_comm); + } +#endif + + if (use_nested_response_mpi + && (local_frequency_slot != 0 || local_channel_replica != 0)) + { + std::fill(target_projector_dimensions.begin(), target_projector_dimensions.end(), 0); + std::fill(target_delta_dimensions.begin(), target_delta_dimensions.end(), 0); + std::fill(target_delta_eigenvalue_min.begin(), target_delta_eigenvalue_min.end(), 0.0); + std::fill(target_delta_eigenvalue_max.begin(), target_delta_eigenvalue_max.end(), 0.0); + std::fill(target_delta_grid_hamiltonian_relative_difference.begin(), + target_delta_grid_hamiltonian_relative_difference.end(), + 0.0); + std::fill(target_delta_grid_hamiltonian_max_abs_difference.begin(), + target_delta_grid_hamiltonian_max_abs_difference.end(), + 0.0); + std::fill(target_lcao_unoccupied_min.begin(), target_lcao_unoccupied_min.end(), 0.0); + std::fill(target_lcao_unoccupied_max.begin(), target_lcao_unoccupied_max.end(), 0.0); + std::fill(target_lcao_occupied_raw_norm_min.begin(), target_lcao_occupied_raw_norm_min.end(), 0.0); + std::fill(target_lcao_occupied_raw_norm_max.begin(), target_lcao_occupied_raw_norm_max.end(), 0.0); + std::fill(target_lcao_unoccupied_raw_norm_min.begin(), target_lcao_unoccupied_raw_norm_min.end(), 0.0); + std::fill(target_lcao_unoccupied_raw_norm_max.begin(), target_lcao_unoccupied_raw_norm_max.end(), 0.0); + std::fill(target_lcao_occ_unocc_overlap_max.begin(), target_lcao_occ_unocc_overlap_max.end(), 0.0); + } + + if (use_kpoint_mpi) + { + if (!use_symmetry_partial_response) + { + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + reduce_kpoint_parallel_complex_vector( + chi0_branches[static_cast(ifrequency)], true, runtime); + if (write_delta_components) + { + reduce_kpoint_parallel_complex_vector( + delta_sos_branches[static_cast(ifrequency)], true, runtime); + reduce_kpoint_parallel_complex_vector( + delta_pulay_branches[static_cast(ifrequency)], true, runtime); + reduce_kpoint_parallel_complex_vector( + delta_out_branches[static_cast(ifrequency)], true, runtime); + } + if (write_lcao_sos) + { + reduce_kpoint_parallel_complex_vector( + lcao_sos_branches[static_cast(ifrequency)], true, runtime); + } + } + } + reduce_kpoint_parallel_int_vector(target_projector_dimensions, true, runtime); + reduce_kpoint_parallel_int_vector(target_delta_dimensions, true, runtime); + reduce_kpoint_parallel_double_vector(target_delta_eigenvalue_min, true, runtime); + reduce_kpoint_parallel_double_vector(target_delta_eigenvalue_max, true, runtime); + reduce_kpoint_parallel_double_vector(target_delta_grid_hamiltonian_relative_difference, true, runtime); + reduce_kpoint_parallel_double_vector(target_delta_grid_hamiltonian_max_abs_difference, true, runtime); + reduce_kpoint_parallel_double_vector(target_lcao_unoccupied_min, true, runtime); + reduce_kpoint_parallel_double_vector(target_lcao_unoccupied_max, true, runtime); + reduce_kpoint_parallel_double_vector(target_lcao_occupied_raw_norm_min, true, runtime); + reduce_kpoint_parallel_double_vector(target_lcao_occupied_raw_norm_max, true, runtime); + reduce_kpoint_parallel_double_vector(target_lcao_unoccupied_raw_norm_min, true, runtime); + reduce_kpoint_parallel_double_vector(target_lcao_unoccupied_raw_norm_max, true, runtime); + reduce_kpoint_parallel_double_vector(target_lcao_occ_unocc_overlap_max, true, runtime); + } + if (!write_lcao_sos) + { + std::fill(target_lcao_unoccupied_raw_norm_min.begin(), target_lcao_unoccupied_raw_norm_min.end(), -1.0); + std::fill(target_lcao_unoccupied_raw_norm_max.begin(), target_lcao_unoccupied_raw_norm_max.end(), -1.0); + std::fill(target_lcao_occ_unocc_overlap_max.begin(), target_lcao_occ_unocc_overlap_max.end(), -1.0); + } + + if (!use_symmetry_partial_response && write_periodic_v1) + { + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + if ((use_kpoint_mpi && runtime.rank != 0) + || (!use_kpoint_mpi + && frequency_owners[static_cast(ifrequency)] != runtime.rank)) + { + continue; + } + const std::vector chi0 + = SternheimerRPA::symmetrize_chi0_imaginary_frequency( + chi0_branches[static_cast(ifrequency)], num_channels); + const SternheimerRPA::Chi0V1Metadata metadata + = make_chi0_v1_metadata(ucell, + channels, + response_plan.iq, + ifrequency + 1, + frequency_grid.omega_ha[static_cast(ifrequency)], + frequency_grid.weights_ha[static_cast(ifrequency)], + output_atom_count); + const std::string data_file + = output_file(chi0_v1_filename(metadata.iq, metadata.ifrequency, runtime.rank)); + SternheimerRPA::write_chi0_v1_file(data_file, metadata, auxiliary_channels, chi0); + runtime.log << " Sternheimer periodic chi0 v1 output: " << data_file << std::endl; + if (write_delta_components) + { + const auto write_component = [&](const std::string& name, + const std::vector& branch) { + const std::vector matrix + = SternheimerRPA::symmetrize_chi0_imaginary_frequency(branch, num_channels); + SternheimerRPA::write_chi0_v1_file(output_file(delta_component_v1_filename(name, + metadata.iq, + metadata.ifrequency, + runtime.rank)), + metadata, + auxiliary_channels, + matrix); + }; + write_component("in_sos", delta_sos_branches[static_cast(ifrequency)]); + write_component("in_pulay", delta_pulay_branches[static_cast(ifrequency)]); + write_component("out_grid", delta_out_branches[static_cast(ifrequency)]); + } + if (write_lcao_sos) + { + const std::vector lcao_sos + = SternheimerRPA::symmetrize_chi0_imaginary_frequency( + lcao_sos_branches[static_cast(ifrequency)], num_channels); + SternheimerRPA::write_chi0_v1_file(output_file(lcao_sos_v1_filename(metadata.iq, + metadata.ifrequency, + runtime.rank)), + metadata, + auxiliary_channels, + lcao_sos); + } + } + } + int global_wavefunction_diagnostic_count = local_wavefunction_diagnostic_count; +#ifdef __MPI + MPI_Allreduce(&local_wavefunction_diagnostic_count, + &global_wavefunction_diagnostic_count, + 1, + MPI_INT, + MPI_SUM, + MPI_COMM_WORLD); +#endif + if (write_wavefunction_diagnostic && global_wavefunction_diagnostic_count != 1) + { + throw std::runtime_error("Sternheimer wavefunction diagnostic selector matched " + + std::to_string(global_wavefunction_diagnostic_count) + + " equations; expected exactly one."); + } + + std::vector gathered_partial_records; + if (write_partial_kresolved) + { + std::vector local_keys; + local_keys.reserve(3 * local_partial_records.size()); + for (const auto& record: local_partial_records) + { + local_keys.push_back(record.iq); + local_keys.push_back(record.ik_full); + local_keys.push_back(record.ifrequency); + } +#ifdef __MPI + const int local_record_count = static_cast(local_partial_records.size()); + std::vector record_counts; + if (runtime.rank == 0) + { + record_counts.resize(static_cast(runtime.nproc), 0); + } + MPI_Gather(&local_record_count, + 1, + MPI_INT, + record_counts.data(), + 1, + MPI_INT, + 0, + MPI_COMM_WORLD); + + std::vector key_counts; + std::vector key_displacements; + std::vector gathered_keys; + if (runtime.rank == 0) + { + key_counts.resize(record_counts.size(), 0); + key_displacements.resize(record_counts.size(), 0); + int total_key_count = 0; + for (std::size_t rank = 0; rank != record_counts.size(); ++rank) + { + key_counts[rank] = 3 * record_counts[rank]; + key_displacements[rank] = total_key_count; + total_key_count += key_counts[rank]; + } + gathered_keys.resize(static_cast(total_key_count)); + } + MPI_Gatherv(local_keys.data(), + static_cast(local_keys.size()), + MPI_INT, + gathered_keys.data(), + key_counts.data(), + key_displacements.data(), + MPI_INT, + 0, + MPI_COMM_WORLD); + if (runtime.rank == 0) + { + gathered_partial_records.reserve(gathered_keys.size() / 3); + for (std::size_t index = 0; index != gathered_keys.size(); index += 3) + { + SternheimerPartialResponseRecord record; + record.iq = gathered_keys[index]; + record.ik_full = gathered_keys[index + 1]; + record.ifrequency = gathered_keys[index + 2]; + record.filename = sternheimer_partial_response_filename( + record.iq, record.ik_full, record.ifrequency); + gathered_partial_records.push_back(std::move(record)); + } + } +#else + gathered_partial_records = local_partial_records; +#endif + } + + reduce_chi0_output_stats(all_converged, + solved_equations, + max_solver_relative_residual, + max_equation_residual_norm, + use_parallel_grid_mpi, + runtime); +#ifdef __MPI + if (use_parallel_grid_mpi && runtime.nproc > 1) + { + MPI_Allreduce(MPI_IN_PLACE, + &max_full_grid_equation_residual_norm, + 1, + MPI_DOUBLE, + MPI_MAX, + MPI_COMM_WORLD); + MPI_Barrier(MPI_COMM_WORLD); + } +#endif + + if (runtime.rank != 0) + { + return; + } + if (write_partial_kresolved) + { + const std::size_t expected_records + = (use_symmetry_partial_response ? fixed_q_orbits.size() : response_plan.kq_pairs.size()) + * static_cast(nfreq); + if (gathered_partial_records.size() != expected_records) + { + throw std::runtime_error( + "Sternheimer k-resolved output did not produce one partial response per expected k record and frequency."); + } + const std::string manifest_file = use_symmetry_partial_response + ? "v1_sternheimer_partial_manifest_iq_" + + std::to_string(response_plan.iq) + ".dat" + : "v1_sternheimer_kresolved_manifest_iq_" + + std::to_string(response_plan.iq) + ".dat"; + std::ofstream manifest(manifest_file); + if (!manifest) + { + throw std::runtime_error("Cannot open Sternheimer partial-response manifest: " + manifest_file); + } + manifest << format_sternheimer_partial_manifest(gathered_partial_records); + + constexpr const char* full_kpoint_manifest = "v1_sternheimer_full_kpoints.dat"; + std::ofstream full_kpoint_out(full_kpoint_manifest); + if (!full_kpoint_out) + { + throw std::runtime_error("Cannot open Sternheimer full-k-point manifest: " + + std::string(full_kpoint_manifest)); + } + full_kpoint_out << format_sternheimer_full_kpoint_manifest(response_kpoints); + + if (use_symmetry_partial_response) + { + const std::string route_fragment + = "v1_sternheimer_symmetry_routes_iq_" + + std::to_string(response_plan.iq) + ".dat"; + std::ofstream route_out(route_fragment); + if (!route_out) + { + throw std::runtime_error( + "Cannot open Sternheimer fixed-q route fragment: " + route_fragment); + } + route_out << format_sternheimer_fixed_q_routes(fixed_q_routes); + + std::vector selected_qstar_routes; + std::copy_if(qstar_routes.begin(), + qstar_routes.end(), + std::back_inserter(selected_qstar_routes), + [&](const auto& route) { + return route.representative_iq == response_plan.iq; + }); + if (selected_qstar_routes.empty()) + { + throw std::runtime_error( + "The selected Sternheimer discrete q-star has no route members."); + } + const std::string qstar_route_fragment + = "v1_sternheimer_qstar_routes_iq_" + + std::to_string(response_plan.iq) + ".dat"; + std::ofstream qstar_route_out(qstar_route_fragment); + if (!qstar_route_out) + { + throw std::runtime_error( + "Cannot open Sternheimer q-star route fragment: " + qstar_route_fragment); + } + qstar_route_out << format_sternheimer_qstar_routes(selected_qstar_routes); + + const int qstar_size = static_cast(selected_qstar_routes.size()); + const double qweight + = static_cast(qstar_size) / static_cast(response_kpoints.size()); + const std::string qpoint_fragment + = "v1_sternheimer_qpoint_iq_" + std::to_string(response_plan.iq) + ".dat"; + std::ofstream qpoint_out(qpoint_fragment); + if (!qpoint_out) + { + throw std::runtime_error("Cannot open Sternheimer q-point fragment: " + qpoint_fragment); + } + qpoint_out << std::setprecision(17) << response_plan.iq << ' ' << response_plan.qpoint[0] << ' ' + << response_plan.qpoint[1] << ' ' << response_plan.qpoint[2] << ' ' << qweight << '\n'; + } + } + std::vector> index_entries; + if (!use_symmetry_partial_response && write_periodic_v1) + { + index_entries.reserve(static_cast(nfreq)); + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const SternheimerRPA::Chi0V1Metadata metadata + = make_chi0_v1_metadata(ucell, + channels, + response_plan.iq, + ifrequency + 1, + frequency_grid.omega_ha[static_cast(ifrequency)], + frequency_grid.weights_ha[static_cast(ifrequency)], + output_atom_count); + index_entries.push_back( + {chi0_v1_filename(metadata.iq, + metadata.ifrequency, + frequency_owners[static_cast(ifrequency)]), + metadata}); + } + write_chi0_index_file(output_file("v1_sternheimer_chi0_index.dat"), index_entries); + } + + out << "status success\n"; + out << "format " << (!write_periodic_v1 + ? "diagnostic_only" + : (write_truncated_v1_diagnostic + ? (use_symmetry_partial_response + ? "v1_partial_diagnostic_truncated_bands" + : "v1_diagnostic_truncated_bands") + : (use_symmetry_partial_response + ? "v1_partial" + : (write_kresolved_diagnostic ? "v1_kresolved" : "v1")))) + << '\n'; + if (write_truncated_v1_diagnostic) + { + out << "diagnostic_only_reason truncated_bands\n"; + out << "physical_result no\n"; + } + if (!write_periodic_v1) + { + out << "data_files 0\n"; + out << "response_output diagnostic_only_no_v1\n"; + out << "diagnostic_only_reason " + << (use_supercell_translation_sum ? "supercell_translation_sum" : "truncated_bands") << '\n'; + } + else if (use_symmetry_partial_response) + { + out << "data_files " << gathered_partial_records.size() << '\n'; + out << "partial_manifest v1_sternheimer_partial_manifest_iq_" << response_plan.iq << ".dat\n"; + out << "symmetry_routes v1_sternheimer_symmetry_routes_iq_" << response_plan.iq << ".dat\n"; + out << "qstar_routes v1_sternheimer_qstar_routes_iq_" << response_plan.iq << ".dat\n"; + out << "qpoint_fragment v1_sternheimer_qpoint_iq_" << response_plan.iq << ".dat\n"; + out << "full_kpoints_manifest v1_sternheimer_full_kpoints.dat\n"; + out << "discrete_spatial_group_order " << fixed_q_discrete_spatial_order << '\n'; + out << "fixed_q_little_group_order " << fixed_q_little_group_order << '\n'; + } + else if (write_kresolved_diagnostic) + { + out << "data_files " << index_entries.size() << '\n'; + out << "index_file v1_sternheimer_chi0_index.dat\n"; + out << "kresolved_data_files " << gathered_partial_records.size() << '\n'; + out << "kresolved_manifest v1_sternheimer_kresolved_manifest_iq_" << response_plan.iq << ".dat\n"; + out << "full_kpoints_manifest v1_sternheimer_full_kpoints.dat\n"; + } + else + { + out << "data_files " << index_entries.size() << '\n'; + out << "index_file v1_sternheimer_chi0_index.dat\n"; + } + out << "sternheimer_q_index " << response_plan.iq << '\n'; + out << "sternheimer_qpoint " << response_plan.qpoint[0] << ' ' << response_plan.qpoint[1] << ' ' + << response_plan.qpoint[2] << '\n'; + out << "sternheimer_kweight_sum " << response_plan.kweight_sum << '\n'; + out << "sternheimer_kq_pairs " << response_plan.kq_pairs.size() << '\n'; + out << "sternheimer_canonical_q_indices"; + for (const int iq: canonical_q_indices) + { + out << ' ' << iq; + } + out << '\n'; + out << "sternheimer_fixed_q_little_group_order " << fixed_q_little_group_order << '\n'; + out << "sternheimer_fixed_q_representatives " + << (use_symmetry_partial_response ? fixed_q_orbits.size() : response_plan.kq_pairs.size()) << '\n'; + out << "source_global_k target_global_k fixed_q_representative gx gy gz target_projector_dimension target_delta_dimension " + "delta_eigenvalue_min_Ry delta_eigenvalue_max_Ry lcao_unoccupied_min_Ry " + "lcao_unoccupied_max_Ry lcao_occupied_raw_norm_min lcao_occupied_raw_norm_max " + "lcao_unoccupied_raw_norm_min lcao_unoccupied_raw_norm_max lcao_occ_unocc_overlap_max " + "delta_grid_hamiltonian_relative_difference delta_grid_hamiltonian_max_abs_difference_Ry\n"; + for (std::size_t pair_index = 0; pair_index != response_plan.kq_pairs.size(); ++pair_index) + { + const SternheimerKQPair& pair = response_plan.kq_pairs[pair_index]; + out << pair.source_index + 1 << ' ' << pair.target_index + 1 << ' ' + << (fixed_q_representative[static_cast(pair.source_index)] ? "yes" : "no") << ' ' + << pair.reciprocal_shift[0] << ' ' + << pair.reciprocal_shift[1] << ' ' << pair.reciprocal_shift[2] << ' ' + << target_projector_dimensions[pair_index] << ' ' << target_delta_dimensions[pair_index] << ' ' + << target_delta_eigenvalue_min[pair_index] << ' ' << target_delta_eigenvalue_max[pair_index] << ' ' + << target_lcao_unoccupied_min[pair_index] << ' ' << target_lcao_unoccupied_max[pair_index] << ' ' + << target_lcao_occupied_raw_norm_min[pair_index] << ' ' + << target_lcao_occupied_raw_norm_max[pair_index] << ' ' + << target_lcao_unoccupied_raw_norm_min[pair_index] << ' ' + << target_lcao_unoccupied_raw_norm_max[pair_index] << ' ' + << target_lcao_occ_unocc_overlap_max[pair_index] << ' ' + << target_delta_grid_hamiltonian_relative_difference[pair_index] << ' ' + << target_delta_grid_hamiltonian_max_abs_difference[pair_index] << '\n'; + } + out << "grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' << grid_data.grid.nz << " size " + << grid_data.grid.size() << " dV " << grid_data.volume_element << '\n'; + out << "pbe_ecutwfc_Ry " << runtime.input.ecutwfc << '\n'; + out << "sternheimer_response_ecutwfc_requested_Ry " + << runtime.input.sternheimer_response_ecutwfc << '\n'; + out << "sternheimer_response_ecutwfc_Ry " << response_grid_ecutwfc << '\n'; + out << "sternheimer_response_grid_requested " << response_grid.requested_dimensions[0] << ' ' + << response_grid.requested_dimensions[1] << ' ' << response_grid.requested_dimensions[2] << '\n'; + out << "sternheimer_response_grid_source " << response_grid_source << '\n'; + out << "pbe_grid " << pw_basis.nx << ' ' << pw_basis.ny << ' ' << pw_basis.nz << " size " << pw_basis.nxyz + << '\n'; + out << "response_grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' << grid_data.grid.nz << " size " + << grid_data.grid.size() << '\n'; + out << "nfreq " << nfreq << '\n'; + out << "frequency_grid_source " << frequency_grid_source << '\n'; + if (use_frequency_grid_file) + { + out << "frequency_grid_file " << frequency_grid_file << '\n'; + } + out << "transition_window_Ha " << transition_window.emin_ha << ' ' << transition_window.emax_ha << '\n'; + out << "pca_threshold " << pca_threshold << '\n'; + out << "sternheimer_abfs_source " << (reuse_rpa_abfs ? "rpa_handoff" : "rebuilt") << '\n'; + out << "ccp_rmesh_times_input " << ccp_rmesh_times << '\n'; + out << "perturbation_coulomb_kernel full_periodic_poisson\n"; + out << "periodic_kmesh " << response_kmesh[0] << ' ' << response_kmesh[1] << ' ' + << response_kmesh[2] << '\n'; + out << "periodic_gamma_massidda_chi " << massidda_chi << '\n'; + out << "periodic_gamma_coulomb_projection " + << (use_supercell_translation_sum + ? "skipped_supercell_translation_sum" + : (evaluate_gamma_projection ? "diagnostic_only_physical_poisson" + : (gamma_qpoint ? "skipped_large_auxiliary_dimension" : "not_applicable"))) + << '\n'; + out << "periodic_gamma_projection_relative_error " << gamma_projection_relative_error << '\n'; + out << "periodic_gamma_limit " + << (gamma_qpoint ? "constant_mode_only_no_headwing" : "not_applicable") << '\n'; + out << "perturbation_ccp_rmesh_times_used no\n"; + out << "supercell_translation_perturbation " << (use_supercell_translation_sum ? "yes" : "no") << '\n'; + out << "supercell_translation_full_response " << (full_supercell_response ? "yes" : "no") << '\n'; + out << "supercell_abfs_root_build_broadcast " + << (full_supercell_response && use_kpoint_mpi ? "yes" : "no") << '\n'; + out << "supercell_response_matrix_scale " << response_matrix_scale << '\n'; + if (use_supercell_translation_sum) + { + out << "supercell_translation_repeats " << supercell_translation_sum.repeats[0] << ' ' + << supercell_translation_sum.repeats[1] << ' ' << supercell_translation_sum.repeats[2] << '\n'; + out << "supercell_translation_primitive_q " << supercell_translation_sum.primitive_qpoint[0] << ' ' + << supercell_translation_sum.primitive_qpoint[1] << ' ' + << supercell_translation_sum.primitive_qpoint[2] << '\n'; + out << "supercell_translation_atoms_per_primitive " + << supercell_translation_sum.atoms_per_primitive << '\n'; + out << "supercell_translation_basis_atom " << supercell_translation_sum.basis_atom << '\n'; + out << "supercell_translation_channel_within_atom " + << supercell_translation_sum.channel_within_atom << '\n'; + out << "supercell_sector_dimension " << supercell_sector_dimension << '\n'; + out << "supercell_sector_kweight " << supercell_sector_kweight << '\n'; + out << "supercell_sector_max_orthonormality_error " + << supercell_sector_max_orthonormality_error << '\n'; + out << "supercell_sector_max_full_space_residual " + << supercell_sector_max_full_space_residual << '\n'; + } + out << "sternheimer_channel_threads " << channel_worker_plan.effective_workers << '\n'; + out << "sternheimer_channel_batch_width " << channel_worker_plan.channel_batch_width << '\n'; + out << "sternheimer_mode " << (use_delta_sternheimer ? "delta" : "standard") << '\n'; + if (use_delta_sternheimer) + { + out << "sternheimer_delta_a_block " << sternheimer_delta_a_block_mode_name(delta_a_block_mode) << '\n'; + } + out << "delta_component_diagnostic " << (write_delta_components ? "yes" : "no") << '\n'; + out << "lcao_sos_diagnostic " << (write_lcao_sos ? "yes" : "no") << '\n'; + out << "wavefunction_diagnostic " << (write_wavefunction_diagnostic ? "yes" : "no") << '\n'; + if (write_wavefunction_diagnostic) + { + out << "wavefunction_diagnostic_file " << wavefunction_diagnostic_config.output_filename << '\n'; + } + out << "abfs_channels " << num_channels << '\n'; + out << "abfs_max_channels_per_atom " << max_channels << '\n'; + out << "occupied_bands_total " << sternheimer_lcao_total_occupied_bands(response_kpoints) << '\n'; + out << "sternheimer_bands_per_k_limit " << max_bands << '\n'; + out << "sternheimer_bands_truncated " << (bands_are_truncated ? "yes" : "no") << '\n'; + out << "sternheimer_truncated_v1_diagnostic " + << (write_truncated_v1_diagnostic ? "yes" : "no") << '\n'; + out << "sternheimer_frequency_mpi " << (use_frequency_mpi ? "yes" : "no") << '\n'; + out << "sternheimer_channel_mpi " << (runtime.input.sternheimer_channel_mpi ? "yes" : "no") << '\n'; + out << "sternheimer_response_slots " + << (use_nested_response_mpi ? kpoint_groups * nfreq : nfreq) << '\n'; + out << "sternheimer_channel_replicas_per_response_slot " << response_replicas << '\n'; + out << "sternheimer_mpi_layout " << runtime.input.sternheimer_mpi_layout << '\n'; + out << "equation_owner_formula " + << (runtime.input.sternheimer_mpi_layout == "global_equation" + ? "occupied_frequency_channel_modulo" + : "frequency_group_assignment") + << '\n'; + out << "sternheimer_fd_order " << runtime.input.sternheimer_fd_order << '\n'; + out << "sternheimer_preconditioner " + << (solver_options.use_fd_spectral_preconditioner ? "fd_spectral" : "none") << '\n'; + out << "sternheimer_preconditioner_regularization_Ry " + << solver_options.fd_spectral_preconditioner_regularization << '\n'; + out << "sternheimer_kpoint_mpi " << (use_kpoint_mpi ? "yes" : "no") << '\n'; + out << "sternheimer_nested_k_frequency_mpi " << (use_nested_response_mpi ? "yes" : "no") << '\n'; + out << "sternheimer_kpoint_groups " << (use_kpoint_mpi ? kpoint_groups : 1) << '\n'; + out << "sternheimer_frequency_groups " << (use_frequency_mpi ? (use_nested_response_mpi ? nfreq + : runtime.nproc) + : 1) + << '\n'; + out << "sternheimer_kpoint_group_ranks " + << (use_kpoint_mpi ? runtime.nproc / kpoint_groups : runtime.nproc) << '\n'; + out << "mpi_ranks " << runtime.nproc << '\n'; + out << "frequency_rank_shift " << frequency_rank_shift << '\n'; + out << "solved_equations " << solved_equations << '\n'; + out << "all_converged " << (all_converged ? "yes" : "no") << '\n'; + out << "max_solver_relative_residual " << max_solver_relative_residual << '\n'; + out << "max_equation_residual_norm " << max_equation_residual_norm << '\n'; + out << "max_full_grid_equation_residual_norm " << max_full_grid_equation_residual_norm << '\n'; +} + +void write_failure_report(std::ofstream& out, const std::string& reason) +{ + out << "# ABACUS Sternheimer FD linear-response smoke test\n"; + out << "status failed\n"; + out << "reason " << reason << '\n'; +} + +} // namespace + +bool sternheimer_abacus_st_smoke_enabled() +{ + return env_is_true(kSmokeEnv); +} + +void run_sternheimer_abacus_st_smoke(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const std::string& output_dir, + const SternheimerAbacusRuntime& runtime) +{ + if (!sternheimer_abacus_st_smoke_enabled() || runtime.rank != 0) + { + return; + } + + const std::string report_path = default_report_path(output_dir); + std::ofstream out(report_path.c_str(), std::ios::out | std::ios::trunc); + if (!out) + { + runtime.log << " Sternheimer FD ST smoke: failed to open " << report_path << std::endl; + return; + } + + try + { + if (runtime.nproc != 1) + { + throw std::runtime_error( + "The current FD ST smoke test requires a single MPI rank so pw_basis.nrxx is the full grid."); + } + if (elec_state.ekb.nc <= 0 || elec_state.wg.nc <= 0) + { + throw std::runtime_error("ABACUS DFT eigenvalues or occupations are not available."); + } + + const int requested_bands = positive_int_from_env(kBandsEnv, 1); + const int num_bands = std::min(requested_bands, elec_state.ekb.nc); + const int max_channels = positive_int_from_env(kChannelsEnv, 1); + const int max_dense_size = positive_int_from_env(kMaxDenseEnv, 4096); + const int lanczos_max_subspace_size = positive_int_from_env(kLanczosSubspaceEnv, 320); + const double omega = nonnegative_double_from_env(kOmegaEnv, 0.5); + const double solver_tolerance + = positive_double_from_env(kSolverToleranceEnv, default_sternheimer_solver_tolerance()); + const int solver_max_iter = positive_int_from_env(kSolverMaxIterEnv, 300); + const double pca_threshold = nonnegative_double_from_env(kPCAThresholdEnv, runtime.input.exx_pca_threshold); + const double ccp_rmesh_times + = positive_double_from_env(kCCPRmeshTimesEnv, runtime.input.rpa_ccp_rmesh_times); + + SternheimerABACUSSTSmokeResult result; + result.grid_data = make_sternheimer_fd_grid(pw_basis); + result.omega = omega; + result.pca_threshold = pca_threshold; + result.ccp_rmesh_times = ccp_rmesh_times; + result.perturbation_source = "abfs_ccp"; + + const SternheimerFDHamiltonian hamiltonian + = make_sternheimer_fd_hamiltonian( + potential, pw_basis, ucell, 0, 1.0, {0.0, 0.0, 0.0}, runtime.input.sternheimer_fd_order); + const SternheimerFDZeroOrderStates states = solve_fd_zero_order_auto(hamiltonian, + num_bands, + result.grid_data.volume_element, + max_dense_size, + lanczos_max_subspace_size); + const std::vector occupied + = occupied_wavefunctions_from_states(states, elec_state, 0); + if (occupied.empty()) + { + throw std::runtime_error("No occupied FD zero-order states are available for Sternheimer ST."); + } + + const std::vector channels + = build_abfs_ccp_grid_channels( + ucell, result.grid_data.grid, max_channels, pca_threshold, ccp_rmesh_times, runtime); + result.num_available_channels = static_cast(channels.size()); + if (channels.empty()) + { + throw std::runtime_error("No ABFS CCP perturbation channels were generated."); + } + + auto dot = [&result](const SternheimerFDHamiltonian::Vector& lhs, const SternheimerFDHamiltonian::Vector& rhs) { + return sternheimer_fd_grid_dot(lhs, rhs, result.grid_data.volume_element); + }; + + SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = solver_max_iter; + solver_options.residual_tol = solver_tolerance; + solver_options.use_fd_spectral_preconditioner + = sternheimer_environment_flag(kSpectralPreconditionerEnv, true); + solver_options.fd_spectral_preconditioner_regularization + = nonnegative_double_from_env(kSpectralPreconditionerRegularizationEnv, 0.0); + + for (int ib = 0; ib != static_cast(states.wavefunctions.size()); ++ib) + { + const double occupation = elec_state.wg(0, ib); + if (occupation <= 1.0e-8) + { + continue; + } + + for (const SternheimerABFGridChannel& channel: channels) + { + const std::vector perturbation_ry = scale_potential(channel.potential_r, kHartreeToRydberg); + SternheimerFDHamiltonian::Vector rhs; + SternheimerRPA::build_rhs_from_hartree_perturbation(perturbation_ry, states.wavefunctions[ib], rhs); + SternheimerFDHamiltonian::Vector projected_rhs = rhs; + SternheimerRPA::project_out_subspace(occupied, dot, projected_rhs); + + const SternheimerFDLinearResponse response + = solve_sternheimer_fd_linear_response(hamiltonian, + occupied, + states.eigenvalues[ib], + rhs, + omega, + result.grid_data.volume_element, + solver_options); + + SternheimerABACUSSTChannelResult channel_result; + channel_result.band_index = ib; + channel_result.channel_index = channel.channel_index; + channel_result.atom_index = channel.atom_index; + channel_result.angular_momentum = channel.angular_momentum; + channel_result.radial_index = channel.radial_index; + channel_result.magnetic_index = channel.magnetic_index; + channel_result.fd_eigenvalue = states.eigenvalues[ib]; + channel_result.occupation = occupation; + channel_result.rhs_norm = sternheimer_fd_grid_norm(rhs, result.grid_data.volume_element); + channel_result.projected_rhs_norm + = sternheimer_fd_grid_norm(projected_rhs, result.grid_data.volume_element); + channel_result.solver_converged = response.solver.converged; + channel_result.solver_iterations = response.solver.iterations; + channel_result.solver_relative_residual = response.solver.relative_residual; + channel_result.equation_residual_norm = response.residual_norm; + channel_result.polarizability + = SternheimerRPA::accumulate_polarizability_grid_element(channel.potential_r, + states.wavefunctions[ib], + response.delta_wavefunction, + result.grid_data.volume_element); + result.channels.push_back(channel_result); + } + } + + out << format_sternheimer_abacus_st_report(result); + runtime.log << " Sternheimer FD ST smoke report: " << report_path << std::endl; + } + catch (const std::exception& error) + { + write_failure_report(out, error.what()); + runtime.log << " Sternheimer FD ST smoke failed: " << error.what() << std::endl; + runtime.log << " Sternheimer FD ST smoke report: " << report_path << std::endl; + } +} + +void run_sternheimer_abacus_chi0_output_impl(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const std::string& output_dir, + const LCAO_Orbitals* lcao_orbitals, + const std::vector* lcao_occupied_kpoints, + const std::array* lcao_kmesh, + const ModulePW::PW_Basis_K* siab_pw_wfc, + const Structure_Factor* siab_structure_factor, + const SternheimerOrbitalSet* reusable_rpa_abfs, + const SternheimerAbacusRuntime& runtime) +{ + const bool write_librpa = runtime.input.out_sternheimer_librpa; + const bool write_siab = runtime.input.out_sternheimer_siab; + const bool write_grid_diagnostics = runtime.input.sternheimer_grid_diagnostics; + if (!write_librpa && !write_siab && !write_grid_diagnostics) + { + return; + } + + // Store all LibRPA producer artifacts below the selected ABACUS output + // directory. Indexes/manifests continue to contain basenames so that + // LibRPA can resolve them relative to that directory. + const auto output_file = [&output_dir](const std::string& filename) { + return join_output_path(output_dir, filename); + }; + + const bool use_frequency_mpi = runtime.input.sternheimer_frequency_mpi; + const bool use_channel_mpi = runtime.input.sternheimer_channel_mpi; + const std::string mpi_layout = runtime.input.sternheimer_mpi_layout; + const bool use_global_equation_mpi = mpi_layout == "global_equation"; + const int nfreq = runtime.input.sternheimer_nfreq; + const char* supercell_translation_sum_raw = std::getenv(kSupercellTranslationSumEnv); + const bool full_supercell_response + = supercell_translation_sum_raw != nullptr + && supercell_translation_sum_raw[0] != '\0' + && env_is_true(kSupercellFullResponseEnv); + int response_kpoint_count = elec_state.wg.nr; + int requested_supercell_kpoint_groups = 1; + if (full_supercell_response) + { + const auto translation_sum + = parse_sternheimer_supercell_translation_sum(supercell_translation_sum_raw); + response_kpoint_count = sternheimer_supercell_primitive_cell_count(translation_sum); + requested_supercell_kpoint_groups + = positive_int_from_env(kSupercellKPointGroupsEnv, 1); + } + const int response_kpoint_groups + = runtime.input.sternheimer_q_index > 0 + ? sternheimer_response_kpoint_group_count(full_supercell_response, + requested_supercell_kpoint_groups, + runtime.kpar_lcao, + response_kpoint_count) + : 1; + const bool use_kpoint_mpi + = runtime.input.sternheimer_q_index > 0 && response_kpoint_groups > 1; + const bool use_nested_response_mpi = use_frequency_mpi && use_kpoint_mpi; + const bool use_parallel_response_mpi = use_frequency_mpi || use_kpoint_mpi; + const bool use_distributed_mpi = use_parallel_response_mpi || use_channel_mpi; + initialize_sternheimer_memory_detection(); + if (!use_distributed_mpi && runtime.rank != 0) + { + return; + } + + const std::string status_path = chi0_status_path(output_dir, runtime); + std::ofstream out; + if (runtime.rank == 0) + { + out.open(status_path.c_str(), std::ios::out | std::ios::trunc); + if (!out) + { + runtime.log << " Sternheimer chi0 output: failed to open " << status_path << std::endl; +#ifdef __MPI + if (use_distributed_mpi && runtime.nproc > 1) + { + MPI_Abort(MPI_COMM_WORLD, 1); + } +#endif + return; + } + out << std::setprecision(16); + out << (write_siab ? "# ABACUS Coulomb-whitened Sternheimer target output for SIAB\n" + : "# ABACUS Sternheimer chi0 output for LibRPA\n"); + } + + try + { + reset_chi0_progress_file(runtime); + if (env_is_true("ABACUS_STERNHEIMER_WEAK_Q_UNIT") && runtime.input.sternheimer_q_index <= 0) + throw std::invalid_argument("Weak q unit requires a positive periodic q index."); + const auto chi0_start_time = std::chrono::steady_clock::now(); + append_chi0_progress_event("enter", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + std::string(use_frequency_mpi ? "frequency_mpi=yes" : "frequency_mpi=no") + + "," + + (use_kpoint_mpi ? "kpoint_mpi=yes" : "kpoint_mpi=no") + + "," + + (use_nested_response_mpi ? "nested_mpi=yes" : "nested_mpi=no"), + runtime); + if (write_librpa && runtime.input.out_librpa_ver != 1) + { + throw std::runtime_error("out_sternheimer_librpa requires out_librpa_ver=1."); + } + SternheimerRPA::validate_mpi_layout(mpi_layout, + use_frequency_mpi, + use_channel_mpi, + write_siab, + write_librpa, + nfreq, + runtime.nproc); + if (runtime.nproc != 1 && !use_parallel_response_mpi) + { + throw std::runtime_error( + "Sternheimer chi0 output with multiple MPI ranks requires sternheimer_frequency_mpi=true."); + } + const int nested_response_slots + = response_kpoint_groups * runtime.input.sternheimer_nfreq; + if (use_nested_response_mpi + && ((!use_channel_mpi && runtime.nproc != nested_response_slots) + || (use_channel_mpi + && (runtime.nproc < nested_response_slots + || runtime.nproc % nested_response_slots != 0)))) + { + throw std::runtime_error( + "Nested Sternheimer MPI requires one rank or an integer channel-MPI multiple per k-frequency slot."); + } + if (use_kpoint_mpi && !use_frequency_mpi + && response_kpoint_groups != runtime.nproc) + { + throw std::runtime_error( + "Sternheimer k-point MPI without frequency MPI requires NPROC=k-point-groups."); + } + if (use_parallel_response_mpi && runtime.nproc > 1 && pw_basis.poolnproc != runtime.nproc) + { + throw std::runtime_error( + "Parallel Sternheimer currently requires all MPI ranks to be in the same real-space pool."); + } + if (elec_state.ekb.nc <= 0 || elec_state.wg.nc <= 0) + { + throw std::runtime_error("ABACUS DFT eigenvalues or occupations are not available."); + } + + const bool use_lcao_zero_order = lcao_occupied_kpoints != nullptr; + const std::string analytic_ao_path = runtime.input.sternheimer_ao_file; + const bool use_analytic_ao = validate_sternheimer_molecular_coulomb(runtime.input); + if (std::getenv("ABACUS_STERNHEIMER_ANALYTIC_AO_POTENTIALS") != nullptr + || sternheimer_environment_flag("ABACUS_STERNHEIMER_MOLECULAR_COULOMB_TAIL", false)) + { + throw std::invalid_argument( + "Use the explicit sternheimer_molecular_coulomb and " + "sternheimer_ao_file inputs instead of experimental environment flags."); + } + if (use_analytic_ao + && (!use_lcao_zero_order + || (!runtime.abfs_files.empty() && runtime.input.exx_pca_threshold < 1.0))) + { + throw std::invalid_argument("isolated_ri requires LCAO states and either product-PCA ABFS or " + "external-only ABFS (exx_pca_threshold >= 1), not a mixed basis."); + } + if ((lcao_orbitals == nullptr) != (lcao_occupied_kpoints == nullptr)) + { + throw std::runtime_error("Sternheimer LCAO zero-order input is incomplete."); + } + if (write_siab + && (!use_lcao_zero_order || siab_pw_wfc == nullptr || siab_structure_factor == nullptr)) + { + throw std::runtime_error( + "out_sternheimer_siab requires LCAO Delta-ST plus the PW FFT basis and structure factor."); + } + + if (runtime.input.sternheimer_q_index > 0) + { + if (!use_lcao_zero_order) + { + throw std::runtime_error("A nonzero sternheimer_q_index requires LCAO k-resolved zero-order states."); + } + if (lcao_kmesh == nullptr) + { + throw std::runtime_error("Periodic Sternheimer requires Monkhorst-Pack dimensions."); + } + validate_sternheimer_full_lcao_occupied_kpoints(*lcao_occupied_kpoints, + elec_state.wg.nr, + runtime.input.nspin, + runtime.nlocal); + run_sternheimer_periodic_lcao_chi0_output(potential, + pw_basis, + ucell, + elec_state, + *lcao_orbitals, + *lcao_occupied_kpoints, + *lcao_kmesh, + out, + use_frequency_mpi, + response_kpoint_groups, + chi0_start_time, + reusable_rpa_abfs, + runtime); + if (runtime.rank == 0) + { + runtime.log << " Sternheimer periodic chi0 status: " << status_path << std::endl; + } + return; + } + + std::vector response_kpoints(1, nullptr); + if (use_lcao_zero_order) + { + validate_sternheimer_lcao_occupied_kpoints( + *lcao_occupied_kpoints, + elec_state.wg.nr, + elec_state.wg.nr, + runtime.input.nspin, + runtime.nlocal, + -1, + false); + response_kpoints + = select_sternheimer_gamma_spin_records(*lcao_occupied_kpoints, runtime.input.nspin); + } + + std::vector occupied_band_counts; + occupied_band_counts.reserve(response_kpoints.size()); + for (const SternheimerLCAOOccupiedKPoint* response_kpoint: response_kpoints) + { + const int response_k_index = response_kpoint == nullptr ? 0 : response_kpoint->local_k_index; + const int occupied_count = use_lcao_zero_order + ? static_cast(response_kpoint->coefficients.size()) + : occupied_band_count(elec_state, response_k_index); + if (occupied_count <= 0) + { + throw std::runtime_error("No occupied DFT bands are available for a Sternheimer response record."); + } + occupied_band_counts.push_back(occupied_count); + } + const int requested_bands = positive_int_from_env(kBandsEnv, occupied_band_counts.front()); + const int fd_num_bands = std::min(requested_bands, elec_state.ekb.nc); + if (!use_lcao_zero_order && fd_num_bands < occupied_band_counts.front()) + { + throw std::runtime_error("Sternheimer chi0 output requires all occupied bands; use the smoke test for " + "band-limited debugging."); + } + + const int max_dense_size = positive_int_from_env(kMaxDenseEnv, 4096); + const int lanczos_max_subspace_size = positive_int_from_env(kLanczosSubspaceEnv, 320); + const double solver_tolerance + = positive_double_from_env(kSolverToleranceEnv, default_sternheimer_solver_tolerance()); + const int solver_max_iter = positive_int_from_env(kSolverMaxIterEnv, 300); + const double pca_threshold = nonnegative_double_from_env(kPCAThresholdEnv, runtime.input.exx_pca_threshold); + const double ccp_rmesh_times + = positive_double_from_env(kCCPRmeshTimesEnv, runtime.input.rpa_ccp_rmesh_times); + const int channel_worker_user_cap = int_from_env(kChannelMaxWorkersEnv, 0); + if (channel_worker_user_cap < 0) + { + throw std::invalid_argument(std::string("Invalid non-negative integer in ") + kChannelMaxWorkersEnv + "."); + } + const int default_frequency_rank_shift = use_frequency_mpi && runtime.nproc > 1 ? 1 : 0; + const int frequency_rank_shift = int_from_env(kFrequencyRankShiftEnv, default_frequency_rank_shift); + const auto frequency_assignment = [&](const int ifrequency) { + if (use_global_equation_mpi) + { + SternheimerRPA::FrequencyMPIAssignment assignment; + assignment.owns_frequency = true; + assignment.frequency_leader_rank = 0; + assignment.frequency_group_size = runtime.nproc; + assignment.frequency_group_local_rank = runtime.rank; + return assignment; + } + if (!use_frequency_mpi) + { + return SternheimerRPA::frequency_mpi_assignment(ifrequency, nfreq, 1, 0, 0, false); + } + return SternheimerRPA::frequency_mpi_assignment(ifrequency, + nfreq, + runtime.nproc, + runtime.rank, + frequency_rank_shift, + use_channel_mpi); + }; + const int frequency_group_size = frequency_assignment(0).frequency_group_size; +#ifdef __MPI + MPI_Comm chi0_frequency_group_communicator = MPI_COMM_NULL; + bool owns_chi0_frequency_group_communicator = false; + if (write_librpa && use_channel_mpi && frequency_group_size > 1) + { + if (use_global_equation_mpi) + { + chi0_frequency_group_communicator = MPI_COMM_WORLD; + } + else + { + const int frequency_group_color = runtime.rank / frequency_group_size; + if (MPI_Comm_split(MPI_COMM_WORLD, + frequency_group_color, + runtime.rank, + &chi0_frequency_group_communicator) + != MPI_SUCCESS) + { + throw std::runtime_error("Failed to create the Sternheimer chi0 frequency-group communicator."); + } + owns_chi0_frequency_group_communicator = true; + } + } +#endif + const std::string frequency_grid_file = runtime.input.sternheimer_freq_file; + const bool use_frequency_grid_file = !frequency_grid_file.empty(); + const bool use_delta_sternheimer = runtime.input.sternheimer_delta; + const int channel_batch_width = use_delta_sternheimer ? sternheimer_channel_batch_width() : 1; + if (use_analytic_ao && pca_threshold != runtime.input.exx_pca_threshold) + { + throw std::invalid_argument("isolated_ri cannot override the producer auxiliary-basis PCA threshold."); + } + const SternheimerDeltaABlockMode delta_a_block_mode = delta_a_block_mode_from_env(); + if (use_lcao_zero_order) + { + if (!use_delta_sternheimer) + { + throw std::runtime_error( + "Sternheimer LCAO zero-order input currently requires sternheimer_delta=true."); + } + if (std::any_of(response_kpoints.begin(), response_kpoints.end(), [](const auto* record) { + return record->coefficients.empty(); + })) + { + throw std::runtime_error("Sternheimer LCAO response record has no occupied coefficients."); + } + } + + SternheimerRPA::TransitionEnergyWindow transition_window; + transition_window.emin_ha = std::numeric_limits::infinity(); + transition_window.emax_ha = 0.0; + for (const SternheimerLCAOOccupiedKPoint* response_kpoint: response_kpoints) + { + const int response_k_index = response_kpoint == nullptr ? 0 : response_kpoint->local_k_index; + const std::vector eigenvalues_ry + = eigenvalues_ry_from_elec_state(elec_state, response_k_index); + const std::vector occupations + = occupations_from_elec_state(elec_state, response_k_index); + const SternheimerRPA::TransitionEnergyWindow spin_window + = SternheimerRPA::transition_energy_window_from_eigenvalues_ry(eigenvalues_ry, occupations); + transition_window.emin_ha = std::min(transition_window.emin_ha, spin_window.emin_ha); + transition_window.emax_ha = std::max(transition_window.emax_ha, spin_window.emax_ha); + } + const std::string frequency_grid_source = use_frequency_grid_file ? "file" : "greenx_minimax"; + const SternheimerRPA::FrequencyGrid frequency_grid + = use_frequency_grid_file + ? SternheimerRPA::read_frequency_grid_file(frequency_grid_file, nfreq) + : SternheimerRPA::generate_greenx_minimax_frequency_grid(nfreq, + transition_window.emin_ha, + transition_window.emax_ha); + const bool transition_window_available + = std::isfinite(transition_window.emin_ha) && std::isfinite(transition_window.emax_ha) + && transition_window.emax_ha > transition_window.emin_ha; + append_chi0_progress_event("frequency_grid", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "rank_shift=" + std::to_string(frequency_rank_shift), runtime); + + const std::array requested_response_dimensions = {runtime.input.sternheimer_response_nx, + runtime.input.sternheimer_response_ny, + runtime.input.sternheimer_response_nz}; + const bool response_grid_requested + = runtime.input.sternheimer_response_ecutwfc > 0.0 + || std::any_of(requested_response_dimensions.begin(), + requested_response_dimensions.end(), + [](const int value) { return value != 0; }); + if (response_grid_requested && (!use_delta_sternheimer || !use_lcao_zero_order || write_siab)) + { + throw std::invalid_argument( + "An independent molecular Sternheimer response grid requires LCAO Delta-ST without SIAB output."); + } + SternheimerResponseGrid response_grid = make_sternheimer_response_grid(pw_basis, + runtime.input.ecutwfc, + runtime.input.sternheimer_response_ecutwfc, + runtime.input.fft_mode, + requested_response_dimensions, + runtime.input.sternheimer_fd_order); + const ModulePW::PW_Basis& response_pw_basis = *response_grid.basis; + const SternheimerABACUSFDGridData grid_data + = use_frequency_mpi ? make_sternheimer_fd_full_grid(response_pw_basis) + : make_sternheimer_fd_grid(response_pw_basis); + if (runtime.rank == 0) + { + out << "sternheimer_response_ecutwfc_requested_Ry " + << runtime.input.sternheimer_response_ecutwfc << '\n'; + out << "sternheimer_response_ecutwfc_Ry " + << (response_grid.source == SternheimerResponseGridSource::Cutoff + ? runtime.input.sternheimer_response_ecutwfc + : (response_grid.source == SternheimerResponseGridSource::Pbe ? runtime.input.ecutwfc : 0.0)) + << '\n'; + out << "sternheimer_response_grid_requested " << requested_response_dimensions[0] << ' ' + << requested_response_dimensions[1] << ' ' << requested_response_dimensions[2] << '\n'; + out << "sternheimer_response_grid_source " << sternheimer_response_grid_source_name(response_grid.source) + << '\n'; + out << "pbe_grid " << pw_basis.nx << ' ' << pw_basis.ny << ' ' << pw_basis.nz << " size " + << pw_basis.nxyz << '\n'; + out << "response_grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' << grid_data.grid.nz + << " size " << grid_data.grid.size() << " dV " << grid_data.volume_element << '\n'; + } + SternheimerABFBuildData abfs_data + = build_abfs_ccp_data( + ucell, grid_data.grid, -1, pca_threshold, ccp_rmesh_times, write_siab, runtime); + std::vector& channels = abfs_data.channels; + const std::string abfs_source + = sternheimer_abfs_perturbation_source(runtime.abfs_files); + if (channels.empty()) + { + throw std::runtime_error("No ABFS CCP perturbation channels were generated."); + } + + const int raw_num_channels = static_cast(channels.size()); + if (write_siab && runtime.abfs_files.empty()) + { + throw std::runtime_error( + "out_sternheimer_siab requires explicit ABFS_ORBITAL files for immutable target provenance."); + } + const std::string auxiliary_basis_sha256 + = write_siab && runtime.rank == 0 + ? siab::sha256_unique_file_manifest(runtime.abfs_files) + : std::string(); + SternheimerCoulombWhitening coulomb_whitening; + if (write_siab) + { + coulomb_whitening = make_sternheimer_coulomb_whitening( + abfs_data.full_coulomb_metric, + raw_num_channels, + runtime.input.sternheimer_siab_vthr); + } + const int num_channels = write_siab ? coulomb_whitening.retained_rank : raw_num_channels; + append_chi0_progress_event("channels_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "raw_channels=" + std::to_string(raw_num_channels) + + " response_channels=" + std::to_string(num_channels) + + " user_cap=" + std::to_string(channel_worker_user_cap), runtime); + + SIABPrimitiveExportData siab_primitives; + SternheimerSIABMemoryEstimate siab_memory_estimate; + std::uint64_t siab_slurm_memory_bytes = 0; + if (write_siab) + { + siab_primitives = build_siab_primitive_export_data( + pw_basis, *siab_structure_factor, ucell, runtime); + append_chi0_progress_event("siab_primitives_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "nprimitive=" + std::to_string(siab_primitives.primitive_count), runtime); + int occupied_total = 0; + for (const int count: occupied_band_counts) + { + occupied_total += count; + } + siab_memory_estimate + = estimate_sternheimer_siab_dense_memory(grid_data.grid.size(), + raw_num_channels, + num_channels, + siab_primitives.primitive_count, + siab_primitives.reciprocal_count, + occupied_total, + nfreq); + siab_slurm_memory_bytes = slurm_memory_limit_bytes(); + constexpr std::uint64_t solver_reserve_bytes = 16ULL * 1024ULL * 1024ULL * 1024ULL; + if (siab_slurm_memory_bytes > 0 + && (siab_memory_estimate.total_bytes > siab_slurm_memory_bytes + || solver_reserve_bytes > siab_slurm_memory_bytes - siab_memory_estimate.total_bytes)) + { + throw std::runtime_error( + "Sternheimer SIAB estimated dense allocation plus the 16-GiB solver reserve exceeds " + "SLURM_MEM_PER_NODE."); + } + if (runtime.rank == 0) + { + std::ofstream memory("STERNHEIMER_SIAB_MEMORY.dat"); + if (!memory) + { + throw std::runtime_error("Failed to open Sternheimer SIAB memory diagnostic file."); + } + memory << "# ABACUS Sternheimer SIAB conservative dense-memory estimate\n"; + memory << "representation streamed_raw_hartree_and_serial_reciprocal_primitives_v1\n"; + memory << "coulomb_metric_bytes " << siab_memory_estimate.coulomb_metric_bytes << '\n'; + memory << "transformed_potential_bytes " << siab_memory_estimate.transformed_potential_bytes << '\n'; + memory << "channel_transform_workspace_bytes " + << siab_memory_estimate.channel_transform_workspace_bytes << '\n'; + memory << "reciprocal_primitive_bytes " << siab_memory_estimate.reciprocal_primitive_bytes << '\n'; + memory << "primitive_overlap_bytes " << siab_memory_estimate.primitive_overlap_bytes << '\n'; + memory << "gathered_reference_row_bytes " << siab_memory_estimate.gathered_reference_row_bytes << '\n'; + memory << "estimated_dense_total_bytes " << siab_memory_estimate.total_bytes << '\n'; + memory << "solver_reserve_bytes " << solver_reserve_bytes << '\n'; + memory << "slurm_memory_per_node_bytes " << siab_slurm_memory_bytes << '\n'; + } + append_chi0_progress_event("siab_memory_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "dense_bytes=" + std::to_string(siab_memory_estimate.total_bytes), runtime); + } + if (runtime.rank == 0) + { + write_abfs_channel_diagnostic("STERNHEIMER_ABFS_CHANNELS.dat", channels); + if (write_siab) + { + write_coulomb_whitening_diagnostic("STERNHEIMER_SIAB_COULOMB_WHITENING.dat", + coulomb_whitening); + } + if (sternheimer_abfs_diag_only_enabled()) + { + out << "status abfs_diag_only\n"; + out << "grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' << grid_data.grid.nz << " size " + << grid_data.grid.size() << " dV " << grid_data.volume_element << '\n'; + out << "frequency_grid_source " << frequency_grid_source << '\n'; + out << "nfreq " << nfreq << '\n'; + out << "abfs_channels " << raw_num_channels << '\n'; + out << "response_channels " << num_channels << '\n'; + out << "abfs_source " << abfs_source << '\n'; + } + } + if (sternheimer_abfs_diag_only_enabled()) + { + return; + } + + std::vector sampled_ao_functions; + if (use_lcao_zero_order) + { + sampled_ao_functions = build_lcao_candidate_grid_functions( + ucell, grid_data.grid, runtime, lcao_orbitals); + if (sampled_ao_functions.empty()) + { + throw std::runtime_error("Sternheimer LCAO zero-order input found no sampled AO functions."); + } + } + + SternheimerAOPotentials analytic_ao_potentials; + if (use_analytic_ao) + { + analytic_ao_potentials = read_sternheimer_ao_potentials(analytic_ao_path, + static_cast(sampled_ao_functions.size()), + num_channels); + } + const std::size_t response_count = response_kpoints.size(); + std::vector analytic_ao_tensors(response_count); + double analytic_ao_max_reconstruction_error = 0.0; + std::vector hamiltonians; + std::vector states_by_response(response_count); + std::vector> lcao_projector_functions_by_response(response_count); + std::vector> occupied_by_response(response_count); + std::vector> occupied_projector_by_response(response_count); + std::vector delta_subspaces(response_count); + std::vector delta_fixed_subspaces(response_count); + hamiltonians.reserve(response_count); + + for (std::size_t response_index = 0; response_index != response_count; ++response_index) + { + const SternheimerLCAOOccupiedKPoint* response_kpoint = response_kpoints[response_index]; + const int response_k_index = response_kpoint == nullptr ? 0 : response_kpoint->local_k_index; + const int response_spin_index = response_kpoint == nullptr ? 0 : response_kpoint->spin_index; + const SternheimerReducedKPoint response_grid_kpoint + = response_kpoint == nullptr ? SternheimerReducedKPoint{0.0, 0.0, 0.0} + : sternheimer_lcao_grid_kpoint(*response_kpoint); + hamiltonians.push_back([&]() { + if (response_grid.independent) + { + const auto fine_potential + = copy_sternheimer_full_local_potential(potential, pw_basis, response_spin_index); + const auto response_potential + = response_grid.source == SternheimerResponseGridSource::Explicit + ? restrict_sternheimer_real_field_rectangular(*response_grid.serial_fine_basis, + *response_grid.serial_response_basis, + fine_potential) + : restrict_sternheimer_real_field(*response_grid.serial_fine_basis, + *response_grid.serial_response_basis, + fine_potential); + SternheimerABACUSFDGridData response_grid_data = grid_data; + response_grid_data.grid.kpoint = response_grid_kpoint; + auto nonlocal_projector = make_sternheimer_fd_nonlocal_projector_from_unitcell( + ucell, response_grid_data.grid, response_grid_data.volume_element); + return make_sternheimer_fd_hamiltonian_from_local_potential(response_grid_data, + response_potential, + 1.0, + std::move(nonlocal_projector), + runtime.input.sternheimer_fd_order); + } + return use_frequency_mpi + ? make_sternheimer_fd_full_hamiltonian( + potential, + pw_basis, + ucell, + response_spin_index, + 1.0, + response_grid_kpoint, + runtime.input.sternheimer_fd_order) + : make_sternheimer_fd_hamiltonian( + potential, + pw_basis, + ucell, + response_spin_index, + 1.0, + response_grid_kpoint, + runtime.input.sternheimer_fd_order); + }()); + append_chi0_progress_event("hamiltonian_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "spin=" + std::to_string(response_spin_index + 1) + + " grid_size=" + std::to_string(grid_data.grid.size()), runtime); + + SternheimerFDZeroOrderStates& states = states_by_response[response_index]; + append_chi0_progress_event("zero_order_start", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + std::string("spin=") + std::to_string(response_spin_index + 1) + " " + + (use_lcao_zero_order + ? "lcao_sample" + : (runtime.rank == 0 ? "fd_solve" : "wait")), runtime); + if (use_lcao_zero_order) + { + const int spin_occupied_count = static_cast(response_kpoint->coefficients.size()); + std::vector lcao_occupied_functions; + lcao_occupied_functions.reserve(static_cast(spin_occupied_count)); + states.eigenvalues.reserve(static_cast(spin_occupied_count)); + states.wavefunctions.reserve(static_cast(spin_occupied_count)); + states.residual_norms.reserve(static_cast(spin_occupied_count)); + for (int ib = 0; ib != spin_occupied_count; ++ib) + { + const auto& coefficients + = response_kpoint->coefficients[static_cast(ib)]; + if (coefficients.size() != sampled_ao_functions.size()) + { + throw std::runtime_error( + "Sternheimer LCAO coefficient basis size does not match sampled AO functions."); + } + SternheimerDeltaGridFunction occupied_function + = linear_combination_delta_sternheimer_grid_functions(sampled_ao_functions, coefficients); + const double norm = sternheimer_fd_grid_norm(occupied_function.values, grid_data.volume_element); + if (norm <= runtime.input.sternheimer_vnorm_tol) + { + throw std::runtime_error("Sternheimer sampled LCAO occupied function has zero norm."); + } + const SternheimerFDHamiltonian::Complex inverse_norm(1.0 / norm, 0.0); + for (auto& value: occupied_function.values) + { + value *= inverse_norm; + } + for (auto& gradient: occupied_function.gradients) + { + for (auto& value: gradient) + { + value *= inverse_norm; + } + } + states.eigenvalues.push_back(response_kpoint->eigenvalues[static_cast(ib)]); + states.wavefunctions.push_back(occupied_function.values); + states.residual_norms.push_back(0.0); + lcao_occupied_functions.push_back(std::move(occupied_function)); + } + lcao_projector_functions_by_response[response_index] + = orthonormalize_delta_sternheimer_grid_functions( + lcao_occupied_functions, + grid_data.volume_element, + runtime.input.sternheimer_vnorm_tol); + } + else + { + if (!use_frequency_mpi || runtime.rank == 0) + { + states = solve_fd_zero_order_auto(hamiltonians[response_index], + fd_num_bands, + grid_data.volume_element, + max_dense_size, + lanczos_max_subspace_size); + } + broadcast_zero_order_states(states, grid_data.grid.size(), use_frequency_mpi, runtime); + } + append_chi0_progress_event("zero_order_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "spin=" + std::to_string(response_spin_index + 1) + " source=" + + (use_lcao_zero_order ? "lcao_ks" : "fd_grid") + + " nstates=" + std::to_string(states.wavefunctions.size()), runtime); + occupied_by_response[response_index] + = occupied_wavefunctions_from_states(states, elec_state, response_k_index); + if (occupied_by_response[response_index].empty()) + { + throw std::runtime_error("No occupied zero-order states are available for Sternheimer chi0 output."); + } + occupied_projector_by_response[response_index] = occupied_by_response[response_index]; + if (use_lcao_zero_order) + { + occupied_projector_by_response[response_index].clear(); + occupied_projector_by_response[response_index].reserve( + lcao_projector_functions_by_response[response_index].size()); + for (const SternheimerDeltaGridFunction& function: + lcao_projector_functions_by_response[response_index]) + { + occupied_projector_by_response[response_index].push_back(function.values); + } + } + } + + const std::vector> potentials + = write_siab + ? sample_sternheimer_abf_grid_channel_transform(abfs_data.radials_by_type, + abfs_data.atom_types, + abfs_data.atom_positions, + grid_data.grid, + channels, + coulomb_whitening.transform, + num_channels) + : collect_channel_potentials(channels); + // ABFS Coulomb potentials are in Ha units; the FD Hamiltonian and omega are in Ry. + // Keep Ha potentials for M=V chi0 V output, but use Ry perturbations in the linear equation. + const std::vector> perturbations_ry = scale_potentials(potentials, kHartreeToRydberg); + + if (use_delta_sternheimer) + { + append_chi0_progress_event("delta_subspace_start", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "candidate_orbitals", runtime); + std::vector loaded_candidate_functions; + const std::vector* candidate_functions = &sampled_ao_functions; + if (candidate_functions->empty()) + { + loaded_candidate_functions = build_lcao_candidate_grid_functions( + ucell, grid_data.grid, runtime); + candidate_functions = &loaded_candidate_functions; + } + if (candidate_functions->empty()) + { + throw std::runtime_error("Sternheimer delta mode found no sampled LCAO candidate orbitals."); + } + + for (std::size_t response_index = 0; response_index != response_count; ++response_index) + { + const SternheimerLCAOOccupiedKPoint* response_kpoint = response_kpoints[response_index]; + const int response_spin_index = response_kpoint == nullptr ? 0 : response_kpoint->spin_index; + std::vector fd_occupied_functions; + const std::vector* occupied_functions + = &lcao_projector_functions_by_response[response_index]; + if (occupied_functions->empty()) + { + fd_occupied_functions.reserve(occupied_by_response[response_index].size()); + for (const SternheimerFDHamiltonian::Vector& occupied_wavefunction: + occupied_by_response[response_index]) + { + fd_occupied_functions.push_back(make_delta_sternheimer_grid_function_with_fd_gradients( + occupied_wavefunction, grid_data.grid)); + } + occupied_functions = &fd_occupied_functions; + } + + SternheimerDeltaSubspaceOptions delta_options; + delta_options.max_virtual_states = sternheimer_delta_virtual_state_limit( + runtime.input.sternheimer_virt_max, + static_cast(candidate_functions->size()), + static_cast(occupied_functions->size())); + delta_options.norm_tolerance = runtime.input.sternheimer_vnorm_tol; + delta_subspaces[response_index] + = build_delta_sternheimer_subspace_by_mode(hamiltonians[response_index], + *occupied_functions, + *candidate_functions, + grid_data.volume_element, + delta_options, + delta_a_block_mode); + if (delta_subspaces[response_index].virtual_states.empty()) + { + throw std::runtime_error("Sternheimer delta mode produced no fixed virtual states."); + } + if (use_analytic_ao) + { + std::vector ao_states; + const auto& wavefunctions = states_by_response[response_index].wavefunctions; + for (const auto& state: wavefunctions) + { + ao_states.push_back(&state); + } + for (const auto& state: delta_subspaces[response_index].virtual_states) + { + ao_states.push_back(&state.orbital); + } + double reconstruction_error = 0.; + double gram_reciprocal_condition = 0.; + const auto coordinates = recover_delta_grid_ao_coefficients(sampled_ao_functions, + ao_states, + grid_data.volume_element, + 1.e-8, + reconstruction_error, + &gram_reciprocal_condition); + analytic_ao_max_reconstruction_error + = std::max(analytic_ao_max_reconstruction_error, reconstruction_error); + const std::vector> occupied_coordinates( + coordinates.begin(), + coordinates.begin() + wavefunctions.size()); + const std::vector> virtual_coordinates( + coordinates.begin() + wavefunctions.size(), + coordinates.end()); + analytic_ao_tensors[response_index] = contract_sternheimer_ao_potentials(analytic_ao_potentials, + occupied_coordinates, + virtual_coordinates); + if (runtime.rank == 0) + { + out << "analytic_ao_source " << analytic_ao_path << '\n'; + out << "analytic_ao_reconstruction_error " << reconstruction_error << '\n'; + out << "analytic_ao_gram_reciprocal_condition " << gram_reciprocal_condition << '\n'; + out << "analytic_ao_coupling_unit Hartree\n"; + } + } + delta_fixed_subspaces[response_index] + = build_delta_sternheimer_fixed_subspace(occupied_projector_by_response[response_index], + delta_subspaces[response_index].virtual_states); + append_chi0_progress_event( + "delta_subspace_ready", + 0, + -1, + -1, + -1, + 0, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "spin=" + std::to_string(response_spin_index + 1) + + " nvirtual=" + + std::to_string(delta_subspaces[response_index].virtual_states.size()), runtime); + } + } + + SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = solver_max_iter; + solver_options.residual_tol = solver_tolerance; + solver_options.use_fd_spectral_preconditioner + = sternheimer_environment_flag(kSpectralPreconditionerEnv, true); + solver_options.fd_spectral_preconditioner_regularization + = nonnegative_double_from_env(kSpectralPreconditionerRegularizationEnv, 0.0); + + bool all_converged = true; + int solved_equations = 0; + std::int64_t local_iteration_sum = 0; + double max_solver_relative_residual = 0.0; + double max_equation_residual_norm = 0.0; + double max_component_reconstruction_error = 0.0; + const std::vector auxiliary_channels + = write_librpa ? make_chi0_auxiliary_channels(channels) + : std::vector(); + + const std::size_t response_matrix_size + = write_librpa ? static_cast(num_channels) * static_cast(num_channels) : 0; + std::vector> chi0_branches(static_cast(nfreq)); + std::vector> chi0_sos_branches; + std::vector> chi0_pulay_branches; + std::vector> chi0_qspace_branches; + if (write_grid_diagnostics) + { + chi0_sos_branches.resize(static_cast(nfreq)); + chi0_pulay_branches.resize(static_cast(nfreq)); + chi0_qspace_branches.resize(static_cast(nfreq)); + } + std::vector frequency_start_times(static_cast(nfreq)); + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const SternheimerRPA::FrequencyMPIAssignment assignment = frequency_assignment(ifrequency); + const int owner_rank = assignment.frequency_leader_rank; + if (!assignment.owns_frequency) + { + continue; + } + if (write_librpa) + { + chi0_branches[static_cast(ifrequency)].assign(response_matrix_size, + SternheimerRPA::Complex(0.0, 0.0)); + if (write_grid_diagnostics) + { + chi0_sos_branches[static_cast(ifrequency)].assign( + response_matrix_size, SternheimerRPA::Complex(0.0, 0.0)); + chi0_pulay_branches[static_cast(ifrequency)].assign( + response_matrix_size, SternheimerRPA::Complex(0.0, 0.0)); + chi0_qspace_branches[static_cast(ifrequency)].assign( + response_matrix_size, SternheimerRPA::Complex(0.0, 0.0)); + } + } + frequency_start_times[static_cast(ifrequency)] = std::chrono::steady_clock::now(); + const double omega_ha = frequency_grid.omega_ha[static_cast(ifrequency)]; + append_chi0_progress_event("frequency_start", + ifrequency + 1, + owner_rank, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "omega_Ha=" + std::to_string(omega_ha) + + " group_size=" + std::to_string(assignment.frequency_group_size) + + " group_local_rank=" + + std::to_string(assignment.frequency_group_local_rank), runtime); + } + + std::vector occupied_state_offsets(response_count, 0); + int occupied_state_offset = 0; + for (std::size_t response_index = 0; response_index != response_count; ++response_index) + { + occupied_state_offsets[response_index] = occupied_state_offset; + occupied_state_offset += static_cast(states_by_response[response_index].wavefunctions.size()); + } + std::vector local_siab_rows; + const SternheimerMemorySnapshot channel_memory = detect_sternheimer_memory_snapshot(); + SternheimerChannelWorkerPlan channel_worker_plan; + bool channel_worker_plan_reported = false; + + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const SternheimerRPA::FrequencyMPIAssignment assignment = frequency_assignment(ifrequency); + const int owner_rank = assignment.frequency_leader_rank; + if (!assignment.owns_frequency) + { + continue; + } + const double omega_ha = frequency_grid.omega_ha[static_cast(ifrequency)]; + const double omega_ry = 2.0 * omega_ha; + std::vector* chi0_branch + = write_librpa ? &chi0_branches[static_cast(ifrequency)] : nullptr; + std::vector* chi0_sos_branch + = write_grid_diagnostics ? &chi0_sos_branches[static_cast(ifrequency)] : nullptr; + std::vector* chi0_pulay_branch + = write_grid_diagnostics ? &chi0_pulay_branches[static_cast(ifrequency)] : nullptr; + std::vector* chi0_qspace_branch + = write_grid_diagnostics ? &chi0_qspace_branches[static_cast(ifrequency)] : nullptr; + + for (std::size_t response_index = 0; response_index != response_count; ++response_index) + { + const SternheimerLCAOOccupiedKPoint* response_kpoint = response_kpoints[response_index]; + const int response_k_index = response_kpoint == nullptr ? 0 : response_kpoint->local_k_index; + const int response_spin_index = response_kpoint == nullptr ? 0 : response_kpoint->spin_index; + const SternheimerFDHamiltonian& hamiltonian = hamiltonians[response_index]; + const SternheimerFDZeroOrderStates& states = states_by_response[response_index]; + const auto& occupied = occupied_by_response[response_index]; + const auto& occupied_projector = occupied_projector_by_response[response_index]; + const SternheimerDeltaSubspace& delta_subspace = delta_subspaces[response_index]; + const SternheimerDeltaFixedSubspace& delta_fixed_subspace = delta_fixed_subspaces[response_index]; + + for (int ib = 0; ib != static_cast(states.wavefunctions.size()); ++ib) + { + const double occupation + = use_lcao_zero_order + ? sternheimer_lcao_weighted_occupation(*response_kpoint, ib) + : elec_state.wg(response_k_index, ib); + if (occupation <= 1.0e-8) + { + continue; + } + + struct ChannelEquationResult + { + int channel_index = -1; + int owner_rank = -1; + SternheimerRPA::SolverResult solver; + double equation_residual_norm = 0.0; + bool has_siab_row = false; + siab::ReferenceRow siab_row; + }; + + std::vector owned_channels; + std::vector equation_owner_ranks; + owned_channels.reserve(static_cast(num_channels)); + equation_owner_ranks.reserve(static_cast(num_channels)); + for (int ichannel = 0; ichannel != num_channels; ++ichannel) + { + int equation_owner_rank = owner_rank; + if (use_global_equation_mpi) + { + equation_owner_rank = SternheimerRPA::global_equation_owner( + occupied_state_offsets[response_index] + ib, + ifrequency, + ichannel, + nfreq, + num_channels, + runtime.nproc, + frequency_rank_shift); + if (equation_owner_rank != runtime.rank) + { + continue; + } + } + else if (use_channel_mpi) + { + const int group_owner = SternheimerRPA::channel_group_owner( + occupied_state_offsets[response_index] + ib, + ichannel, + num_channels, + assignment.frequency_group_size); + equation_owner_rank = assignment.frequency_leader_rank + group_owner; + if (group_owner != assignment.frequency_group_local_rank) + { + continue; + } + } + owned_channels.push_back(ichannel); + equation_owner_ranks.push_back(equation_owner_rank); + } + + if (owned_channels.empty()) + { + continue; + } + const SternheimerChannelWorkerPlan local_channel_worker_plan + = plan_sternheimer_owned_channel_workers(num_channels, + static_cast(owned_channels.size()), + sternheimer_channel_openmp_threads(), + grid_data.grid.size(), + channel_worker_user_cap, + channel_memory, + channel_batch_width); + const int local_channel_batch_width = local_channel_worker_plan.channel_batch_width; + if (!channel_worker_plan_reported) + { + channel_worker_plan = local_channel_worker_plan; + channel_worker_plan_reported = true; + append_chi0_progress_event( + "channel_workers_ready", + 0, + -1, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "global_channels=" + std::to_string(num_channels) + + " local_channels=" + std::to_string(owned_channels.size()) + " " + + format_sternheimer_channel_worker_diagnostic(channel_memory, + local_channel_worker_plan, + grid_data.grid.size(), + channel_worker_user_cap), runtime); + } + + const auto finalize_channel_result = [&](const int local_task, + const SternheimerFDHamiltonian::Vector& delta_wavefunction, + const SternheimerRPA::SolverResult& solver, + const double equation_residual_norm, + const SternheimerDeltaPostprocessResult* delta_response) { + const int ichannel = owned_channels[static_cast(local_task)]; + ChannelEquationResult result; + result.channel_index = ichannel; + result.owner_rank = equation_owner_ranks[static_cast(local_task)]; + result.solver = solver; + result.equation_residual_norm = equation_residual_norm; + if (write_grid_diagnostics && delta_response != nullptr) + { + if (use_analytic_ao) + { + accumulate_sternheimer_analytic_ao_column(analytic_ao_tensors[response_index], + ib, + delta_response->sos_coefficients, + occupation, + ichannel, + *chi0_sos_branch); + accumulate_sternheimer_analytic_ao_column(analytic_ao_tensors[response_index], + ib, + delta_response->pulay_coefficients, + occupation, + ichannel, + *chi0_pulay_branch); + } + else + { + SternheimerRPA::accumulate_chi0_branch_column(potentials, + states.wavefunctions[ib], + delta_response->in_sos_wavefunction, + grid_data.volume_element, + occupation, + ichannel, + *chi0_sos_branch); + SternheimerRPA::accumulate_chi0_branch_column(potentials, + states.wavefunctions[ib], + delta_response->in_pulay_wavefunction, + grid_data.volume_element, + occupation, + ichannel, + *chi0_pulay_branch); + } + SternheimerRPA::accumulate_chi0_branch_column(potentials, + states.wavefunctions[ib], + delta_response->out_wavefunction, + grid_data.volume_element, + occupation, + ichannel, + *chi0_qspace_branch); + } + if (write_siab && delta_response != nullptr) + { + const auto& complete_response = delta_response->reconstructed_wavefunction; + if (complete_response.size() != static_cast(grid_data.grid.size())) + { + throw std::runtime_error( + "Sternheimer SIAB requires each equation owner to hold a complete response grid."); + } + result.has_siab_row = true; + result.siab_row.occupied_state = occupied_state_offsets[response_index] + ib; + result.siab_row.auxiliary_channel = ichannel; + result.siab_row.frequency_index = ifrequency; + result.siab_row.frequency_ha = omega_ha; + result.siab_row.occupation = occupation; + result.siab_row.frequency_weight + = frequency_grid.weights_ha[static_cast(ifrequency)]; + result.siab_row.norm = siab::norm(complete_response, grid_data.volume_element); + result.siab_row.q + = project_siab_response_to_primitives(complete_response, ucell, siab_primitives); + } + if (write_librpa) + { + if (use_analytic_ao) + { + if (delta_response == nullptr) + { + throw std::runtime_error("Missing analytic AO Delta response."); + } + accumulate_sternheimer_analytic_ao_column(analytic_ao_tensors[response_index], + ib, + delta_response->coefficients, + occupation, + ichannel, + *chi0_branch); + SternheimerRPA::accumulate_chi0_branch_column(potentials, + states.wavefunctions[ib], + delta_response->out_wavefunction, + grid_data.volume_element, + occupation, + ichannel, + *chi0_branch); + } + else + { + SternheimerRPA::accumulate_chi0_branch_column(potentials, + states.wavefunctions[ib], + delta_wavefunction, + grid_data.volume_element, + occupation, + ichannel, + *chi0_branch); + } + } + return result; + }; + + const auto get_delta_couplings = [&](const int ichannel, const std::vector& perturbation) { + if (!use_analytic_ao) + { + return delta_sternheimer_perturbation_matrix_elements(delta_subspace.virtual_states, + perturbation, + states.wavefunctions[ib], + grid_data.volume_element); + } + const auto& tensor = analytic_ao_tensors[response_index]; + std::vector couplings(tensor.virtuals); + for (int a = 0; a < tensor.virtuals; ++a) + { + couplings[a] = kHartreeToRydberg * tensor.at(ib, a, ichannel); + } + return couplings; + }; + + std::vector channel_results; + if (local_channel_batch_width == 1 || !use_delta_sternheimer) + { + channel_results = run_sternheimer_channel_tasks( + static_cast(owned_channels.size()), + [&](const int local_task) { + const int ichannel = owned_channels[static_cast(local_task)]; + const std::size_t channel_index = static_cast(ichannel); + SternheimerFDHamiltonian::Vector rhs; + SternheimerRPA::build_rhs_from_hartree_perturbation(perturbations_ry[channel_index], + states.wavefunctions[ib], + rhs); + if (use_delta_sternheimer) + { + const auto perturbation_matrix_elements + = get_delta_couplings(ichannel, perturbations_ry[channel_index]); + const SternheimerDeltaLinearResponse response + = solve_delta_sternheimer_linear_response(hamiltonian, + delta_fixed_subspace, + states.eigenvalues[ib], + rhs, + delta_subspace.virtual_states, + perturbation_matrix_elements, + omega_ry, + grid_data.volume_element, + solver_options); + return finalize_channel_result(local_task, + response.response.reconstructed_wavefunction, + response.solver, + response.residual_norm, + &response.response); + } + const SternheimerFDLinearResponse response + = solve_sternheimer_fd_linear_response(hamiltonian, + occupied, + states.eigenvalues[ib], + rhs, + omega_ry, + grid_data.volume_element, + solver_options); + return finalize_channel_result(local_task, + response.delta_wavefunction, + response.solver, + response.residual_norm, + nullptr); + }, + local_channel_worker_plan.effective_workers); + } + else + { + const std::vector channel_batches + = make_sternheimer_channel_batches(static_cast(owned_channels.size()), + local_channel_batch_width); + std::vector> grouped_results + = run_sternheimer_channel_tasks>( + static_cast(channel_batches.size()), + [&](const int batch_task) { + const SternheimerChannelBatch batch + = channel_batches[static_cast(batch_task)]; + SternheimerFDHamiltonian::Matrix rhs_batch(static_cast(batch.size)); + std::vector> + perturbation_matrix_elements(static_cast(batch.size)); + for (int offset = 0; offset != batch.size; ++offset) + { + const int local_task = batch.begin + offset; + const int ichannel = owned_channels[static_cast(local_task)]; + const std::size_t channel_index = static_cast(ichannel); + SternheimerRPA::build_rhs_from_hartree_perturbation( + perturbations_ry[channel_index], + states.wavefunctions[ib], + rhs_batch[static_cast(offset)]); + perturbation_matrix_elements[static_cast(offset)] + = get_delta_couplings(ichannel, perturbations_ry[channel_index]); + } + const std::vector responses + = solve_delta_sternheimer_linear_response_batch(hamiltonian, + delta_fixed_subspace, + states.eigenvalues[ib], + rhs_batch, + delta_subspace.virtual_states, + perturbation_matrix_elements, + omega_ry, + grid_data.volume_element, + solver_options); + std::vector results; + results.reserve(static_cast(batch.size)); + for (int offset = 0; offset != batch.size; ++offset) + { + const int local_task = batch.begin + offset; + const SternheimerDeltaLinearResponse& response + = responses[static_cast(offset)]; + results.push_back( + finalize_channel_result(local_task, + response.response.reconstructed_wavefunction, + response.solver, + response.residual_norm, + &response.response)); + } + return results; + }, + local_channel_worker_plan.effective_workers); + channel_results.reserve(owned_channels.size()); + for (std::vector& group: grouped_results) + { + for (ChannelEquationResult& result: group) + { + channel_results.push_back(std::move(result)); + } + } + } + + for (ChannelEquationResult& result: channel_results) + { + if (result.has_siab_row) + { + local_siab_rows.push_back(std::move(result.siab_row)); + } + all_converged = all_converged && result.solver.converged; + ++solved_equations; + local_iteration_sum += result.solver.iterations; + max_solver_relative_residual + = std::max(max_solver_relative_residual, result.solver.relative_residual); + max_equation_residual_norm + = std::max(max_equation_residual_norm, result.equation_residual_norm); + append_chi0_progress_event("equation", + ifrequency + 1, + result.owner_rank, + ib, + result.channel_index, + solved_equations, + &result.solver, + result.equation_residual_norm, + elapsed_seconds_since(chi0_start_time), + "spin=" + std::to_string(response_spin_index + 1) + " " + + (use_delta_sternheimer ? "delta" : "standard"), runtime); + } + } + } + + } + + std::vector global_siab_rows; + if (write_siab) + { +#ifdef __MPI + global_siab_rows = siab::gather_reference_rows_to_root(local_siab_rows, + static_cast(siab_primitives.primitive_count), + 0, + MPI_COMM_WORLD); +#else + global_siab_rows = siab::gather_reference_rows_to_root(local_siab_rows, + static_cast(siab_primitives.primitive_count), + 0); +#endif + if (runtime.rank == 0) + { + std::size_t occupied_total = 0; + for (const int count: occupied_band_counts) + { + occupied_total += static_cast(count); + } + const std::size_t expected_rows + = occupied_total * static_cast(num_channels) * static_cast(nfreq); + if (global_siab_rows.size() != expected_rows) + { + throw std::runtime_error("Sternheimer SIAB global row assembly has missing or duplicate rows."); + } + const siab::Provenance provenance = make_siab_production_provenance(ucell, + auxiliary_basis_sha256, + frequency_grid, + pca_threshold, + coulomb_whitening, + runtime); + siab::write_v1(join_output_path(output_dir, "sternheimer_matrix.dat"), + grid_data.volume_element, + siab_primitives.blocks, + global_siab_rows, + siab_primitives.overlap_s, + provenance); + runtime.log << " Sternheimer SIAB v1 output: " + << join_output_path(output_dir, "sternheimer_matrix.dat") << std::endl; + } + } + + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const SternheimerRPA::FrequencyMPIAssignment assignment = frequency_assignment(ifrequency); + const int owner_rank = assignment.frequency_leader_rank; + if (!assignment.owns_frequency) + { + continue; + } + const double omega_ha = frequency_grid.omega_ha[static_cast(ifrequency)]; + if (write_librpa) + { +#ifdef __MPI + if (use_channel_mpi && assignment.frequency_group_size > 1) + { + sternheimer_chi0::reduce_branch_to_root(chi0_branches[static_cast(ifrequency)], + 0, + chi0_frequency_group_communicator); + if (write_grid_diagnostics) + { + sternheimer_chi0::reduce_branch_to_root( + chi0_sos_branches[static_cast(ifrequency)], + 0, + chi0_frequency_group_communicator); + sternheimer_chi0::reduce_branch_to_root( + chi0_pulay_branches[static_cast(ifrequency)], + 0, + chi0_frequency_group_communicator); + sternheimer_chi0::reduce_branch_to_root( + chi0_qspace_branches[static_cast(ifrequency)], + 0, + chi0_frequency_group_communicator); + } + } +#endif + const bool writes_frequency = !use_channel_mpi || assignment.frequency_group_local_rank == 0; + if (writes_frequency) + { + const std::vector chi0 + = SternheimerRPA::symmetrize_chi0_imaginary_frequency( + chi0_branches[static_cast(ifrequency)], num_channels); + const SternheimerRPA::Chi0V1Metadata metadata + = make_chi0_v1_metadata(ucell, + channels, + 1, + ifrequency + 1, + omega_ha, + frequency_grid.weights_ha[static_cast(ifrequency)]); + const std::string data_file + = output_file(chi0_v1_filename(metadata.iq, metadata.ifrequency, runtime.rank)); + if (write_grid_diagnostics) + { + const std::vector chi0_sos + = SternheimerRPA::symmetrize_chi0_imaginary_frequency( + chi0_sos_branches[static_cast(ifrequency)], num_channels); + const std::vector chi0_pulay + = SternheimerRPA::symmetrize_chi0_imaginary_frequency( + chi0_pulay_branches[static_cast(ifrequency)], num_channels); + const std::vector chi0_qspace + = SternheimerRPA::symmetrize_chi0_imaginary_frequency( + chi0_qspace_branches[static_cast(ifrequency)], num_channels); + const double reconstruction_error + = relative_component_reconstruction_error(chi0, chi0_sos, chi0_pulay, chi0_qspace); + if (reconstruction_error > 1.0e-10) + { + throw std::runtime_error( + "Sternheimer response components do not reconstruct the total response matrix."); + } + max_component_reconstruction_error + = std::max(max_component_reconstruction_error, reconstruction_error); + SternheimerRPA::write_chi0_v1_file(data_file, metadata, auxiliary_channels, chi0); + const std::string sos_file + = output_file(sternheimer_component_v1_filename("sos", metadata.iq, metadata.ifrequency, runtime.rank)); + const std::string pulay_file + = output_file(sternheimer_component_v1_filename("pulay", metadata.iq, metadata.ifrequency, runtime.rank)); + const std::string qspace_file + = output_file(sternheimer_component_v1_filename("qspace", metadata.iq, metadata.ifrequency, runtime.rank)); + SternheimerRPA::write_chi0_v1_file(sos_file, metadata, auxiliary_channels, chi0_sos); + SternheimerRPA::write_chi0_v1_file(pulay_file, metadata, auxiliary_channels, chi0_pulay); + SternheimerRPA::write_chi0_v1_file(qspace_file, metadata, auxiliary_channels, chi0_qspace); + } + else + { + SternheimerRPA::write_chi0_v1_file(data_file, metadata, auxiliary_channels, chi0); + } + runtime.log << " Sternheimer chi0 v1 output: " << data_file << std::endl; + } + } + append_chi0_progress_event("frequency_finish", + ifrequency + 1, + owner_rank, + -1, + -1, + solved_equations, + nullptr, + -1.0, + elapsed_seconds_since(chi0_start_time), + "elapsed_freq_s=" + + std::to_string(elapsed_seconds_since( + frequency_start_times[static_cast(ifrequency)])), runtime); + } + + const int local_solved_equations = solved_equations; + int rank_local_equations_min = local_solved_equations; + int rank_local_equations_max = local_solved_equations; + std::int64_t rank_local_iterations_min = local_iteration_sum; + std::int64_t rank_local_iterations_max = local_iteration_sum; +#ifdef __MPI + if (use_distributed_mpi && runtime.nproc > 1) + { + MPI_Allreduce(&local_solved_equations, + &rank_local_equations_min, + 1, + MPI_INT, + MPI_MIN, + MPI_COMM_WORLD); + MPI_Allreduce(&local_solved_equations, + &rank_local_equations_max, + 1, + MPI_INT, + MPI_MAX, + MPI_COMM_WORLD); + MPI_Allreduce(&local_iteration_sum, + &rank_local_iterations_min, + 1, + MPI_INT64_T, + MPI_MIN, + MPI_COMM_WORLD); + MPI_Allreduce(&local_iteration_sum, + &rank_local_iterations_max, + 1, + MPI_INT64_T, + MPI_MAX, + MPI_COMM_WORLD); + } +#endif +#ifdef __MPI + if (write_grid_diagnostics && runtime.nproc > 1) + { + MPI_Allreduce(MPI_IN_PLACE, + &max_component_reconstruction_error, + 1, + MPI_DOUBLE, + MPI_MAX, + MPI_COMM_WORLD); + } +#endif + +#ifdef __MPI + if (owns_chi0_frequency_group_communicator) + { + MPI_Comm_free(&chi0_frequency_group_communicator); + } +#endif + + reduce_chi0_output_stats(all_converged, + solved_equations, + max_solver_relative_residual, + max_equation_residual_norm, + use_frequency_mpi, + runtime); + +#ifdef __MPI + if (use_parallel_response_mpi && runtime.nproc > 1) + { + MPI_Barrier(MPI_COMM_WORLD); + } +#endif + + std::vector> index_entries; + if (write_librpa && runtime.rank == 0) + { + index_entries.reserve(frequency_grid.omega_ha.size()); + for (int ifrequency = 0; ifrequency != nfreq; ++ifrequency) + { + const SternheimerRPA::Chi0V1Metadata metadata + = make_chi0_v1_metadata(ucell, + channels, + 1, + ifrequency + 1, + frequency_grid.omega_ha[static_cast(ifrequency)], + frequency_grid.weights_ha[static_cast(ifrequency)]); + const int owner_rank = frequency_assignment(ifrequency).frequency_leader_rank; + index_entries.push_back({chi0_v1_filename(metadata.iq, metadata.ifrequency, owner_rank), metadata}); + } + write_chi0_index_file(output_file("v1_sternheimer_chi0_index.dat"), index_entries); + } + + const int grid_size = grid_data.grid.nx * grid_data.grid.ny * grid_data.grid.nz; + if (runtime.rank != 0) + { + return; + } + + out << "status success\n"; + out << ::ModuleRI::preorth_report(abfs_data.preorth_report); + out << "format " << (write_siab ? "siab_v1" : "librpa_v1") << '\n'; + out << "data_files " << (write_siab ? 1 : index_entries.size()) << '\n'; + out << "index_file " << (write_siab ? "none" : "v1_sternheimer_chi0_index.dat") << '\n'; + out << "grid " << grid_data.grid.nx << ' ' << grid_data.grid.ny << ' ' << grid_data.grid.nz << " size " + << grid_size << " dV " << grid_data.volume_element << '\n'; + out << "nfreq " << nfreq << '\n'; + out << "frequency_grid_source " << frequency_grid_source << '\n'; + if (use_frequency_grid_file) + { + out << "frequency_grid_file " << frequency_grid_file << '\n'; + } + if (transition_window_available) + { + out << "transition_window_Ha " << transition_window.emin_ha << ' ' << transition_window.emax_ha << '\n'; + } + else + { + out << "transition_window_Ha unavailable_external_grid\n"; + } + out << "sternheimer_frequency_mpi " << (use_frequency_mpi ? "yes" : "no") << '\n'; + out << "sternheimer_channel_mpi " << (use_channel_mpi ? "yes" : "no") << '\n'; + out << "sternheimer_mpi_layout " << mpi_layout << '\n'; + out << "equation_owner_formula " + << (use_global_equation_mpi ? "occupied_frequency_channel_modulo" : "frequency_group_assignment") + << '\n'; + out << "frequency_group_size " << frequency_group_size << '\n'; + out << "sternheimer_channel_threads " << channel_worker_plan.effective_workers << '\n'; + out << "mpi_ranks " << runtime.nproc << '\n'; + out << "frequency_rank_shift " << frequency_rank_shift << '\n'; + out << "rank_local_equations_min " << rank_local_equations_min << '\n'; + out << "rank_local_equations_max " << rank_local_equations_max << '\n'; + out << "rank_local_iterations_min " << rank_local_iterations_min << '\n'; + out << "rank_local_iterations_max " << rank_local_iterations_max << '\n'; + out << "progress_file_pattern " + << (write_siab ? "STERNHEIMER_SIAB_PROGRESS_rank*.dat" + : "STERNHEIMER_CHI0_PROGRESS_rank*.dat") + << '\n'; + out << "ifrequency omega_Ha weight_Ha omega_Ry data_file\n"; + for (const auto& entry: index_entries) + { + const SternheimerRPA::Chi0V1Metadata& metadata = entry.second; + out << metadata.ifrequency << ' ' << metadata.omega << ' ' << metadata.weight << ' ' << 2.0 * metadata.omega + << ' ' << entry.first << '\n'; + } + out << "pca_threshold " << pca_threshold << '\n'; + out << "ccp_rmesh_times " << ccp_rmesh_times << '\n'; + out << "solver_tolerance " << solver_tolerance << '\n'; + out << "sternheimer_zero_order_source " << (use_lcao_zero_order ? "lcao_ks" : "fd_grid") << '\n'; + out << "sternheimer_lcao_virtual_source " << (use_lcao_zero_order ? "ks_bands" : "none") << '\n'; + out << "analytic_ao_coupling " << (use_analytic_ao ? "yes" : "no") << '\n'; + out << "sternheimer_molecular_coulomb " << runtime.input.sternheimer_molecular_coulomb << '\n'; + out << "molecular_coulomb_tail " << (use_analytic_ao ? "isolated_charge_multipole" : "none") << '\n'; + if (use_analytic_ao) + { + out << "analytic_ao_reconstruction_max_relative_error " << analytic_ao_max_reconstruction_error << '\n'; + out << "analytic_ao_contract rhs_and_left_projection\n"; + out << "analytic_ao_qspace re_solved_grid_rhs\n"; + out << "analytic_ao_residual hybrid_ao_qspace_equations\n"; + } + out << "sternheimer_lcao_unoccupied_bands_per_spin"; + for (const SternheimerDeltaSubspace& delta_subspace: delta_subspaces) + { + out << ' ' << delta_subspace.virtual_states.size(); + } + out << '\n'; + out << "sternheimer_response_records " << response_count << '\n'; + out << "sternheimer_response_spin_channels"; + for (const SternheimerLCAOOccupiedKPoint* response_kpoint: response_kpoints) + { + out << ' ' << (response_kpoint == nullptr ? 1 : response_kpoint->spin_index + 1); + } + out << '\n'; + if (response_count == 1) + { + const SternheimerLCAOOccupiedKPoint* response_kpoint = response_kpoints.front(); + out << "sternheimer_response_spin_channel " + << (response_kpoint == nullptr ? 1 : response_kpoint->spin_index + 1) << '\n'; + out << "sternheimer_response_local_k_index " + << (response_kpoint == nullptr ? 1 : response_kpoint->local_k_index + 1) << '\n'; + if (response_kpoint != nullptr) + { + out << "sternheimer_response_global_k_index " << response_kpoint->global_k_index + 1 << '\n'; + out << "sternheimer_response_kpoint " << response_kpoint->kpoint[0] << ' ' + << response_kpoint->kpoint[1] << ' ' << response_kpoint->kpoint[2] << '\n'; + out << "sternheimer_response_kweight " << response_kpoint->kweight << '\n'; + } + } + std::size_t occupied_bands_total = 0; + std::size_t occupied_projector_total = 0; + for (std::size_t response_index = 0; response_index != response_count; ++response_index) + { + occupied_bands_total += occupied_by_response[response_index].size(); + occupied_projector_total += occupied_projector_by_response[response_index].size(); + } + out << "occupied_bands " << occupied_bands_total << '\n'; + out << "occupied_bands_per_spin"; + for (const auto& occupied: occupied_by_response) + { + out << ' ' << occupied.size(); + } + out << '\n'; + out << "occupied_projector_dimension " << occupied_projector_total << '\n'; + out << "occupied_projector_dimensions_per_spin"; + for (const auto& occupied_projector: occupied_projector_by_response) + { + out << ' ' << occupied_projector.size(); + } + out << '\n'; + out << "abfs_channels " << num_channels << '\n'; + out << "abfs_source " << abfs_source << '\n'; + out << "sternheimer_delta " << (use_delta_sternheimer ? "yes" : "no") << '\n'; + out << "sternheimer_grid_diagnostics " << (write_grid_diagnostics ? "yes" : "no") << '\n'; + out << "sternheimer_fd_order " << runtime.input.sternheimer_fd_order << '\n'; + out << "sternheimer_preconditioner " + << (solver_options.use_fd_spectral_preconditioner ? "fd_spectral" : "none") << '\n'; + out << "sternheimer_preconditioner_regularization_Ry " + << solver_options.fd_spectral_preconditioner_regularization << '\n'; + if (write_grid_diagnostics) + { + out << "sternheimer_component_reconstruction_error_max " << max_component_reconstruction_error << '\n'; + out << "sternheimer_component_file_pattern " + "v1_sternheimer_component_{sos,pulay,qspace}_iq__ifreq__rank.dat\n"; + out << "sternheimer_grid_matrix_file_pattern STERNHEIMER_DELTA_GRID_MATRICES_spin_.dat\n"; + out << "sternheimer_perturbation_file_pattern STERNHEIMER_DELTA_PERTURBATION_spin_.dat\n"; + out << "sternheimer_perturbation_unit Rydberg\n"; + } + if (use_delta_sternheimer) + { + out << "sternheimer_delta_backend " << sternheimer_delta_a_block_mode_name(delta_a_block_mode) << '\n'; + out << "sternheimer_virt_max " << runtime.input.sternheimer_virt_max << '\n'; + out << "sternheimer_vnorm_tol " << runtime.input.sternheimer_vnorm_tol << '\n'; + std::size_t virtual_states_total = 0; + int accepted_candidates_total = 0; + int discarded_candidates_total = 0; + for (std::size_t response_index = 0; response_index != response_count; ++response_index) + { + virtual_states_total += delta_subspaces[response_index].virtual_states.size(); + accepted_candidates_total += delta_subspaces[response_index].accepted_candidates; + discarded_candidates_total += delta_subspaces[response_index].discarded_candidates; + out << "sternheimer_delta_spin " << response_index + 1 << " virtual_states " + << delta_subspaces[response_index].virtual_states.size() << " accepted_candidates " + << delta_subspaces[response_index].accepted_candidates << " discarded_candidates " + << delta_subspaces[response_index].discarded_candidates << '\n'; + } + out << "sternheimer_delta_virtual_states " << virtual_states_total << '\n'; + out << "sternheimer_delta_accepted_candidates " << accepted_candidates_total << '\n'; + out << "sternheimer_delta_discarded_candidates " << discarded_candidates_total << '\n'; + } + out << "solved_equations " << solved_equations << '\n'; + out << "all_converged " << (all_converged ? "yes" : "no") << '\n'; + out << "max_solver_relative_residual " << max_solver_relative_residual << '\n'; + out << "max_equation_residual_norm " << max_equation_residual_norm << '\n'; + runtime.log << " Sternheimer chi0 status: " << status_path << std::endl; + } + catch (const std::exception& error) + { + { + std::ofstream rank_failure( + chi0_failure_filename(runtime.rank).c_str(), std::ios::out | std::ios::trunc); + rank_failure << "rank " << runtime.rank << '\n'; + rank_failure << "reason " << error.what() << '\n'; + } + if (runtime.rank == 0 && out) + { + out << "status failed\n"; + out << "reason " << error.what() << '\n'; + } + runtime.log << " Sternheimer chi0 output failed: " << error.what() << std::endl; + runtime.log << " Sternheimer chi0 status: " << status_path << std::endl; + runtime.log.flush(); + const bool invalid_excitation_spectrum + = dynamic_cast(&error) != nullptr; + const bool fatal_spectrum_failure = invalid_excitation_spectrum + || env_is_true("ABACUS_STERNHEIMER_WEAK_Q_UNIT") + || env_is_true("ABACUS_STERNHEIMER_WEAK_RESPONSE_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_WEAK_MATRIX_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_SPECTRUM_AUDIT") + || env_is_true("ABACUS_STERNHEIMER_GALERKIN_AUDIT"); + if (fatal_spectrum_failure) + { + out.flush(); + std::cerr << "Fatal Sternheimer spectrum or audit: " << error.what() << std::endl; + } +#ifdef __MPI + if ((use_parallel_response_mpi || fatal_spectrum_failure) && runtime.nproc > 1) + { + MPI_Abort(MPI_COMM_WORLD, 1); + } +#endif + if (fatal_spectrum_failure) + { + std::exit(EXIT_FAILURE); + } + } +} + +void run_sternheimer_abacus_chi0_output(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const std::string& output_dir, + const SternheimerAbacusRuntime& runtime) +{ + run_sternheimer_abacus_chi0_output_impl(potential, + pw_basis, + ucell, + elec_state, + output_dir, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + runtime); +} + +void run_sternheimer_abacus_lcao_chi0_output(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const LCAO_Orbitals& orbitals, + const std::vector& occupied_kpoints, + const std::array& kmesh, + const ModulePW::PW_Basis_K* pw_wfc, + const Structure_Factor* structure_factor, + const std::string& output_dir, + const SternheimerAbacusRuntime& runtime, + const SternheimerOrbitalSet* reusable_rpa_abfs) +{ + run_sternheimer_abacus_chi0_output_impl(potential, + pw_basis, + ucell, + elec_state, + output_dir, + &orbitals, + &occupied_kpoints, + &kmesh, + pw_wfc, + structure_factor, + reusable_rpa_abfs, + runtime); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h new file mode 100644 index 00000000000..a4ba207312d --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h @@ -0,0 +1,1716 @@ +#ifndef STERNHEIMER_ABACUS_ST_SMOKE_H +#define STERNHEIMER_ABACUS_ST_SMOKE_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class UnitCell; +class LCAO_Orbitals; +class Structure_Factor; +class Input_para; + +namespace ModulePW +{ +class PW_Basis; +class PW_Basis_K; +} + +namespace elecstate +{ +class ElecState; +class Potential; +} // namespace elecstate + +namespace ModuleRI +{ + +struct SternheimerAbacusRuntime +{ + const Input_para& input; + int nlocal; + int rank; + int nproc; + int kpar_lcao; + int nthread_per_proc; + bool deepks_setorb; + const std::vector& abfs_files; + double abfs_kmesh_times; + std::ofstream& log; +}; + +inline constexpr double default_sternheimer_solver_tolerance() noexcept +{ + return 1.0e-6; +} + +struct SternheimerLCAOOccupiedKPoint +{ + int local_k_index = -1; + int global_k_index = -1; + int zero_order_k_index = -1; + int symmetry_spatial_isym = 0; + bool symmetry_time_reversal = false; + int spin_index = -1; + SternheimerReducedKPoint kpoint{0.0, 0.0, 0.0}; + bool has_grid_kpoint_override = false; + SternheimerReducedKPoint grid_kpoint{0.0, 0.0, 0.0}; + double kweight = 0.0; + std::vector eigenvalues; + std::vector occupations; + std::vector>> coefficients; + std::vector unoccupied_eigenvalues; + std::vector>> unoccupied_coefficients; +}; + +inline const SternheimerReducedKPoint& sternheimer_lcao_grid_kpoint( + const SternheimerLCAOOccupiedKPoint& record) +{ + return record.has_grid_kpoint_override ? record.grid_kpoint : record.kpoint; +} + +inline bool sternheimer_full_k_reconstruction_required(const int stored_kpoint_count, + const int full_kpoint_count, + const int spin_channel_count, + const int symmetry_flag) +{ + if (stored_kpoint_count <= 0 || full_kpoint_count <= 0 || spin_channel_count <= 0) + { + throw std::invalid_argument("Invalid Sternheimer k-point reconstruction dimensions."); + } + const long long full_record_count + = static_cast(full_kpoint_count) * spin_channel_count; + if (symmetry_flag != 1 || stored_kpoint_count == full_record_count) + { + return false; + } + if (spin_channel_count != 1) + { + throw std::invalid_argument( + "Sternheimer full-k reconstruction supports only nspin=1."); + } + return true; +} + +inline SternheimerLCAOOccupiedKPoint make_sternheimer_full_kpoint_record( + const SternheimerLCAOOccupiedKPoint& ibz_record, + const int full_k_index, + const SternheimerReducedKPoint& kpoint, + const double full_kweight, + std::vector>> occupied_coefficients, + std::vector>> unoccupied_coefficients = {}) +{ + if (full_k_index < 0 || !std::isfinite(full_kweight) || full_kweight <= 0.0 + || occupied_coefficients.size() != ibz_record.coefficients.size() + || (!ibz_record.unoccupied_coefficients.empty() + && unoccupied_coefficients.size() != ibz_record.unoccupied_coefficients.size())) + { + throw std::invalid_argument("Invalid full-grid Sternheimer LCAO record data."); + } + SternheimerLCAOOccupiedKPoint record = ibz_record; + record.local_k_index = full_k_index; + record.global_k_index = full_k_index; + record.kpoint = kpoint; + record.kweight = full_kweight; + record.coefficients = std::move(occupied_coefficients); + record.unoccupied_coefficients = std::move(unoccupied_coefficients); + return record; +} + +inline std::vector sternheimer_canonical_q_indices_one_based( + const std::vector& records) +{ + if (records.empty()) + { + throw std::invalid_argument("Cannot identify canonical Sternheimer q points on an empty grid."); + } + int zero_order_count = 0; + for (const auto& record: records) + { + if (record.global_k_index < 0 || record.zero_order_k_index < 0) + { + throw std::invalid_argument("Sternheimer q-point metadata contain a negative index."); + } + zero_order_count = std::max(zero_order_count, record.zero_order_k_index + 1); + } + std::vector representatives(static_cast(zero_order_count), -1); + for (const auto& record: records) + { + if (record.symmetry_spatial_isym != 0 || record.symmetry_time_reversal) + { + continue; + } + int& representative = representatives[static_cast(record.zero_order_k_index)]; + if (representative >= 0) + { + throw std::invalid_argument("A Sternheimer q star has multiple canonical full-q points."); + } + representative = record.global_k_index + 1; + } + if (std::any_of(representatives.begin(), representatives.end(), [](const int index) { return index <= 0; })) + { + throw std::invalid_argument("A Sternheimer q star has no canonical full-q point."); + } + return representatives; +} + +inline int sternheimer_coulomb_reader_q_index_one_based( + const std::vector& records, + const int full_q_index) +{ + if (records.empty() || full_q_index <= 0 + || full_q_index > static_cast(records.size())) + { + throw std::invalid_argument("Cannot map an invalid full-q index to the Coulomb reader-v1 star."); + } + const int global_q_index = full_q_index - 1; + const SternheimerLCAOOccupiedKPoint* selected = nullptr; + for (const auto& record : records) + { + if (record.global_k_index == global_q_index) + { + if (selected != nullptr) + { + throw std::invalid_argument("Coulomb reader-v1 q mapping found a duplicate full-q record."); + } + selected = &record; + } + } + if (selected == nullptr || selected->zero_order_k_index < 0) + { + throw std::invalid_argument("Coulomb reader-v1 q mapping found incomplete zero-order metadata."); + } + return selected->zero_order_k_index + 1; +} + +inline bool sternheimer_lcao_sos_diagnostic_enabled() +{ + const char* raw = std::getenv("ABACUS_STERNHEIMER_LCAO_SOS_DIAG"); + if (raw == nullptr) + { + return false; + } + std::string value(raw); + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return !(value.empty() || value == "0" || value == "false" || value == "off" || value == "no"); +} + +inline bool sternheimer_supercell_translation_sector_enabled() +{ + const char* raw = std::getenv("ABACUS_STERNHEIMER_SUPERCELL_TRANSLATION_SUM"); + return raw != nullptr && raw[0] != '\0'; +} + +inline int sternheimer_lcao_virtual_state_gather_count(const int available_states, + const bool use_delta_sternheimer, + const std::string& virtual_source, + const int requested_states) +{ + if (available_states < 0 || requested_states < 0) + { + throw std::invalid_argument("Sternheimer LCAO virtual-state gather count must be non-negative."); + } + if (sternheimer_lcao_sos_diagnostic_enabled() + || sternheimer_supercell_translation_sector_enabled()) + { + return available_states; + } + if (!use_delta_sternheimer || virtual_source != "ks_bands") + { + return 0; + } + return requested_states == 0 ? available_states : std::min(available_states, requested_states); +} + +struct SternheimerPeriodicResponsePlan +{ + int iq = 1; + SternheimerReducedKPoint qpoint{0.0, 0.0, 0.0}; + std::vector record_index_by_global_k; + std::vector kq_pairs; + double kweight_sum = 0.0; +}; + +inline int sternheimer_periodic_band_count(const int available_bands, const int requested_bands) +{ + if (available_bands <= 0) + { + throw std::invalid_argument("Periodic Sternheimer requires at least one occupied band."); + } + return requested_bands > 0 ? std::min(available_bands, requested_bands) : available_bands; +} + +inline bool sternheimer_write_periodic_v1(const bool use_supercell_translation_sum, + const bool bands_are_truncated, + const bool full_supercell_response = false, + const bool allow_truncated_diagnostic = false) +{ + return (!use_supercell_translation_sum || full_supercell_response) + && (!bands_are_truncated || allow_truncated_diagnostic); +} + +inline void validate_sternheimer_periodic_output_mode(const bool write_periodic_v1, + const bool write_partial_kresolved) +{ + if (!write_periodic_v1 && write_partial_kresolved) + { + throw std::invalid_argument( + "Diagnostic-only periodic Sternheimer output is incompatible with symmetry or k-resolved partial v1."); + } +} + +struct SternheimerFixedQKOrbit +{ + int representative_ik_full = -1; + std::vector members; +}; + +struct SternheimerFixedQKRoute +{ + int iq = 0; + int representative_ik_full = -1; + int member_ik_full = -1; + int spatial_isym = -1; + bool time_reversal = false; + std::array fold_G{0, 0, 0}; +}; + +struct SternheimerQStarPermutation +{ + int spatial_isym = -1; + bool time_reversal = false; + std::vector mapped_index_by_full_q; + std::vector> fold_G_by_full_q; +}; + +struct SternheimerQStarRoute +{ + int representative_iq = 0; + int member_iq = 0; + int spatial_isym = -1; + bool time_reversal = false; + std::array fold_G{0, 0, 0}; +}; + +inline std::string format_sternheimer_fixed_q_routes( + const std::vector& routes) +{ + if (routes.empty()) + { + throw std::invalid_argument("Sternheimer fixed-q route manifest cannot be empty."); + } + std::vector ordered = routes; + std::sort(ordered.begin(), ordered.end(), [](const auto& lhs, const auto& rhs) { + return std::tie(lhs.iq, lhs.member_ik_full, lhs.representative_ik_full) + < std::tie(rhs.iq, rhs.member_ik_full, rhs.representative_ik_full); + }); + std::ostringstream output; + output << "version 1\n"; + output << "# iq representative_ik member_ik spatial_isym time_reversal fold_Gx fold_Gy fold_Gz\n"; + for (std::size_t index = 0; index != ordered.size(); ++index) + { + const auto& route = ordered[index]; + if (route.iq <= 0 || route.representative_ik_full < 0 || route.member_ik_full < 0 + || route.spatial_isym < 0) + { + throw std::invalid_argument("Invalid Sternheimer fixed-q route record."); + } + if (index > 0 + && std::tie(route.iq, route.member_ik_full) + == std::tie(ordered[index - 1].iq, ordered[index - 1].member_ik_full)) + { + throw std::invalid_argument("Duplicate Sternheimer fixed-q route member."); + } + output << route.iq << ' ' << route.representative_ik_full << ' ' + << route.member_ik_full << ' ' << route.spatial_isym << ' ' + << static_cast(route.time_reversal) << ' ' << route.fold_G[0] << ' ' + << route.fold_G[1] << ' ' << route.fold_G[2] << '\n'; + } + return output.str(); +} + +inline std::vector build_sternheimer_fixed_q_k_orbits_from_permutations( + const int full_kpoint_count, + const std::vector>& little_group_permutations) +{ + if (full_kpoint_count <= 0 || little_group_permutations.empty()) + { + throw std::invalid_argument("Fixed-q Sternheimer k orbits require a nonempty full k grid and little group."); + } + for (const auto& permutation: little_group_permutations) + { + if (permutation.size() != static_cast(full_kpoint_count)) + { + throw std::invalid_argument("Fixed-q Sternheimer little-group permutation has an invalid size."); + } + std::vector seen(static_cast(full_kpoint_count), false); + for (const int mapped_index: permutation) + { + if (mapped_index < 0 || mapped_index >= full_kpoint_count + || seen[static_cast(mapped_index)]) + { + throw std::invalid_argument("Fixed-q Sternheimer little-group operation is not a permutation."); + } + seen[static_cast(mapped_index)] = true; + } + } + + std::vector assigned(static_cast(full_kpoint_count), false); + std::vector orbits; + for (int seed = 0; seed != full_kpoint_count; ++seed) + { + if (assigned[static_cast(seed)]) + { + continue; + } + std::vector in_orbit(static_cast(full_kpoint_count), false); + std::vector pending = {seed}; + in_orbit[static_cast(seed)] = true; + for (std::size_t pending_index = 0; pending_index != pending.size(); ++pending_index) + { + const int member = pending[pending_index]; + for (const auto& permutation: little_group_permutations) + { + const int mapped = permutation[static_cast(member)]; + if (!in_orbit[static_cast(mapped)]) + { + in_orbit[static_cast(mapped)] = true; + pending.push_back(mapped); + } + } + } + std::sort(pending.begin(), pending.end()); + SternheimerFixedQKOrbit orbit; + orbit.representative_ik_full = pending.front(); + orbit.members = std::move(pending); + for (const int member: orbit.members) + { + if (assigned[static_cast(member)]) + { + throw std::invalid_argument("Fixed-q Sternheimer k orbits overlap."); + } + assigned[static_cast(member)] = true; + } + orbits.push_back(std::move(orbit)); + } + if (std::any_of(assigned.begin(), assigned.end(), [](const bool value) { return !value; })) + { + throw std::invalid_argument("Fixed-q Sternheimer k orbits do not cover the full k grid."); + } + return orbits; +} + +struct SternheimerWeakQSymmetrySource +{ + int iq = 0; + int source_ik_full = -1; + int representative_ik_full = -1; + bool source_is_representative = false; + int orbit_size = 0; + std::vector inverse_routes; +}; + +inline SternheimerWeakQSymmetrySource select_sternheimer_weak_q_symmetry_source( + const int iq, + const int source_k_one_based, + const std::vector& orbits, + const std::vector& routes) +{ + if (iq <= 0 || source_k_one_based <= 0 || orbits.empty() || routes.empty()) + { + throw std::invalid_argument( + "Weak-q Sternheimer symmetry requires a positive q/source-k index and nonempty routes."); + } + + std::vector member_to_representative; + for (const auto& orbit: orbits) + { + if (orbit.representative_ik_full < 0 || orbit.members.empty() + || std::find(orbit.members.begin(), orbit.members.end(), orbit.representative_ik_full) + == orbit.members.end()) + { + throw std::invalid_argument("Invalid weak-q Sternheimer symmetry orbit."); + } + const int largest_member = *std::max_element(orbit.members.begin(), orbit.members.end()); + if (largest_member < 0) + { + throw std::invalid_argument("Invalid weak-q Sternheimer symmetry orbit member."); + } + if (member_to_representative.size() <= static_cast(largest_member)) + { + member_to_representative.resize(static_cast(largest_member + 1), -1); + } + for (const int member: orbit.members) + { + if (member < 0 + || member_to_representative[static_cast(member)] >= 0) + { + throw std::invalid_argument("Weak-q Sternheimer symmetry orbits overlap."); + } + member_to_representative[static_cast(member)] + = orbit.representative_ik_full; + } + } + + std::vector route_count(member_to_representative.size(), 0); + for (const auto& route: routes) + { + if (route.iq != iq) + { + continue; + } + if (route.member_ik_full < 0 + || static_cast(route.member_ik_full) >= member_to_representative.size() + || member_to_representative[static_cast(route.member_ik_full)] < 0 + || route.representative_ik_full + != member_to_representative[static_cast(route.member_ik_full)] + || route.spatial_isym < 0) + { + throw std::invalid_argument("Invalid weak-q Sternheimer inverse route."); + } + if (++route_count[static_cast(route.member_ik_full)] != 1) + { + throw std::invalid_argument("Duplicate weak-q Sternheimer inverse route."); + } + } + for (std::size_t member = 0; member != member_to_representative.size(); ++member) + { + if (member_to_representative[member] >= 0 && route_count[member] != 1) + { + throw std::invalid_argument("Incomplete weak-q Sternheimer inverse routes."); + } + } + + const int source_ik_full = source_k_one_based - 1; + const auto orbit = std::find_if(orbits.begin(), orbits.end(), [&](const auto& candidate) { + return std::find(candidate.members.begin(), candidate.members.end(), source_ik_full) + != candidate.members.end(); + }); + if (orbit == orbits.end()) + { + throw std::invalid_argument("Weak-q Sternheimer source k point is outside the symmetry orbits."); + } + + SternheimerWeakQSymmetrySource selection; + selection.iq = iq; + selection.source_ik_full = source_ik_full; + selection.representative_ik_full = orbit->representative_ik_full; + selection.source_is_representative = source_ik_full == orbit->representative_ik_full; + selection.orbit_size = static_cast(orbit->members.size()); + for (const auto& route: routes) + { + if (route.iq == iq && route.representative_ik_full == orbit->representative_ik_full) + { + selection.inverse_routes.push_back(route); + } + } + std::sort(selection.inverse_routes.begin(), selection.inverse_routes.end(), + [](const auto& lhs, const auto& rhs) { + return lhs.member_ik_full < rhs.member_ik_full; + }); + if (selection.inverse_routes.size() != orbit->members.size()) + { + throw std::invalid_argument("Weak-q Sternheimer source orbit has incomplete inverse routes."); + } + return selection; +} + +inline void validate_sternheimer_weak_q_symmetry_band_coverage(const int band_begin_one_based, + const int band_end_one_based, + const int occupied_band_count) +{ + if (occupied_band_count <= 0 || band_begin_one_based != 1 + || band_end_one_based != occupied_band_count) + { + throw std::invalid_argument( + "Weak-q Sternheimer symmetry restoration requires the complete occupied subspace."); + } +} + +inline std::vector build_sternheimer_qstar_routes_from_permutations( + const int full_qpoint_count, + const std::vector& permutations) +{ + if (full_qpoint_count <= 0 || permutations.empty()) + { + throw std::invalid_argument( + "Discrete Sternheimer q-star routes require a nonempty full q grid and symmetry group."); + } + std::vector> index_permutations; + index_permutations.reserve(permutations.size()); + for (const auto& permutation: permutations) + { + if (permutation.spatial_isym < 0 + || permutation.mapped_index_by_full_q.size() + != static_cast(full_qpoint_count) + || permutation.fold_G_by_full_q.size() + != static_cast(full_qpoint_count)) + { + throw std::invalid_argument("Invalid discrete Sternheimer q-star permutation."); + } + index_permutations.push_back(permutation.mapped_index_by_full_q); + } + + const auto orbits = build_sternheimer_fixed_q_k_orbits_from_permutations( + full_qpoint_count, index_permutations); + std::vector routes; + routes.reserve(static_cast(full_qpoint_count)); + for (const auto& orbit: orbits) + { + for (const int member: orbit.members) + { + const auto inverse = std::find_if( + permutations.begin(), permutations.end(), [&](const auto& permutation) { + return permutation.mapped_index_by_full_q[static_cast(member)] + == orbit.representative_ik_full; + }); + if (inverse == permutations.end()) + { + throw std::invalid_argument( + "A discrete Sternheimer q-star member has no inverse route to its representative."); + } + routes.push_back({orbit.representative_ik_full + 1, + member + 1, + inverse->spatial_isym, + inverse->time_reversal, + inverse->fold_G_by_full_q[static_cast(member)]}); + } + } + std::sort(routes.begin(), routes.end(), [](const auto& lhs, const auto& rhs) { + return lhs.member_iq < rhs.member_iq; + }); + return routes; +} + +inline std::string format_sternheimer_qstar_routes( + const std::vector& routes) +{ + if (routes.empty()) + { + throw std::invalid_argument("Sternheimer q-star route manifest cannot be empty."); + } + std::vector ordered = routes; + std::sort(ordered.begin(), ordered.end(), [](const auto& lhs, const auto& rhs) { + return std::tie(lhs.member_iq, lhs.representative_iq) + < std::tie(rhs.member_iq, rhs.representative_iq); + }); + std::ostringstream output; + output << "version 1\n"; + output << "# representative_iq member_iq spatial_isym time_reversal fold_Gx fold_Gy fold_Gz\n"; + for (std::size_t index = 0; index != ordered.size(); ++index) + { + const auto& route = ordered[index]; + if (route.representative_iq <= 0 || route.member_iq <= 0 || route.spatial_isym < 0) + { + throw std::invalid_argument("Invalid Sternheimer q-star route record."); + } + if (index > 0 && route.member_iq == ordered[index - 1].member_iq) + { + throw std::invalid_argument("Duplicate Sternheimer q-star route member."); + } + output << route.representative_iq << ' ' << route.member_iq << ' ' + << route.spatial_isym << ' ' << static_cast(route.time_reversal) << ' ' + << route.fold_G[0] << ' ' << route.fold_G[1] << ' ' << route.fold_G[2] << '\n'; + } + return output.str(); +} + +struct SternheimerPartialResponseRecord +{ + int iq = 0; + int ik_full = -1; + int ifrequency = 0; + std::string filename; + std::vector> matrix; +}; + +inline std::string sternheimer_partial_response_filename(const int iq, + const int ik_full, + const int ifrequency) +{ + if (iq <= 0 || ik_full < 0 || ifrequency <= 0) + { + throw std::invalid_argument("Invalid Sternheimer partial-response index."); + } + std::ostringstream filename; + filename << "v1_sternheimer_chi0_iq_" << iq << "_ik_" << ik_full << "_ifreq_" + << ifrequency << ".dat"; + return filename.str(); +} + +inline SternheimerPartialResponseRecord make_sternheimer_partial_response_record( + const int iq, + const int ik_full, + const int ifrequency, + const std::vector>& branch_matrix, + const int num_channels) +{ + if (num_channels <= 0 + || branch_matrix.size() + != static_cast(num_channels) * static_cast(num_channels)) + { + throw std::invalid_argument("Invalid Sternheimer partial-response matrix dimensions."); + } + + SternheimerPartialResponseRecord record; + record.iq = iq; + record.ik_full = ik_full; + record.ifrequency = ifrequency; + record.filename = sternheimer_partial_response_filename(iq, ik_full, ifrequency); + record.matrix.assign(branch_matrix.size(), std::complex(0.0, 0.0)); + for (int row = 0; row != num_channels; ++row) + { + for (int column = 0; column != num_channels; ++column) + { + const std::size_t index + = static_cast(row) * static_cast(num_channels) + + static_cast(column); + const std::size_t transpose + = static_cast(column) * static_cast(num_channels) + + static_cast(row); + record.matrix[index] = branch_matrix[index] + std::conj(branch_matrix[transpose]); + } + } + return record; +} + +inline std::string format_sternheimer_partial_manifest( + const std::vector& records) +{ + if (records.empty()) + { + throw std::invalid_argument("Sternheimer partial-response manifest cannot be empty."); + } + std::vector ordered = records; + std::sort(ordered.begin(), ordered.end(), [](const auto& lhs, const auto& rhs) { + return std::tie(lhs.iq, lhs.ik_full, lhs.ifrequency) + < std::tie(rhs.iq, rhs.ik_full, rhs.ifrequency); + }); + + std::ostringstream manifest; + manifest << "# iq ik_full ifreq response_file\n"; + for (std::size_t index = 0; index != ordered.size(); ++index) + { + const auto& record = ordered[index]; + if (record.iq <= 0 || record.ik_full < 0 || record.ifrequency <= 0 + || record.filename.empty() + || record.filename.find_first_of(" \t\r\n") != std::string::npos) + { + throw std::invalid_argument("Invalid Sternheimer partial-response manifest record."); + } + if (index > 0 + && std::tie(record.iq, record.ik_full, record.ifrequency) + == std::tie(ordered[index - 1].iq, + ordered[index - 1].ik_full, + ordered[index - 1].ifrequency)) + { + throw std::invalid_argument("Duplicate Sternheimer partial-response manifest key."); + } + manifest << record.iq << ' ' << record.ik_full << ' ' << record.ifrequency << ' ' + << record.filename << '\n'; + } + return manifest.str(); +} + +inline std::string format_sternheimer_full_kpoint_manifest( + const std::vector& records) +{ + if (records.empty()) + { + throw std::invalid_argument("Sternheimer full-k-point manifest cannot be empty."); + } + std::vector ordered(records.size(), nullptr); + for (const auto& record: records) + { + if (record.global_k_index < 0 + || record.global_k_index >= static_cast(records.size()) + || !std::isfinite(record.kpoint[0]) || !std::isfinite(record.kpoint[1]) + || !std::isfinite(record.kpoint[2]) + || ordered[static_cast(record.global_k_index)] != nullptr) + { + throw std::invalid_argument("Invalid Sternheimer full-k-point manifest record."); + } + ordered[static_cast(record.global_k_index)] = &record; + } + + std::ostringstream manifest; + manifest << std::setprecision(17) << "# ik_full kx ky kz\n"; + for (std::size_t ik = 0; ik != ordered.size(); ++ik) + { + if (ordered[ik] == nullptr) + { + throw std::invalid_argument("Sternheimer full-k-point manifest indices are not contiguous."); + } + const auto& kpoint = ordered[ik]->kpoint; + manifest << ik << ' ' << kpoint[0] << ' ' << kpoint[1] << ' ' << kpoint[2] << '\n'; + } + return manifest.str(); +} + +inline std::vector limit_sternheimer_abf_channels_per_atom( + std::vector channels, + const int max_channels_per_atom) +{ + if (max_channels_per_atom <= 0) + { + return channels; + } + + int max_atom_index = -1; + for (const SternheimerABFBlochGridChannel& channel: channels) + { + if (channel.atom_index < 0) + { + throw std::invalid_argument("A Sternheimer ABFS channel has an invalid atom index."); + } + max_atom_index = std::max(max_atom_index, channel.atom_index); + } + + std::vector selected_per_atom(static_cast(max_atom_index + 1), 0); + std::vector limited; + limited.reserve(channels.size()); + for (SternheimerABFBlochGridChannel& channel: channels) + { + int& atom_count = selected_per_atom[static_cast(channel.atom_index)]; + if (atom_count >= max_channels_per_atom) + { + continue; + } + SternheimerABFBlochGridChannel selected = std::move(channel); + selected.channel_index = static_cast(limited.size()); + selected.atom_local_index = atom_count; + ++atom_count; + limited.push_back(std::move(selected)); + } + return limited; +} + +inline SternheimerPeriodicResponsePlan build_sternheimer_periodic_response_plan( + const std::vector& records, + const int q_index, + const bool single_gamma_supercell_translation = false) +{ + if (records.empty()) + { + throw std::invalid_argument("Sternheimer response plan requires occupied k-point records."); + } + if (q_index < 0 || q_index > static_cast(records.size())) + { + throw std::invalid_argument("sternheimer_q_index is outside the full k-point mesh."); + } + + SternheimerPeriodicResponsePlan plan; + plan.record_index_by_global_k.assign(records.size(), -1); + std::vector kpoints(records.size()); + for (std::size_t record_index = 0; record_index != records.size(); ++record_index) + { + const SternheimerLCAOOccupiedKPoint& record = records[record_index]; + if (record.global_k_index < 0 || record.global_k_index >= static_cast(records.size())) + { + throw std::invalid_argument("Sternheimer response plan found an invalid global k-point index."); + } + int& mapped_record = plan.record_index_by_global_k[static_cast(record.global_k_index)]; + if (mapped_record >= 0) + { + throw std::invalid_argument("Sternheimer response plan found a duplicate global k-point index."); + } + mapped_record = static_cast(record_index); + kpoints[static_cast(record.global_k_index)] = record.kpoint; + plan.kweight_sum += record.kweight; + } + + constexpr double tolerance = 1.0e-10; + if (q_index == 0 || single_gamma_supercell_translation) + { + if (single_gamma_supercell_translation && q_index != 1) + { + throw std::invalid_argument( + "A single-Gamma supercell translation response must use output q index 1."); + } + if (records.size() != 1 + || std::any_of(records.front().kpoint.begin(), + records.front().kpoint.end(), + [](const double coordinate) { return std::abs(coordinate) > tolerance; })) + { + throw std::invalid_argument( + "sternheimer_q_index=0 is reserved for the single-k Gamma compatibility path."); + } + } + else + { + // A positive q index can select Gamma even on a one-point periodic mesh. + if (records.size() == 1 + && std::any_of(records.front().kpoint.begin(), + records.front().kpoint.end(), + [](const double coordinate) { return std::abs(coordinate) > tolerance; })) + { + throw std::invalid_argument("A single-k periodic Sternheimer response requires a Gamma k point."); + } + for (const SternheimerLCAOOccupiedKPoint& record: records) + { + if (record.spin_index != 0) + { + throw std::invalid_argument("The first solid Sternheimer driver supports only nspin=1."); + } + for (const double occupation: record.occupations) + { + if (std::abs(occupation - 1.0) > tolerance) + { + throw std::invalid_argument( + "The first nspin=1 solid Sternheimer driver requires fully occupied insulating bands."); + } + } + } + plan.iq = q_index; + const int q_record_index + = plan.record_index_by_global_k[static_cast(q_index - 1)]; + plan.qpoint = records[static_cast(q_record_index)].kpoint; + } + plan.kq_pairs = build_sternheimer_kq_map(kpoints, plan.qpoint, tolerance); + return plan; +} + +inline void validate_sternheimer_periodic_kmesh(const std::array& kmesh, + const int global_kpoint_count) +{ + if (global_kpoint_count <= 0 + || std::any_of(kmesh.begin(), kmesh.end(), [](const int dimension) { return dimension <= 0; })) + { + throw std::invalid_argument("Periodic Sternheimer requires positive Monkhorst-Pack dimensions."); + } + const long long mesh_size = static_cast(kmesh[0]) * kmesh[1] * kmesh[2]; + if (mesh_size != global_kpoint_count) + { + throw std::invalid_argument( + "Periodic Sternheimer Monkhorst-Pack dimensions do not match the full k-point count."); + } +} + +inline double sternheimer_periodic_gamma_inverse_k2(const SternheimerReducedKPoint& qpoint, + const std::string& singularity_correction, + const double massidda_chi) +{ + constexpr double tolerance = 1.0e-10; + const bool gamma = std::all_of(qpoint.begin(), qpoint.end(), [tolerance](const double coordinate) { + return std::abs(coordinate) <= tolerance; + }); + if (!gamma) + { + return 0.0; + } + if (singularity_correction != "massidda") + { + throw std::invalid_argument("Periodic Sternheimer q=0 requires Massidda singularity correction."); + } + if (!std::isfinite(massidda_chi) || massidda_chi <= 0.0) + { + throw std::invalid_argument("Periodic Sternheimer q=0 requires a positive finite Massidda value."); + } + return massidda_chi; +} + +inline bool sternheimer_periodic_gamma_uses_2d_massidda( + const SternheimerReducedKPoint& qpoint, + const int ewald_dimension) +{ + if (ewald_dimension != 2 && ewald_dimension != 3) + { + throw std::invalid_argument("Periodic Sternheimer Ewald dimension must be 2 or 3."); + } + constexpr double tolerance = 1.0e-10; + const bool gamma = std::all_of(qpoint.begin(), qpoint.end(), [tolerance](const double coordinate) { + return std::abs(coordinate) <= tolerance; + }); + return gamma && ewald_dimension == 2; +} + +inline int sternheimer_kpoint_owner_group(const int global_kpoint_index, + const int global_kpoint_count, + const int kpoint_groups) +{ + if (global_kpoint_count <= 0 || kpoint_groups <= 0 || kpoint_groups > global_kpoint_count + || global_kpoint_index < 0 || global_kpoint_index >= global_kpoint_count) + { + throw std::invalid_argument("Invalid Sternheimer k-point partition dimensions."); + } + + const int base_count = global_kpoint_count / kpoint_groups; + const int extra_groups = global_kpoint_count % kpoint_groups; + const int enlarged_span = extra_groups * (base_count + 1); + if (global_kpoint_index < enlarged_span) + { + return global_kpoint_index / (base_count + 1); + } + return extra_groups + (global_kpoint_index - enlarged_span) / base_count; +} + +inline int sternheimer_response_kpoint_group_count(const bool full_supercell_response, + const int supercell_kpoint_groups, + const int lcao_kpar, + const int response_kpoint_count) +{ + const int group_count = full_supercell_response ? supercell_kpoint_groups : lcao_kpar; + if (response_kpoint_count <= 0 || group_count <= 0 || group_count > response_kpoint_count) + { + throw std::invalid_argument( + "Sternheimer k-point groups must be positive and no larger than the response k-point count."); + } + return group_count; +} + +struct SternheimerNestedMPIAssignment +{ + int kpoint_group = 0; + int frequency_slot = 0; + int owner_rank = 0; +}; + +struct SternheimerNestedMPIReplicaLayout +{ + int response_slots = 1; + int replicas_per_slot = 1; + int local_response_slot = 0; + int local_replica = 0; +}; + +inline SternheimerNestedMPIReplicaLayout sternheimer_nested_mpi_replica_layout( + const int kpoint_groups, + const int frequency_count, + const int mpi_ranks, + const int mpi_rank, + const bool use_channel_mpi) +{ + if (kpoint_groups <= 0 || frequency_count <= 0 || mpi_ranks <= 0 + || mpi_rank < 0 || mpi_rank >= mpi_ranks) + { + throw std::invalid_argument("Invalid Sternheimer nested-MPI replica dimensions."); + } + const std::int64_t response_slots_64 + = static_cast(kpoint_groups) * frequency_count; + if (response_slots_64 > std::numeric_limits::max()) + { + throw std::overflow_error("Sternheimer nested-MPI response-slot count overflow."); + } + const int response_slots = static_cast(response_slots_64); + if ((!use_channel_mpi && mpi_ranks != response_slots) + || (use_channel_mpi && (mpi_ranks < response_slots || mpi_ranks % response_slots != 0))) + { + throw std::invalid_argument( + "Nested Sternheimer MPI requires one rank or an integer channel-MPI multiple per response slot."); + } + const int replicas_per_slot = use_channel_mpi ? mpi_ranks / response_slots : 1; + return {response_slots, + replicas_per_slot, + mpi_rank / replicas_per_slot, + mpi_rank % replicas_per_slot}; +} + +inline int sternheimer_channel_batch_replica_owner(const int occupied_state, + const int batch_index, + const int batch_count, + const int replica_count) +{ + if (occupied_state < 0 || batch_index < 0 || batch_count <= 0 + || batch_index >= batch_count || replica_count <= 0) + { + throw std::invalid_argument("Invalid Sternheimer channel-batch replica dimensions."); + } + const std::int64_t task_index + = static_cast(occupied_state) * batch_count + batch_index; + return static_cast(task_index % replica_count); +} + +inline SternheimerNestedMPIAssignment sternheimer_nested_mpi_assignment( + const int global_kpoint_index, + const int global_kpoint_count, + const int ifrequency_zero_based, + const int frequency_count, + const int kpoint_groups, + const int mpi_ranks, + const int frequency_rank_shift = 0) +{ + if (frequency_count <= 0 || ifrequency_zero_based < 0 + || ifrequency_zero_based >= frequency_count) + { + throw std::invalid_argument("Invalid Sternheimer nested-MPI frequency dimensions."); + } + if (mpi_ranks != kpoint_groups * frequency_count) + { + throw std::invalid_argument( + "Nested Sternheimer MPI requires NPROC=k-point-groups*frequency-count."); + } + + const int kpoint_group = sternheimer_kpoint_owner_group(global_kpoint_index, + global_kpoint_count, + kpoint_groups); + int normalized_shift = frequency_rank_shift % frequency_count; + if (normalized_shift < 0) + { + normalized_shift += frequency_count; + } + const int frequency_slot = (ifrequency_zero_based + normalized_shift) % frequency_count; + return {kpoint_group, + frequency_slot, + kpoint_group * frequency_count + frequency_slot}; +} + +inline std::vector sternheimer_owned_kq_pair_indices(const SternheimerPeriodicResponsePlan& plan, + const int kpoint_group, + const int kpoint_groups) +{ + if (kpoint_group < 0 || kpoint_group >= kpoint_groups) + { + throw std::invalid_argument("Invalid Sternheimer k-point group index."); + } + const int global_kpoint_count = static_cast(plan.record_index_by_global_k.size()); + std::vector owned; + for (std::size_t pair_index = 0; pair_index != plan.kq_pairs.size(); ++pair_index) + { + if (sternheimer_kpoint_owner_group(plan.kq_pairs[pair_index].source_index, + global_kpoint_count, + kpoint_groups) + == kpoint_group) + { + owned.push_back(pair_index); + } + } + return owned; +} + +class SternheimerKPointSchedule +{ + public: + SternheimerKPointSchedule(const std::vector& active_sources, const int kpoint_groups) + : ordinal_by_global_k_(active_sources.size(), -1), kpoint_groups_(kpoint_groups) + { + if (active_sources.size() > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Sternheimer representative k-point count overflow."); + } + for (std::size_t source = 0; source != active_sources.size(); ++source) + { + if (active_sources[source]) + { + ordinal_by_global_k_[source] = active_kpoint_count_++; + } + } + if (kpoint_groups_ <= 0 || kpoint_groups_ > active_kpoint_count_) + { + throw std::invalid_argument( + "Sternheimer k-point groups must not exceed the active representative count."); + } + } + + int owner_group(const int global_source) const + { + return sternheimer_kpoint_owner_group(active_ordinal(global_source), + active_kpoint_count_, + kpoint_groups_); + } + + SternheimerNestedMPIAssignment assignment(const int global_source, + const int ifrequency, + const int frequency_count, + const int response_slots, + const int frequency_rank_shift = 0) const + { + return sternheimer_nested_mpi_assignment(active_ordinal(global_source), + active_kpoint_count_, + ifrequency, + frequency_count, + kpoint_groups_, + response_slots, + frequency_rank_shift); + } + + std::vector owned_pair_indices(const SternheimerPeriodicResponsePlan& plan, + const int kpoint_group) const + { + if (plan.record_index_by_global_k.size() != ordinal_by_global_k_.size() + || kpoint_group < 0 || kpoint_group >= kpoint_groups_) + { + throw std::invalid_argument("Invalid Sternheimer representative schedule dimensions."); + } + std::vector owned; + for (std::size_t pair_index = 0; pair_index != plan.kq_pairs.size(); ++pair_index) + { + const int source = plan.kq_pairs[pair_index].source_index; + if (source < 0 || source >= static_cast(ordinal_by_global_k_.size())) + { + throw std::invalid_argument("Invalid Sternheimer representative source index."); + } + if (ordinal_by_global_k_[source] >= 0 && owner_group(source) == kpoint_group) + { + owned.push_back(pair_index); + } + } + return owned; + } + + private: + int active_ordinal(const int global_source) const + { + if (global_source < 0 || global_source >= static_cast(ordinal_by_global_k_.size()) + || ordinal_by_global_k_[global_source] < 0) + { + throw std::invalid_argument("Sternheimer response owner requires an active representative source."); + } + return ordinal_by_global_k_[global_source]; + } + + // Scheduling ordinals never replace the global source/target indices in the response plan. + std::vector ordinal_by_global_k_; + int kpoint_groups_; + int active_kpoint_count_ = 0; +}; + +inline void validate_sternheimer_lcao_occupied_kpoints( + const std::vector& records, + const int local_kpoint_count, + const int global_kpoint_count, + const int spin_channel_count, + const int basis_size, + const int zero_order_kpoint_count = -1, + const bool require_complete_records = true) +{ + if (local_kpoint_count <= 0 || global_kpoint_count <= 0 || spin_channel_count <= 0 || basis_size <= 0) + { + throw std::invalid_argument("Sternheimer LCAO k-point dimensions must be positive."); + } + if (local_kpoint_count != global_kpoint_count + || records.size() > static_cast(global_kpoint_count) + || (require_complete_records + && records.size() != static_cast(global_kpoint_count))) + { + throw std::invalid_argument( + "Sternheimer LCAO occupied k-point records are incomplete; the first solid implementation requires " + "KPAR=1."); + } + + std::vector seen_local(static_cast(local_kpoint_count), false); + std::vector seen_global(static_cast(global_kpoint_count), false); + const int zero_order_count + = zero_order_kpoint_count > 0 ? zero_order_kpoint_count : local_kpoint_count; + for (const SternheimerLCAOOccupiedKPoint& record: records) + { + if (record.local_k_index < 0 || record.local_k_index >= local_kpoint_count) + { + throw std::invalid_argument("Sternheimer LCAO local k-point index is out of range."); + } + if (record.global_k_index < 0 || record.global_k_index >= global_kpoint_count) + { + throw std::invalid_argument("Sternheimer LCAO global k-point index is out of range."); + } + if (record.zero_order_k_index < 0 || record.zero_order_k_index >= zero_order_count) + { + throw std::invalid_argument("Sternheimer LCAO zero-order k-point index is out of range."); + } + if (seen_local[static_cast(record.local_k_index)]) + { + throw std::invalid_argument("Sternheimer LCAO local k-point index is duplicated."); + } + if (seen_global[static_cast(record.global_k_index)]) + { + throw std::invalid_argument("Sternheimer LCAO global k-point index is duplicated."); + } + seen_local[static_cast(record.local_k_index)] = true; + seen_global[static_cast(record.global_k_index)] = true; + + if (record.spin_index < 0 || record.spin_index >= spin_channel_count) + { + throw std::invalid_argument("Sternheimer LCAO occupied spin index is out of range."); + } + for (const double coordinate: record.kpoint) + { + if (!std::isfinite(coordinate)) + { + throw std::invalid_argument("Sternheimer LCAO reduced k-point coordinate is not finite."); + } + } + if (!std::isfinite(record.kweight) || record.kweight <= 0.0) + { + throw std::invalid_argument("Sternheimer LCAO k-point weight must be finite and positive."); + } + if (record.coefficients.empty() + || record.eigenvalues.size() != record.coefficients.size() + || record.occupations.size() != record.coefficients.size()) + { + throw std::invalid_argument("Sternheimer LCAO occupied k-point band data are inconsistent."); + } + for (std::size_t ib = 0; ib != record.coefficients.size(); ++ib) + { + if (!std::isfinite(record.eigenvalues[ib]) + || !std::isfinite(record.occupations[ib]) + || record.occupations[ib] <= 0.0) + { + throw std::invalid_argument("Sternheimer LCAO occupied eigenvalue or occupation is invalid."); + } + const auto& band_coefficients = record.coefficients[ib]; + if (band_coefficients.size() != static_cast(basis_size)) + { + throw std::invalid_argument("Sternheimer LCAO coefficient basis size is inconsistent."); + } + for (const std::complex& coefficient: band_coefficients) + { + if (!std::isfinite(coefficient.real()) || !std::isfinite(coefficient.imag())) + { + throw std::invalid_argument("Sternheimer LCAO coefficient is not finite."); + } + } + } + if (record.unoccupied_eigenvalues.size() != record.unoccupied_coefficients.size()) + { + throw std::invalid_argument("Sternheimer LCAO unoccupied k-point band data are inconsistent."); + } + for (std::size_t ib = 0; ib != record.unoccupied_coefficients.size(); ++ib) + { + if (!std::isfinite(record.unoccupied_eigenvalues[ib])) + { + throw std::invalid_argument("Sternheimer LCAO unoccupied eigenvalue is invalid."); + } + const auto& band_coefficients = record.unoccupied_coefficients[ib]; + if (band_coefficients.size() != static_cast(basis_size)) + { + throw std::invalid_argument("Sternheimer LCAO unoccupied coefficient basis size is inconsistent."); + } + for (const std::complex& coefficient: band_coefficients) + { + if (!std::isfinite(coefficient.real()) || !std::isfinite(coefficient.imag())) + { + throw std::invalid_argument("Sternheimer LCAO unoccupied coefficient is not finite."); + } + } + } + } +} + +inline void validate_sternheimer_full_lcao_occupied_kpoints( + const std::vector& records, + const int zero_order_kpoint_count, + const int spin_channel_count, + const int basis_size) +{ + if (zero_order_kpoint_count <= 0) + { + throw std::invalid_argument("Sternheimer LCAO zero-order k-point count must be positive."); + } + const int full_kpoint_count = static_cast(records.size()); + validate_sternheimer_lcao_occupied_kpoints(records, + full_kpoint_count, + full_kpoint_count, + spin_channel_count, + basis_size, + zero_order_kpoint_count); +} + +inline int sternheimer_lcao_total_occupied_bands( + const std::vector& records) +{ + int count = 0; + for (const SternheimerLCAOOccupiedKPoint& record: records) + { + count += static_cast(record.coefficients.size()); + } + return count; +} + +inline double sternheimer_lcao_weighted_occupation(const SternheimerLCAOOccupiedKPoint& record, + const int band_index) +{ + if (band_index < 0 || band_index >= static_cast(record.occupations.size())) + { + throw std::out_of_range("Sternheimer LCAO occupied band index is out of range."); + } + return record.kweight * record.occupations[static_cast(band_index)]; +} + +inline double sternheimer_supercell_sector_kweight(const double supercell_kweight, + const int primitive_cell_count) +{ + if (!std::isfinite(supercell_kweight) || supercell_kweight <= 0.0 + || primitive_cell_count <= 0) + { + throw std::invalid_argument("Invalid supercell translation-sector k-point weight."); + } + return supercell_kweight / static_cast(primitive_cell_count); +} + +inline double sternheimer_supercell_response_matrix_scale( + const bool full_supercell_response, + const int primitive_cell_count) +{ + if (!full_supercell_response) + { + return 1.0; + } + if (primitive_cell_count <= 0) + { + throw std::invalid_argument("Full supercell response requires a positive primitive-cell count."); + } + return static_cast(primitive_cell_count); +} + +struct SternheimerLCAOSamplingPlan +{ + bool sample_source_unoccupied = false; + bool sample_target_unoccupied = false; + bool build_target_ao_candidates = false; +}; + +inline SternheimerLCAOSamplingPlan sternheimer_lcao_sampling_plan( + const bool use_delta_sternheimer, + const bool write_lcao_sos, + const bool target_has_unoccupied_states) +{ + if (write_lcao_sos && !target_has_unoccupied_states) + { + throw std::invalid_argument( + "Periodic direct LCAO-SOS diagnostic requires gathered unoccupied LCAO states."); + } + SternheimerLCAOSamplingPlan plan; + plan.sample_target_unoccupied + = target_has_unoccupied_states && (use_delta_sternheimer || write_lcao_sos); + plan.build_target_ao_candidates = use_delta_sternheimer && !target_has_unoccupied_states; + return plan; +} + +inline bool can_reuse_sternheimer_target_lcao_sampling(const bool same_record, const SternheimerLCAOSamplingPlan& plan) +{ + return same_record && (!plan.sample_source_unoccupied || plan.sample_target_unoccupied); +} + +inline std::size_t sternheimer_sampled_occupied_count(const std::size_t available_states, + const int requested_states) +{ + if (available_states == 0) + { + throw std::invalid_argument("Sternheimer occupied-state sampling requires at least one state."); + } + return requested_states > 0 + ? std::min(available_states, static_cast(requested_states)) + : available_states; +} + +inline std::size_t sternheimer_sampled_unoccupied_count(const bool include_unoccupied, + const std::size_t available_states, + const int requested_states) +{ + if (requested_states < 0) + { + throw std::invalid_argument("Sternheimer sampled unoccupied-state count must be non-negative."); + } + if (!include_unoccupied) + { + return 0; + } + return requested_states == 0 + ? available_states + : std::min(available_states, static_cast(requested_states)); +} + +inline void validate_sternheimer_supercell_sector_occupations( + const std::vector& occupations, + const int expected_occupied_count, + const double tolerance = 1.0e-10) +{ + if (expected_occupied_count <= 0 + || occupations.size() != static_cast(expected_occupied_count) + || !std::isfinite(tolerance) || tolerance < 0.0) + { + throw std::invalid_argument("Supercell translation-sector occupations are inconsistent."); + } + for (const double occupation: occupations) + { + if (!std::isfinite(occupation) || std::abs(occupation - 1.0) > tolerance) + { + throw std::invalid_argument( + "Supercell translation-sector recovery requires uniformly occupied insulating bands."); + } + } +} + +inline std::vector +build_sternheimer_supercell_full_kpoint_records( + const SternheimerLCAOOccupiedKPoint& gamma_record, + const std::vector& sectors) +{ + const int cell_count = static_cast(sectors.size()); + const int full_state_count = static_cast(gamma_record.coefficients.size() + + gamma_record.unoccupied_coefficients.size()); + if (cell_count <= 0 || full_state_count <= 0 || full_state_count % cell_count != 0 + || gamma_record.coefficients.empty() + || gamma_record.coefficients.size() % static_cast(cell_count) != 0 + || gamma_record.eigenvalues.size() != gamma_record.coefficients.size() + || gamma_record.unoccupied_eigenvalues.size() + != gamma_record.unoccupied_coefficients.size()) + { + throw std::invalid_argument("Cannot expand an incomplete supercell Gamma eigensystem."); + } + validate_sternheimer_supercell_sector_occupations( + gamma_record.occupations, static_cast(gamma_record.coefficients.size())); + const int occupied_count + = static_cast(gamma_record.coefficients.size()) / cell_count; + const int sector_dimension = full_state_count / cell_count; + if (occupied_count <= 0 || occupied_count >= sector_dimension) + { + throw std::invalid_argument("Supercell Gamma eigensystem has an invalid primitive occupied dimension."); + } + + std::vector records; + records.reserve(sectors.size()); + for (std::size_t ik = 0; ik != sectors.size(); ++ik) + { + const SternheimerSupercellKPointSector& sector_record = sectors[ik]; + const SternheimerSupercellSector& sector = sector_record.sector; + if (sector.eigenvalues.size() != static_cast(sector_dimension) + || sector.coefficients.size() != static_cast(sector_dimension)) + { + throw std::invalid_argument("A recovered supercell translation sector has an invalid dimension."); + } + for (const auto& coefficients: sector.coefficients) + { + if (coefficients.size() != static_cast(full_state_count)) + { + throw std::invalid_argument( + "A recovered supercell translation-sector vector has an invalid AO dimension."); + } + } + + SternheimerLCAOOccupiedKPoint record = gamma_record; + record.local_k_index = static_cast(ik); + record.global_k_index = static_cast(ik); + record.zero_order_k_index = static_cast(ik); + record.symmetry_spatial_isym = 0; + record.symmetry_time_reversal = false; + record.kpoint = sector_record.kpoint; + record.has_grid_kpoint_override = true; + record.grid_kpoint = {0.0, 0.0, 0.0}; + record.kweight = sternheimer_supercell_sector_kweight(gamma_record.kweight, + cell_count); + record.eigenvalues.assign(sector.eigenvalues.begin(), + sector.eigenvalues.begin() + occupied_count); + record.occupations.assign(static_cast(occupied_count), + gamma_record.occupations.front()); + record.coefficients.assign(sector.coefficients.begin(), + sector.coefficients.begin() + occupied_count); + record.unoccupied_eigenvalues.assign(sector.eigenvalues.begin() + occupied_count, + sector.eigenvalues.end()); + record.unoccupied_coefficients.assign(sector.coefficients.begin() + occupied_count, + sector.coefficients.end()); + records.push_back(std::move(record)); + } + return records; +} + +inline int sternheimer_find_kpoint_one_based( + const std::vector& records, + const SternheimerReducedKPoint& target, + const double tolerance = 1.0e-10) +{ + int found = -1; + for (const auto& record: records) + { + bool matches = true; + for (int direction = 0; direction != 3; ++direction) + { + double difference = record.kpoint[static_cast(direction)] + - target[static_cast(direction)]; + difference -= std::round(difference); + matches = matches && std::abs(difference) <= tolerance; + } + if (!matches) + { + continue; + } + if (found >= 0) + { + throw std::invalid_argument("A primitive supercell q point matches multiple k records."); + } + found = record.global_k_index + 1; + } + if (found <= 0) + { + throw std::invalid_argument("The primitive supercell q point is absent from the recovered k mesh."); + } + return found; +} + +inline std::vector select_sternheimer_gamma_spin_records( + const std::vector& records, + const int spin_channel_count, + const double tolerance = 1.0e-12) +{ + if (spin_channel_count <= 0 || records.empty() + || records.size() > static_cast(spin_channel_count)) + { + throw std::invalid_argument( + "Gamma Sternheimer response requires at least one occupied LCAO spin record."); + } + std::vector selected( + static_cast(spin_channel_count), nullptr); + for (const SternheimerLCAOOccupiedKPoint& record: records) + { + if (record.spin_index < 0 || record.spin_index >= spin_channel_count) + { + throw std::invalid_argument("Gamma Sternheimer response spin index is out of range."); + } + if (std::any_of(record.kpoint.begin(), record.kpoint.end(), [tolerance](const double coordinate) { + return std::abs(coordinate) > tolerance; + })) + { + throw std::invalid_argument("Gamma Sternheimer response received a non-Gamma LCAO record."); + } + const std::size_t spin_index = static_cast(record.spin_index); + if (selected[spin_index] != nullptr) + { + throw std::invalid_argument("Gamma Sternheimer response has duplicate spin records."); + } + selected[spin_index] = &record; + } + selected.erase(std::remove(selected.begin(), selected.end(), nullptr), selected.end()); + return selected; +} + +struct SternheimerABACUSSTChannelResult +{ + int band_index = -1; + int channel_index = -1; + int atom_index = -1; + int angular_momentum = 0; + int radial_index = 0; + int magnetic_index = 0; + double fd_eigenvalue = 0.0; + double occupation = 0.0; + double rhs_norm = 0.0; + double projected_rhs_norm = 0.0; + bool solver_converged = false; + int solver_iterations = 0; + double solver_relative_residual = 0.0; + double equation_residual_norm = 0.0; + std::complex polarizability = {0.0, 0.0}; +}; + +struct SternheimerABACUSSTSmokeResult +{ + SternheimerABACUSFDGridData grid_data; + double omega = 0.0; + double pca_threshold = 0.0; + double ccp_rmesh_times = 0.0; + std::string perturbation_source; + int num_available_channels = 0; + std::vector channels; +}; + +inline std::string format_sternheimer_abacus_st_report(const SternheimerABACUSSTSmokeResult& result) +{ + std::ostringstream out; + const int grid_size = result.grid_data.grid.nx * result.grid_data.grid.ny * result.grid_data.grid.nz; + out << std::setprecision(16); + out << "# ABACUS Sternheimer FD linear-response smoke test\n"; + out << "grid " << result.grid_data.grid.nx << ' ' << result.grid_data.grid.ny << ' ' + << result.grid_data.grid.nz << " size " << grid_size << " dV " << result.grid_data.volume_element << '\n'; + out << "omega_Ry " << result.omega << '\n'; + out << "pca_threshold " << result.pca_threshold << '\n'; + out << "ccp_rmesh_times " << result.ccp_rmesh_times << '\n'; + out << "perturbation_source " << result.perturbation_source << '\n'; + out << "available_channels " << result.num_available_channels << '\n'; + out << "band channel atom l radial m fd_eigenvalue_Ry occupation rhs_norm projected_rhs_norm " + "solver_converged solver_iterations solver_relative_residual equation_residual_norm " + "polarizability_real polarizability_imag\n"; + for (const SternheimerABACUSSTChannelResult& channel: result.channels) + { + out << channel.band_index << ' ' << channel.channel_index << ' ' << channel.atom_index << ' ' + << channel.angular_momentum << ' ' << channel.radial_index << ' ' << channel.magnetic_index << ' ' + << channel.fd_eigenvalue << ' ' << channel.occupation << ' ' << channel.rhs_norm << ' ' + << channel.projected_rhs_norm << ' ' << (channel.solver_converged ? "yes" : "no") << ' ' + << channel.solver_iterations << ' ' << channel.solver_relative_residual << ' ' + << channel.equation_residual_norm << ' ' << channel.polarizability.real() << ' ' + << channel.polarizability.imag() << '\n'; + } + return out.str(); +} + +bool sternheimer_abacus_st_smoke_enabled(); + +inline bool sternheimer_abfs_diag_only_enabled() +{ + const char* raw = std::getenv("ABACUS_STERNHEIMER_FD_ST_ABFS_DIAG_ONLY"); + if (raw == nullptr) + { + return false; + } + std::string value(raw); + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return !(value.empty() || value == "0" || value == "false" || value == "off" || value == "no"); +} + +inline bool sternheimer_grid_coulomb_diagnostic_enabled(const int num_channels) +{ + if (num_channels <= 32) + { + return true; + } + const char* raw = std::getenv("ABACUS_STERNHEIMER_GRID_COULOMB_DIAG"); + if (raw == nullptr) + { + return false; + } + std::string value(raw); + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return !(value.empty() || value == "0" || value == "false" || value == "off" || value == "no"); +} + +void run_sternheimer_abacus_st_smoke(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const std::string& output_dir, + const SternheimerAbacusRuntime& runtime); + +void run_sternheimer_abacus_chi0_output(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const std::string& output_dir, + const SternheimerAbacusRuntime& runtime); + +void run_sternheimer_abacus_lcao_chi0_output(const elecstate::Potential& potential, + const ModulePW::PW_Basis& pw_basis, + const UnitCell& ucell, + const elecstate::ElecState& elec_state, + const LCAO_Orbitals& orbitals, + const std::vector& occupied_kpoints, + const std::array& kmesh, + const ModulePW::PW_Basis_K* pw_wfc, + const Structure_Factor* structure_factor, + const std::string& output_dir, + const SternheimerAbacusRuntime& runtime, + const SternheimerOrbitalSet* reusable_rpa_abfs = nullptr); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.cpp new file mode 100644 index 00000000000..5e278ea5c5a --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.cpp @@ -0,0 +1,787 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" + +#include "source_base/constants.h" +#include "source_base/math_integral.h" +#include "source_base/math_ylmreal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace +{ + +int grid_size(const ModuleRI::SternheimerFDHamiltonian::Grid& grid) +{ + return grid.nx * grid.ny * grid.nz; +} + +int grid_index(const ModuleRI::SternheimerFDHamiltonian::Grid& grid, const int ix, const int iy, const int iz) +{ + return (ix * grid.ny + iy) * grid.nz + iz; +} + +void validate_grid(const ModuleRI::SternheimerFDHamiltonian::Grid& grid) +{ + if (grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0) + { + throw std::invalid_argument("Sternheimer ABFS perturbation requires positive grid dimensions."); + } + if (grid.hx <= 0.0 || grid.hy <= 0.0 || grid.hz <= 0.0) + { + throw std::invalid_argument("Sternheimer ABFS perturbation requires positive grid spacings."); + } +} + +struct PeriodicImageRange +{ + int first = 0; + int last = 0; +}; + +std::array periodic_image_ranges( + const std::array& displacement, + const ModuleRI::SternheimerFDHamiltonian::Grid& grid, + const double cutoff) +{ + std::array ranges{}; + if (!grid.periodic) + { + return ranges; + } + const ModuleRI::SternheimerFDLatticeVectors dual = ModuleRI::sternheimer_fd_grid_dual_vectors(grid); + for (int direction = 0; direction != 3; ++direction) + { + double reduced_displacement = 0.0; + double dual_norm_squared = 0.0; + for (int component = 0; component != 3; ++component) + { + reduced_displacement += dual[direction][component] * displacement[component]; + dual_norm_squared += dual[direction][component] * dual[direction][component]; + } + const double reduced_cutoff = std::sqrt(dual_norm_squared) * cutoff; + ranges[direction].first = static_cast(std::ceil(reduced_displacement - reduced_cutoff)); + ranges[direction].last = static_cast(std::floor(reduced_displacement + reduced_cutoff)); + } + return ranges; +} + +void validate_qpoint(const ModuleRI::SternheimerReducedKPoint& qpoint, const bool periodic) +{ + bool has_nonzero_qpoint = false; + for (const double coordinate: qpoint) + { + if (!std::isfinite(coordinate)) + { + throw std::invalid_argument("Sternheimer ABFS perturbation requires a finite reduced q point."); + } + has_nonzero_qpoint = has_nonzero_qpoint || coordinate != 0.0; + } + if (!periodic && has_nonzero_qpoint) + { + throw std::invalid_argument("Sternheimer ABFS nonperiodic grids cannot use a Bloch q point."); + } +} + +double interpolate_radial(const std::vector& radial_grid, const std::vector& values, const double radius) +{ + if (radial_grid.size() != values.size() || radial_grid.size() < 2) + { + throw std::invalid_argument("Sternheimer ABFS radial interpolation found an invalid radial function."); + } + if (radius < radial_grid.front() || radius > radial_grid.back()) + { + return 0.0; + } + if (radius == radial_grid.front()) + { + return values.front(); + } + + const auto upper = std::upper_bound(radial_grid.begin(), radial_grid.end(), radius); + if (upper == radial_grid.end()) + { + return values.back(); + } + const int hi = static_cast(upper - radial_grid.begin()); + const int lo = hi - 1; + const double width = radial_grid[hi] - radial_grid[lo]; + const double t = (radius - radial_grid[lo]) / width; + return (1.0 - t) * values[lo] + t * values[hi]; +} + +int signed_fft_index(const int index, const int count) +{ + return index < (count + 1) / 2 ? index : index - count; +} + +bool is_gamma_qpoint(const ModuleRI::SternheimerReducedKPoint& qpoint) +{ + constexpr double tolerance = 1.0e-14; + return std::all_of(qpoint.begin(), qpoint.end(), [](const double value) { + return std::abs(value) <= tolerance; + }); +} + +void evaluate_real_spherical_harmonics(const int lmax, + const double x, + const double y, + const double z, + std::vector& ylm) +{ + ylm.assign((lmax + 1) * (lmax + 1), 0.0); + if (lmax == 0 || x * x + y * y + z * z > 1.0e-28) + { + ModuleBase::YlmReal::rlylm(lmax, x, y, z, ylm.data()); + } + else + { + ylm[0] = 0.28209479177387814347; + } +} + +void validate_radial(const ModuleRI::SternheimerRadialPerturbation& radial) +{ + if (radial.angular_momentum < 0) + { + throw std::invalid_argument("Sternheimer ABFS perturbation found negative angular momentum."); + } + if (radial.radial_grid.size() != radial.radial_values.size() || radial.radial_grid.size() < 2) + { + throw std::invalid_argument("Sternheimer ABFS perturbation found inconsistent radial data."); + } + for (std::size_t ir = 1; ir != radial.radial_grid.size(); ++ir) + { + if (radial.radial_grid[ir] <= radial.radial_grid[ir - 1]) + { + throw std::invalid_argument("Sternheimer ABFS perturbation radial grid is not strictly increasing."); + } + } +} + +} // namespace + +namespace ModuleRI +{ + +void set_sternheimer_coulomb_tail_from_charge(SternheimerRadialPerturbation& potential, + const Numerical_Orbital_Lm& charge) +{ + validate_radial(potential); + if (charge.getNr() < 3 || charge.getNr() % 2 == 0 || charge.getL() != potential.angular_momentum + || charge.getType() != potential.type_index || charge.getChi() != potential.radial_index + || charge.get_r_radial().back() > potential.radial_grid.back() + 1.e-12 || potential.radial_grid.back() <= 0.0) + { + throw std::invalid_argument("Coulomb tail requires a matching compact charge within the potential table."); + } + std::vector integrand(charge.getNr()); + for (int ir = 0; ir < charge.getNr(); ++ir) + { + const double radius = charge.getRadial(ir); + if (!std::isfinite(radius) || radius < 0.0 || !std::isfinite(charge.getPsi(ir)) + || !std::isfinite(charge.getRab(ir)) || charge.getRab(ir) <= 0.0 + || (ir > 0 && radius <= charge.getRadial(ir - 1))) + { + throw std::invalid_argument("Invalid auxiliary charge radial quadrature for Coulomb tail."); + } + integrand[ir] = charge.getPsi(ir) * std::pow(radius, charge.getL() + 2); + } + double moment = 0.0; + ModuleBase::Integral::Simpson_Integral(charge.getNr(), integrand.data(), charge.get_rab().data(), moment); + const double coefficient = ModuleBase::FOUR_PI * moment / (2 * charge.getL() + 1); + if (!std::isfinite(coefficient)) + { + throw std::invalid_argument("Nonfinite auxiliary Coulomb multipole moment."); + } + potential.coulomb_tail_coefficient = coefficient; + potential.continue_coulomb_tail = true; +} + +std::vector> make_sternheimer_radial_perturbations_from_orbitals( + const std::vector>>& orbitals) +{ + std::vector> radials_by_type(orbitals.size()); + for (std::size_t type = 0; type != orbitals.size(); ++type) + { + for (std::size_t l = 0; l != orbitals[type].size(); ++l) + { + for (std::size_t n = 0; n != orbitals[type][l].size(); ++n) + { + const Numerical_Orbital_Lm& orbital = orbitals[type][l][n]; + SternheimerRadialPerturbation radial; + radial.type_index = static_cast(type); + radial.angular_momentum = orbital.getL(); + radial.radial_index = orbital.getChi(); + radial.label = orbital.getLabel(); + radial.radial_grid = orbital.get_r_radial(); + radial.radial_values = orbital.get_psi(); + validate_radial(radial); + radials_by_type[type].push_back(std::move(radial)); + } + } + } + return radials_by_type; +} + +std::vector sample_sternheimer_abf_bloch_grid_channels( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + const int max_channels, + const int start_channel) +{ + validate_grid(grid); + validate_qpoint(qpoint, grid.periodic); + if (start_channel < 0) + { + throw std::invalid_argument("Sternheimer ABFS sampling start channel must be nonnegative."); + } + if (atom_types.size() != atom_positions.size()) + { + throw std::invalid_argument("Sternheimer ABFS perturbation atom type/position count mismatch."); + } + + std::vector channels; + const int size = grid_size(grid); + int channel_index = 0; + + for (std::size_t iat = 0; iat != atom_types.size(); ++iat) + { + const int type = atom_types[iat]; + if (type < 0 || type >= static_cast(radials_by_type.size())) + { + throw std::invalid_argument("Sternheimer ABFS perturbation atom type is out of range."); + } + int atom_local_index = 0; + for (const SternheimerRadialPerturbation& radial: radials_by_type[type]) + { + validate_radial(radial); + if (radial.continue_coulomb_tail + && (grid.periodic || radial.radial_grid.back() <= 0.0 + || !std::isfinite(radial.coulomb_tail_coefficient))) + { + throw std::invalid_argument( + "Coulomb multipole tails require an isolated grid and a charge-derived coefficient."); + } + for (int m_index = 0; m_index != 2 * radial.angular_momentum + 1; ++m_index) + { + if (max_channels > 0 && static_cast(channels.size()) >= max_channels) + { + return channels; + } + if (channel_index < start_channel) + { + ++channel_index; + ++atom_local_index; + continue; + } + + SternheimerABFBlochGridChannel channel; + channel.channel_index = channel_index++; + channel.atom_index = static_cast(iat); + channel.atom_local_index = atom_local_index++; + channel.type_index = type; + channel.angular_momentum = radial.angular_momentum; + channel.radial_index = radial.radial_index; + channel.magnetic_index = m_index; + channel.label = radial.label; + channel.potential_r.assign(size, std::complex(0.0, 0.0)); + + const int ylm_index = radial.angular_momentum * radial.angular_momentum + m_index; + const double cutoff = radial.radial_grid.back(); + double channel_max_abs = 0.0; +#pragma omp parallel reduction(max : channel_max_abs) + { + std::vector ylm; +#pragma omp for collapse(3) schedule(static) + for (int iz = 0; iz != grid.nz; ++iz) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int ix = 0; ix != grid.nx; ++ix) + { + const std::array position + = sternheimer_fd_grid_cartesian_position(grid, ix, iy, iz); + const std::array displacement{position[0] - atom_positions[iat].x, + position[1] - atom_positions[iat].y, + position[2] - atom_positions[iat].z}; + const std::array image_ranges + = periodic_image_ranges(displacement, grid, cutoff); + const int ir = grid_index(grid, ix, iy, iz); + + for (int rz = image_ranges[2].first; rz <= image_ranges[2].last; ++rz) + { + for (int ry = image_ranges[1].first; ry <= image_ranges[1].last; ++ry) + { + for (int rx = image_ranges[0].first; rx <= image_ranges[0].last; ++rx) + { + const std::array image{rx, ry, rz}; + const std::array translation + = sternheimer_fd_grid_lattice_translation(grid, image); + const double dx = displacement[0] - translation[0]; + const double dy = displacement[1] - translation[1]; + const double dz = displacement[2] - translation[2]; + const double radius = std::sqrt(dx * dx + dy * dy + dz * dz); + if (radius > cutoff && !radial.continue_coulomb_tail) + { + continue; + } + evaluate_real_spherical_harmonics(radial.angular_momentum, dx, dy, dz, ylm); + // Outside a compact charge, the isolated l-channel potential is r^(-l-1). + const double radial_value + = radius > cutoff ? radial.coulomb_tail_coefficient + / std::pow(radius, radial.angular_momentum + 1) + : interpolate_radial(radial.radial_grid, + radial.radial_values, + radius); + const double radial_angular_value = radial_value * ylm[ylm_index]; + const std::complex phase = sternheimer_bloch_phase(qpoint, image); + channel.potential_r[ir] += phase * radial_angular_value; + } + } + } + channel_max_abs = std::max(channel_max_abs, std::abs(channel.potential_r[ir])); + } + } + } + } + channel.max_abs = channel_max_abs; + channels.push_back(std::move(channel)); + } + } + } + return channels; +} + +std::vector describe_sternheimer_abf_grid_channels( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + const int max_channels) +{ + if (atom_types.size() != atom_positions.size()) + { + throw std::invalid_argument("Sternheimer ABFS perturbation atom type/position count mismatch."); + } + + std::vector channels; + int channel_index = 0; + for (std::size_t iat = 0; iat != atom_types.size(); ++iat) + { + const int type = atom_types[iat]; + if (type < 0 || type >= static_cast(radials_by_type.size())) + { + throw std::invalid_argument("Sternheimer ABFS perturbation atom type is out of range."); + } + int atom_local_index = 0; + for (const SternheimerRadialPerturbation& radial: radials_by_type[static_cast(type)]) + { + validate_radial(radial); + for (int m_index = 0; m_index != 2 * radial.angular_momentum + 1; ++m_index) + { + if (max_channels > 0 && static_cast(channels.size()) >= max_channels) + { + return channels; + } + SternheimerABFGridChannel channel; + channel.channel_index = channel_index++; + channel.atom_index = static_cast(iat); + channel.atom_local_index = atom_local_index++; + channel.type_index = type; + channel.angular_momentum = radial.angular_momentum; + channel.radial_index = radial.radial_index; + channel.magnetic_index = m_index; + channel.label = radial.label; + channels.push_back(std::move(channel)); + } + } + } + return channels; +} + +std::vector sample_sternheimer_abf_grid_channels( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + const SternheimerFDHamiltonian::Grid& grid, + const int max_channels) +{ + const std::vector bloch_channels + = sample_sternheimer_abf_bloch_grid_channels( + radials_by_type, atom_types, atom_positions, grid, {0.0, 0.0, 0.0}, max_channels); + + std::vector channels; + channels.reserve(bloch_channels.size()); + for (const SternheimerABFBlochGridChannel& bloch: bloch_channels) + { + SternheimerABFGridChannel channel; + channel.channel_index = bloch.channel_index; + channel.atom_index = bloch.atom_index; + channel.atom_local_index = bloch.atom_local_index; + channel.type_index = bloch.type_index; + channel.angular_momentum = bloch.angular_momentum; + channel.radial_index = bloch.radial_index; + channel.magnetic_index = bloch.magnetic_index; + channel.label = bloch.label; + channel.max_abs = bloch.max_abs; + channel.potential_r.reserve(bloch.potential_r.size()); + for (const std::complex& value: bloch.potential_r) + { + if (std::abs(value.imag()) > 1.0e-13 * std::max(1.0, channel.max_abs)) + { + throw std::runtime_error("Sternheimer ABFS Gamma potential acquired an unexpected imaginary part."); + } + channel.potential_r.push_back(value.real()); + } + channels.push_back(std::move(channel)); + } + return channels; +} + +std::vector> sample_sternheimer_abf_grid_channel_transform( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + const SternheimerFDHamiltonian::Grid& grid, + std::vector& channel_metadata, + const std::vector& raw_to_output_transform, + const int output_channels) +{ + validate_grid(grid); + if (channel_metadata.empty() || output_channels <= 0) + { + throw std::invalid_argument("Sternheimer ABFS channel transform requires positive input and output dimensions."); + } + const std::size_t raw_channels = channel_metadata.size(); + const std::size_t expected_transform_size + = raw_channels * static_cast(output_channels); + if (raw_to_output_transform.size() != expected_transform_size) + { + throw std::invalid_argument("Sternheimer ABFS channel transform has inconsistent dimensions."); + } + + const std::size_t size = static_cast(grid_size(grid)); + std::vector> transformed( + static_cast(output_channels), std::vector(size, 0.0)); + for (std::size_t raw_index = 0; raw_index != raw_channels; ++raw_index) + { + const std::vector sampled + = sample_sternheimer_abf_bloch_grid_channels(radials_by_type, + atom_types, + atom_positions, + grid, + {0.0, 0.0, 0.0}, + 1, + static_cast(raw_index)); + if (sampled.size() != 1) + { + throw std::runtime_error("Sternheimer ABFS channel transform could not sample a raw channel."); + } + SternheimerABFGridChannel& metadata = channel_metadata[raw_index]; + const SternheimerABFBlochGridChannel& raw_channel = sampled.front(); + if (metadata.channel_index != raw_channel.channel_index || metadata.atom_index != raw_channel.atom_index + || metadata.atom_local_index != raw_channel.atom_local_index || metadata.type_index != raw_channel.type_index + || metadata.angular_momentum != raw_channel.angular_momentum + || metadata.radial_index != raw_channel.radial_index || metadata.magnetic_index != raw_channel.magnetic_index + || metadata.label != raw_channel.label || raw_channel.potential_r.size() != size) + { + throw std::runtime_error("Sternheimer ABFS channel transform metadata does not match sampled channels."); + } + metadata.max_abs = raw_channel.max_abs; + + for (std::size_t grid_begin = 0; grid_begin != size; + grid_begin += static_cast(sternheimer_abfs_transform_grid_chunk)) + { + const std::size_t grid_end + = std::min(size, grid_begin + static_cast(sternheimer_abfs_transform_grid_chunk)); + for (std::size_t ir = grid_begin; ir != grid_end; ++ir) + { + const std::complex& value = raw_channel.potential_r[ir]; + if (std::abs(value.imag()) > 1.0e-13 * std::max(1.0, raw_channel.max_abs)) + { + throw std::runtime_error("Sternheimer ABFS Gamma potential acquired an unexpected imaginary part."); + } + for (int output = 0; output != output_channels; ++output) + { + transformed[static_cast(output)][ir] + += value.real() + * raw_to_output_transform[raw_index * static_cast(output_channels) + + static_cast(output)]; + } + } + } + } + return transformed; +} + +void solve_sternheimer_abf_periodic_full_coulomb_in_place(std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + const double gamma_inverse_k2) +{ + validate_grid(grid); + validate_qpoint(qpoint, grid.periodic); + if (!grid.periodic) + { + throw std::invalid_argument("Sternheimer periodic Poisson solve requires a periodic grid."); + } + if (!std::isfinite(gamma_inverse_k2) || gamma_inverse_k2 < 0.0) + { + throw std::invalid_argument("Sternheimer periodic Poisson Gamma factor must be finite and nonnegative."); + } + const bool gamma_qpoint = is_gamma_qpoint(qpoint); + if (!gamma_qpoint && gamma_inverse_k2 != 0.0) + { + throw std::invalid_argument("A non-Gamma periodic Poisson solve cannot use a Gamma zero-mode factor."); + } + const int size = grid_size(grid); + for (const SternheimerABFBlochGridChannel& density: density_channels) + { + if (density.potential_r.size() != static_cast(size)) + { + throw std::invalid_argument( + "Sternheimer periodic Poisson density size does not match the grid."); + } + } + if (density_channels.empty()) + { + return; + } + const SternheimerFDLatticeVectors dual = sternheimer_fd_grid_dual_vectors(grid); + std::vector> bloch_phase(static_cast(size)); + std::vector coulomb_factor(static_cast(size), 0.0); +#pragma omp parallel for collapse(3) schedule(static) + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const int ig = grid_index(grid, ix, iy, iz); + const double phase_angle + = ModuleBase::TWO_PI + * (qpoint[0] * static_cast(ix) / grid.nx + qpoint[1] * static_cast(iy) / grid.ny + + qpoint[2] * static_cast(iz) / grid.nz); + bloch_phase[static_cast(ig)] = std::exp(std::complex(0.0, phase_angle)); + const std::array reduced_wavevector{ + static_cast(signed_fft_index(ix, grid.nx)) + qpoint[0], + static_cast(signed_fft_index(iy, grid.ny)) + qpoint[1], + static_cast(signed_fft_index(iz, grid.nz)) + qpoint[2]}; + std::array wavevector{}; + for (int direction = 0; direction != 3; ++direction) + { + for (int component = 0; component != 3; ++component) + { + wavevector[component] + += ModuleBase::TWO_PI * reduced_wavevector[direction] * dual[direction][component]; + } + } + const double wavevector_squared + = wavevector[0] * wavevector[0] + wavevector[1] * wavevector[1] + wavevector[2] * wavevector[2]; + if (wavevector_squared <= 1.0e-28) + { + if (!gamma_qpoint) + { + throw std::runtime_error( + "Sternheimer periodic Poisson solve encountered an unexpected zero G+q vector."); + } + coulomb_factor[static_cast(ig)] + = ModuleBase::FOUR_PI * gamma_inverse_k2 / static_cast(size); + } + else + { + coulomb_factor[static_cast(ig)] + = ModuleBase::FOUR_PI / (wavevector_squared * static_cast(size)); + } + } + } + } + + struct FFTWorkspace + { + explicit FFTWorkspace(const SternheimerFDHamiltonian::Grid& workspace_grid) + { + buffer = fftw_alloc_complex(static_cast(workspace_grid.size())); + if (buffer == nullptr) + { + throw std::bad_alloc(); + } + forward = fftw_plan_dft_3d(workspace_grid.nx, + workspace_grid.ny, + workspace_grid.nz, + buffer, + buffer, + FFTW_FORWARD, + FFTW_ESTIMATE); + backward = fftw_plan_dft_3d(workspace_grid.nx, + workspace_grid.ny, + workspace_grid.nz, + buffer, + buffer, + FFTW_BACKWARD, + FFTW_ESTIMATE); + if (forward == nullptr || backward == nullptr) + { + if (forward != nullptr) + { + fftw_destroy_plan(forward); + forward = nullptr; + } + if (backward != nullptr) + { + fftw_destroy_plan(backward); + backward = nullptr; + } + fftw_free(buffer); + buffer = nullptr; + throw std::runtime_error("Failed to initialize the Sternheimer periodic Poisson FFT."); + } + } + ~FFTWorkspace() + { + if (forward != nullptr) + fftw_destroy_plan(forward); + if (backward != nullptr) + fftw_destroy_plan(backward); + if (buffer != nullptr) + fftw_free(buffer); + } + fftw_complex* buffer = nullptr; + fftw_plan forward = nullptr; + fftw_plan backward = nullptr; + }; + + int worker_count = 1; +#ifdef _OPENMP + worker_count = std::min(static_cast(density_channels.size()), omp_get_max_threads()); +#endif + std::vector> workspaces; + workspaces.reserve(static_cast(worker_count)); + for (int worker = 0; worker != worker_count; ++worker) + { + workspaces.emplace_back(new FFTWorkspace(grid)); + } + +#pragma omp parallel for schedule(dynamic) num_threads(worker_count) + for (std::size_t ichannel = 0; ichannel < density_channels.size(); ++ichannel) + { + int worker = 0; +#ifdef _OPENMP + worker = omp_get_thread_num(); +#endif + FFTWorkspace& workspace = *workspaces[static_cast(worker)]; + auto& channel = density_channels[ichannel]; + for (int ir = 0; ir != size; ++ir) + { + const std::complex periodic_value = std::conj(bloch_phase[static_cast(ir)]) + * channel.potential_r[static_cast(ir)]; + workspace.buffer[ir][0] = periodic_value.real(); + workspace.buffer[ir][1] = periodic_value.imag(); + } + fftw_execute(workspace.forward); + for (int ig = 0; ig != size; ++ig) + { + const double factor = coulomb_factor[static_cast(ig)]; + workspace.buffer[ig][0] *= factor; + workspace.buffer[ig][1] *= factor; + } + fftw_execute(workspace.backward); + double max_abs = 0.0; + for (int ir = 0; ir != size; ++ir) + { + const std::complex periodic_potential(workspace.buffer[ir][0], workspace.buffer[ir][1]); + channel.potential_r[static_cast(ir)] + = bloch_phase[static_cast(ir)] * periodic_potential; + max_abs = std::max(max_abs, std::abs(channel.potential_r[static_cast(ir)])); + } + channel.max_abs = max_abs; + } +} + +std::vector solve_sternheimer_abf_periodic_full_coulomb( + const std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + const double gamma_inverse_k2) +{ + std::vector potentials = density_channels; + solve_sternheimer_abf_periodic_full_coulomb_in_place(potentials, grid, qpoint, gamma_inverse_k2); + return potentials; +} + +std::vector> sternheimer_grid_projected_matrix( + const std::vector& densities, + const std::vector& potentials, + const double volume_element) +{ + if (!(volume_element > 0.0) || !std::isfinite(volume_element)) + { + throw std::invalid_argument("Sternheimer grid projection requires a positive finite volume element."); + } + if (densities.size() != potentials.size()) + { + throw std::invalid_argument("Sternheimer grid projection channel counts differ."); + } + const std::size_t size = densities.size(); + std::vector> matrix(size * size, std::complex(0.0, 0.0)); + for (std::size_t row = 0; row != size; ++row) + { + for (std::size_t col = 0; col != size; ++col) + { + if (densities[row].potential_r.size() != potentials[col].potential_r.size()) + { + throw std::invalid_argument("Sternheimer grid projection vector sizes differ."); + } + std::complex value(0.0, 0.0); + for (std::size_t ir = 0; ir != densities[row].potential_r.size(); ++ir) + { + value += std::conj(densities[row].potential_r[ir]) * potentials[col].potential_r[ir]; + } + matrix[row * size + col] = volume_element * value; + } + } + return matrix; +} + +SternheimerCoulombProjectionDiagnostic compare_sternheimer_periodic_coulomb_projection( + const std::vector& densities, + const std::vector& potentials, + const std::vector>& target_coulomb, + const double volume_element) +{ + const std::size_t size = densities.size(); + const std::size_t matrix_size = size * size; + if (size == 0 || potentials.size() != size || target_coulomb.size() != matrix_size) + { + throw std::invalid_argument("Sternheimer Coulomb projection comparison received inconsistent dimensions."); + } + + const std::vector> current + = sternheimer_grid_projected_matrix(densities, potentials, volume_element); + double target_norm_squared = 0.0; + double difference_norm_squared = 0.0; + for (std::size_t index = 0; index != matrix_size; ++index) + { + target_norm_squared += std::norm(target_coulomb[index]); + difference_norm_squared += std::norm(target_coulomb[index] - current[index]); + } + if (!(target_norm_squared > 0.0)) + { + throw std::invalid_argument("Sternheimer target Coulomb matrix has zero norm."); + } + + return {std::sqrt(difference_norm_squared / target_norm_squared)}; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h new file mode 100644 index 00000000000..4bfcbbbb982 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h @@ -0,0 +1,138 @@ +#ifndef STERNHEIMER_ABFS_PERTURBATION_H +#define STERNHEIMER_ABFS_PERTURBATION_H + +#include "source_base/vector3.h" +#include "source_basis/module_ao/orb_atomic_lm.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include +#include +#include +#include + +namespace ModuleRI +{ + +constexpr int sternheimer_abfs_transform_grid_chunk = 1024; + +struct SternheimerRadialPerturbation +{ + int type_index = -1; + int angular_momentum = 0; + int radial_index = 0; + std::string label; + std::vector radial_grid; + std::vector radial_values; + bool continue_coulomb_tail = false; + double coulomb_tail_coefficient = std::numeric_limits::quiet_NaN(); +}; + +struct SternheimerABFGridChannel +{ + int channel_index = -1; + int atom_index = -1; + int atom_local_index = -1; + int type_index = -1; + int angular_momentum = 0; + int radial_index = 0; + int magnetic_index = 0; + std::string label; + std::vector potential_r; + double max_abs = 0.0; +}; + +struct SternheimerABFBlochGridChannel +{ + int channel_index = -1; + int atom_index = -1; + int atom_local_index = -1; + int type_index = -1; + int angular_momentum = 0; + int radial_index = 0; + int magnetic_index = 0; + std::string label; + std::vector> potential_r; + double max_abs = 0.0; +}; + +using SternheimerOrbitalSet = std::vector>>; + +struct SternheimerCoulombProjectionDiagnostic +{ + double relative_error = 0.0; +}; + +std::vector> make_sternheimer_radial_perturbations_from_orbitals( + const std::vector>>& orbitals); + +// Set the exterior r^(-l-1) coefficient from the finalized compact auxiliary charge. +void set_sternheimer_coulomb_tail_from_charge(SternheimerRadialPerturbation& potential, + const Numerical_Orbital_Lm& charge); + +std::vector describe_sternheimer_abf_grid_channels( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + int max_channels = -1); + +std::vector sample_sternheimer_abf_grid_channels( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + const SternheimerFDHamiltonian::Grid& grid, + int max_channels = -1); + +// Sample Gamma-point raw channels and apply a row-major raw-to-output transform. +// channel_metadata defines the raw-channel order used by the transform. +std::vector> sample_sternheimer_abf_grid_channel_transform( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + const SternheimerFDHamiltonian::Grid& grid, + std::vector& channel_metadata, + const std::vector& raw_to_output_transform, + int output_channels); + +// max_channels limits the total returned count, not the count per atom. +// start_channel skips global descriptors without sampling their fine-grid values. +std::vector sample_sternheimer_abf_bloch_grid_channels( + const std::vector>& radials_by_type, + const std::vector& atom_types, + const std::vector>& atom_positions, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + int max_channels = -1, + int start_channel = 0); + +// Solve the periodic Poisson equation for Bloch auxiliary densities. +// Input channel values are densities; output channel values are Hartree potentials in Ha. +// gamma_inverse_k2 replaces 1/|G+q|^2 only for the Gamma zero mode and must be +// zero for non-Gamma q points. +std::vector solve_sternheimer_abf_periodic_full_coulomb( + const std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + double gamma_inverse_k2); + +// Replace each input density by its periodic Hartree potential. This is the +// production interface for large auxiliary spaces because it does not retain +// a second full-grid channel set. +void solve_sternheimer_abf_periodic_full_coulomb_in_place(std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + double gamma_inverse_k2); + +std::vector> sternheimer_grid_projected_matrix( + const std::vector& densities, + const std::vector& potentials, + double volume_element); + +SternheimerCoulombProjectionDiagnostic compare_sternheimer_periodic_coulomb_projection( + const std::vector& densities, + const std::vector& potentials, + const std::vector>& target_coulomb, + double volume_element); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.cpp new file mode 100644 index 00000000000..f93e9292bd0 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.cpp @@ -0,0 +1,494 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.h" + +#include "source_base/constants.h" +#include "source_base/module_external/blas_connector.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using Complex = std::complex; +using Grid = ModuleRI::SternheimerFDHamiltonian::Grid; + +struct Geometry +{ + std::size_t size; + std::size_t nxy; + double length; + double dv; + ModuleRI::SternheimerFDLatticeVectors dual; +}; + +Geometry checked_geometry(const Grid& grid) +{ + if (!grid.periodic || grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires positive dimensions and periodic xy sampling."); + } + for (const double spacing: {grid.hx, grid.hy, grid.hz}) + { + if (!std::isfinite(spacing) || spacing <= 0.0) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires finite positive grid spacings."); + } + } + // FFTW dimensions and the existing Grid use int. + std::size_t size = 1; + for (const int dimension: {grid.nx, grid.ny, grid.nz}) + { + if (size > static_cast(std::numeric_limits::max()) / dimension) + { + throw std::invalid_argument("Strict2D ABFS Coulomb grid exceeds the supported FFT index range."); + } + size *= dimension; + } + const auto lattice = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + std::array lengths{}; + for (int i = 0; i < 3; ++i) + { + for (const double component: lattice[i]) + { + if (!std::isfinite(component)) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires a finite lattice."); + } + } + lengths[i] = std::hypot(std::hypot(lattice[i][0], lattice[i][1]), lattice[i][2]); + if (!std::isfinite(lengths[i]) || lengths[i] <= 0.0) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires nonzero finite lattice vectors."); + } + } + constexpr double tolerance = 1.0e-12; + if (std::abs(lattice[0][2]) > tolerance * lengths[0] + || std::abs(lattice[1][2]) > tolerance * lengths[1] + || std::hypot(lattice[2][0], lattice[2][1]) > tolerance * lengths[2]) + { + throw std::invalid_argument("Strict2D ABFS Coulomb does not support tilted cells: require xy plane and z normal."); + } + const double area = std::abs(lattice[0][0] * lattice[1][1] - lattice[0][1] * lattice[1][0]); + const double volume = area * std::abs(lattice[2][2]); + if (!std::isfinite(area) || area <= tolerance * lengths[0] * lengths[1] + || !std::isfinite(volume) || volume <= 0.0) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires a nonsingular planar cell."); + } + // Drop only accepted geometry roundoff, so Q has no out-of-plane part. + Grid planar_grid = grid; + planar_grid.lattice_vectors = lattice; + planar_grid.lattice_vectors[0][2] = 0.0; + planar_grid.lattice_vectors[1][2] = 0.0; + planar_grid.lattice_vectors[2][0] = 0.0; + planar_grid.lattice_vectors[2][1] = 0.0; + return {size, + static_cast(grid.nx) * grid.ny, + std::abs(lattice[2][2]), + volume / static_cast(size), + ModuleRI::sternheimer_fd_grid_dual_vectors(planar_grid)}; +} + +bool finite(const Complex& value) +{ + return std::isfinite(value.real()) && std::isfinite(value.imag()); +} + +// 1 - (1-exp(-x))/x, without cancellation in the constant-mode projection. +double constant_mode_fraction(const double x) +{ + if (x >= 0.1) return 1.0 + std::expm1(-x) / x; + double term = x / 2.0; + double result = term; + for (int order = 2; order <= 18; ++order) + { + term *= -x / (order + 1); + result += term; + } + return result; +} + +struct FourierFFT +{ + fftw_complex* data = nullptr; + fftw_plan forward = nullptr; + fftw_plan backward = nullptr; + + FourierFFT(const Grid& grid, const std::size_t size) + { + data = fftw_alloc_complex(size); + if (data == nullptr) + { + throw std::bad_alloc(); + } + // Fourier coordinates in all three directions; z is a finite basis, + // NOT a periodic Green function or a periodic boundary condition. +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + forward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, data, data, FFTW_FORWARD, FFTW_ESTIMATE); + backward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, data, data, FFTW_BACKWARD, FFTW_ESTIMATE); + } + if (forward == nullptr || backward == nullptr) + { + release(); + throw std::runtime_error("Failed to initialize strict2D ABFS Galerkin FFTW plans."); + } + } + + FourierFFT(const FourierFFT&) = delete; + FourierFFT& operator=(const FourierFFT&) = delete; + + ~FourierFFT() + { + release(); + } + + void release() + { +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + if (forward != nullptr) fftw_destroy_plan(forward); + if (backward != nullptr) fftw_destroy_plan(backward); + } + fftw_free(data); + data = nullptr; + forward = nullptr; + backward = nullptr; + } +}; +} // namespace + +namespace ModuleRI +{ +std::vector> sternheimer_abf_coulomb_panel( + const std::vector>& density_cm, + const std::vector>& potential_cm, + const std::size_t grid_size, const std::size_t rows, const std::size_t columns, + const double volume_element) +{ + const auto finite_value = [](Complex value) { + return std::isfinite(value.real()) && std::isfinite(value.imag()); + }; + const auto extent = [](std::size_t a, std::size_t b) { + if (a == 0 || b == 0 || a > static_cast(std::numeric_limits::max()) + || b > static_cast(std::numeric_limits::max()) + || a > std::numeric_limits::max()/b) + throw std::invalid_argument("Coulomb panel invalid dimensions."); + return a*b; + }; + if (density_cm.size() != extent(grid_size, rows) || potential_cm.size() != extent(grid_size, columns) + || !std::isfinite(volume_element) || volume_element <= 0 + || !std::all_of(density_cm.begin(), density_cm.end(), finite_value) + || !std::all_of(potential_cm.begin(), potential_cm.end(), finite_value)) + throw std::invalid_argument("Coulomb panel invalid or nonfinite fields."); + std::vector result(extent(rows, columns)); + BlasConnector::gemm_cm('C', 'N', static_cast(rows), static_cast(columns), + static_cast(grid_size), Complex(volume_element), density_cm.data(), + static_cast(grid_size), potential_cm.data(), static_cast(grid_size), + Complex(0), result.data(), static_cast(rows)); + if (!std::all_of(result.begin(), result.end(), finite_value)) + throw std::overflow_error("Coulomb panel nonfinite contraction."); + return result; +} + +namespace +{ +void solve_sternheimer_abf_strict2d_coulomb_impl( + std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint, + const bool gamma_finite_part) +{ + const Geometry geometry = checked_geometry(grid); + for (const double coordinate: qpoint) + { + if (!std::isfinite(coordinate)) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires a finite reduced q point."); + } + } + if (qpoint[2] != 0.0) + { + throw std::invalid_argument("Strict2D ABFS Coulomb supports only qz=0, without reciprocal-z folding."); + } + const bool gamma_equivalent + = std::abs(std::remainder(qpoint[0], 1.0)) <= 1.0e-14 + && std::abs(std::remainder(qpoint[1], 1.0)) <= 1.0e-14; + const bool gamma = qpoint[0] == 0.0 && qpoint[1] == 0.0; + if (gamma_finite_part && !gamma) + { + throw std::invalid_argument("Strict2D ABFS Gamma finite-part Coulomb requires exactly q=(0,0,0)."); + } + if (gamma_equivalent && !gamma_finite_part) + { + throw std::invalid_argument("Strict2D ABFS Coulomb does not support Gamma or its planar reciprocal equivalents."); + } + // Validate every channel before changing the first one. + for (const auto& channel: density_channels) + { + if (channel.potential_r.size() != geometry.size) + { + throw std::invalid_argument("Strict2D ABFS Coulomb density size does not match the fine grid."); + } + for (const Complex value: channel.potential_r) + { + if (!finite(value)) + { + throw std::invalid_argument("Strict2D ABFS Coulomb density contains a nonfinite value."); + } + } + } + std::vector phase(geometry.nxy); + std::vector wave_number(geometry.nxy); + std::vector boundary_average(geometry.nxy); + std::vector constant_fraction(geometry.nxy); + std::vector zero_mode(geometry.nxy, false); + for (int x = 0; x < grid.nx; ++x) + { + for (int y = 0; y < grid.ny; ++y) + { + const std::size_t p = static_cast(x) * grid.ny + y; + const int gx = x < grid.nx / 2 + grid.nx % 2 ? x : x - grid.nx; + const int gy = y < grid.ny / 2 + grid.ny % 2 ? y : y - grid.ny; + const double Qx = ModuleBase::TWO_PI * ((gx + qpoint[0]) * geometry.dual[0][0] + + (gy + qpoint[1]) * geometry.dual[1][0]); + const double Qy = ModuleBase::TWO_PI * ((gx + qpoint[0]) * geometry.dual[0][1] + + (gy + qpoint[1]) * geometry.dual[1][1]); + const double Q = std::hypot(Qx, Qy); + if (!std::isfinite(Q)) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires finite nonzero Q for EVERY planar mode."); + } + if (Q == 0.0) + { + if (!gamma_finite_part || !gamma || gx != 0 || gy != 0) + { + throw std::invalid_argument("Strict2D ABFS Coulomb requires finite nonzero Q for EVERY planar mode."); + } + zero_mode[p] = true; + phase[p] = Complex(1.0, 0.0); + wave_number[p] = 0.0; + continue; + } + const double angle = ModuleBase::TWO_PI * (qpoint[0] * (static_cast(x) / grid.nx) + + qpoint[1] * (static_cast(y) / grid.ny)); + phase[p] = std::exp(Complex(0.0, angle)); + const double x = Q * geometry.length; + if (!finite(phase[p]) || !std::isfinite(x) || x <= 0.0) + { + throw std::invalid_argument("Strict2D ABFS Coulomb has an unrepresentable phase or kernel scale."); + } + wave_number[p] = Q; + boundary_average[p] = -std::expm1(-x) / x; + constant_fraction[p] = constant_mode_fraction(x); + } + } + std::vector kz(grid.nz); + for (int n = 0; n < grid.nz; ++n) + { + const int sn = n < grid.nz / 2 + grid.nz % 2 ? n : n - grid.nz; + kz[n] = ModuleBase::TWO_PI * (static_cast(sn) / geometry.length); + if (!std::isfinite(kz[n])) + throw std::invalid_argument("Strict2D ABFS Coulomb has an unrepresentable z wave number."); + } + if (density_channels.empty()) return; + + FourierFFT fft(grid, geometry.size); + std::vector particular(grid.nz); + for (auto& channel: density_channels) + { + for (std::size_t p = 0; p < geometry.nxy; ++p) + { + for (int z = 0; z < grid.nz; ++z) + { + const std::size_t i = p * grid.nz + z; + const Complex value = std::conj(phase[p]) * channel.potential_r[i]; + if (!finite(value)) + { + throw std::overflow_error("Strict2D ABFS Coulomb overflow in density Bloch phase removal."); + } + fft.data[i][0] = value.real(); + fft.data[i][1] = value.imag(); + } + } + fftw_execute(fft.forward); + for (std::size_t p = 0; p < geometry.nxy; ++p) + { + const std::size_t base = p * grid.nz; + const double Q = wave_number[p]; + Complex nonzero_sum{}, k_moment{}; + for (int n = 0; n < grid.nz; ++n) + { + const Complex rho(fft.data[base + n][0], fft.data[base + n][1]); + if (!finite(rho)) + { + throw std::overflow_error("Strict2D ABFS Coulomb overflow in forward FFT."); + } + if (zero_mode[p] && n == 0) + { + particular[n] = Complex(); + continue; + } + const double norm = std::hypot(Q, kz[n]); + const double symbol = (ModuleBase::FOUR_PI / norm) / norm; + particular[n] = (rho / static_cast(geometry.size)) * symbol; + if (!std::isfinite(symbol) || !finite(particular[n])) + { + throw std::overflow_error("Strict2D ABFS Coulomb overflow in Galerkin particular solution."); + } + if (n != 0) nonzero_sum += particular[n]; + k_moment += kz[n] * particular[n]; + if (!finite(nonzero_sum) || !finite(k_moment)) + throw std::overflow_error("Strict2D ABFS Coulomb overflow in Galerkin boundary moments."); + } + const Complex rho_zero(fft.data[base][0], fft.data[base][1]); + const Complex p_at_zero = particular[0] + nonzero_sum; + if (!finite(p_at_zero)) + throw std::overflow_error("Strict2D ABFS Coulomb overflow in Galerkin boundary value."); + // p'(0)=i*k_moment. Combining the EXACT Fourier projections of + // A exp(-Qz) and B exp(-Q(L-z)) gives the real symmetric rank-two + // correction below. Neither exponential is sampled on the grid. + for (int m = 0; m < grid.nz; ++m) + { + Complex potential; + if (zero_mode[p] && m == 0) + { + potential = -ModuleBase::TWO_PI * geometry.length * geometry.length + * rho_zero / (3.0 * static_cast(geometry.size)) + - nonzero_sum; + } + else if (zero_mode[p]) + { + const double km = kz[m]; + const Complex charge_term + = ModuleBase::FOUR_PI * rho_zero + / (static_cast(geometry.size) * km * km); + potential = particular[m] - charge_term + k_moment / km; + } + else if (m == 0) + { + potential = constant_fraction[p] * particular[0] - boundary_average[p] * nonzero_sum; + } + else + { + const double norm = std::hypot(Q, kz[m]); + const double q_weight = (Q / norm) * (Q / norm); + const double k_weight = (kz[m] / norm) / norm; + const Complex correction = q_weight * p_at_zero - k_weight * k_moment; + if (!finite(correction)) + throw std::overflow_error("Strict2D ABFS Coulomb overflow in Galerkin boundary projection."); + potential = particular[m] - boundary_average[p] * correction; + } + if (!finite(potential)) + throw std::overflow_error("Strict2D ABFS Coulomb overflow in Galerkin potential coefficients."); + fft.data[base + m][0] = potential.real(); + fft.data[base + m][1] = potential.imag(); + } + } + fftw_execute(fft.backward); + double max_abs = 0.0; + for (std::size_t p = 0; p < geometry.nxy; ++p) + { + for (int z = 0; z < grid.nz; ++z) + { + const std::size_t i = p * grid.nz + z; + const Complex periodic_value(fft.data[i][0], fft.data[i][1]); + if (!finite(periodic_value)) + { + throw std::overflow_error("Strict2D ABFS Coulomb overflow in inverse FFT."); + } + const Complex value = phase[p] * periodic_value; + const double magnitude = std::abs(value); + if (!finite(value) || !std::isfinite(magnitude)) + { + throw std::overflow_error("Strict2D ABFS Coulomb overflow in potential Bloch phase or magnitude."); + } + fft.data[i][0] = value.real(); + fft.data[i][1] = value.imag(); + max_abs = std::max(max_abs, magnitude); + } + } + // Validate the complete channel in the workspace before replacing its + // density. Earlier completed channels remain potentials on failure. + for (std::size_t i = 0; i < geometry.size; ++i) + { + channel.potential_r[i] = Complex(fft.data[i][0], fft.data[i][1]); + } + channel.max_abs = max_abs; + } +} +} // namespace + +void solve_sternheimer_abf_strict2d_coulomb_in_place( + std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint) +{ + solve_sternheimer_abf_strict2d_coulomb_impl(density_channels, grid, qpoint, false); +} + +void solve_sternheimer_abf_strict2d_coulomb_finite_part_in_place( + std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint) +{ + solve_sternheimer_abf_strict2d_coulomb_impl(density_channels, grid, qpoint, true); +} + +std::vector> sternheimer_abf_strict2d_selected_coulomb_integrals( + const std::vector& densities, + const std::vector& potentials, + const SternheimerFDHamiltonian::Grid& grid, + const std::vector>& pairs) +{ + const Geometry geometry = checked_geometry(grid); + std::vector result; + result.reserve(pairs.size()); + for (const auto& pair: pairs) + { + if (pair.first >= densities.size() || pair.second >= potentials.size()) + { + throw std::invalid_argument("Strict2D ABFS selected Coulomb integral has an invalid channel index."); + } + const auto& rho = densities[pair.first].potential_r; + const auto& phi = potentials[pair.second].potential_r; + if (rho.size() != geometry.size || phi.size() != geometry.size) + { + throw std::invalid_argument("Strict2D ABFS selected Coulomb integral size does not match the fine grid."); + } + Complex sum{}; + for (std::size_t i = 0; i < geometry.size; ++i) + { + if (!finite(rho[i]) || !finite(phi[i])) + { + throw std::invalid_argument("Strict2D ABFS selected Coulomb integral contains a nonfinite field."); + } + const Complex term = std::conj(rho[i]) * phi[i]; + if (!finite(term)) + { + throw std::overflow_error("Strict2D ABFS selected Coulomb integral overflow in field product."); + } + sum += term; + if (!finite(sum)) + { + throw std::overflow_error("Strict2D ABFS selected Coulomb integral overflow in accumulation."); + } + } + const Complex integral = geometry.dv * sum; + if (!finite(integral)) + { + throw std::overflow_error("Strict2D ABFS selected Coulomb integral overflow in volume scaling."); + } + result.push_back(integral); + } + return result; +} +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.h new file mode 100644 index 00000000000..96d78adc192 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.h @@ -0,0 +1,128 @@ +#ifndef STERNHEIMER_ABFS_STRICT2D_H +#define STERNHEIMER_ABFS_STRICT2D_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" + +#include +#include + +namespace ModuleRI +{ + +#define STERNHEIMER_FULL_COULOMB_PANEL 1 +// Complete rectangular density/potential contraction, column-major fields +// (grid_size x rows/columns) and column-major output (rows x columns). +// Raw dV*rho^H*phi only: no symmetry completion, filtering, or energy gate. +std::vector> sternheimer_abf_coulomb_panel( + const std::vector>& density_cm, + const std::vector>& potential_cm, + std::size_t grid_size, std::size_t rows, std::size_t columns, double volume_element); + +// Finite-Fourier Galerkin strict-2D Coulomb (atomic units, distances in bohr): +// The open-z kernel follows from integrating the full Coulomb Fourier symbol: +// integral dkz/(2*pi) 4*pi*exp[i*kz*(z-z')]/(Q^2+kz^2) +// = (2*pi/Q)*exp[-Q*abs(z-z')], Q>0. +// rho_G(z_j) = (1/Nxy) sum_xy exp[-i(G+q).r_xy] rho_q(r_xy,z_j) +// For each Q=|G+q|>0, let L=|a3|, z_j=j*L/nz, k_n=2*pi*n/L and +// r_n = (1/nz) sum_j exp(-i*k_n*z_j) rho_G(z_j). +// These coefficients define the input interpolant rho_G(z) on [0,L]. Apply +// the CONTINUOUS open Green kernel, then orthogonally Fourier-project the +// resulting potential onto the same finite z basis, before evaluating nodes. +// Equivalently solve (-d_z^2+Q^2)u=4*pi*rho_G with the open Robin conditions +// u'(0)-Q*u(0)=0, u'(L)+Q*u(L)=0: +// p_n = 4*pi*r_n/(Q^2+k_n^2), p0=sum_n p_n, dp0=sum_n i*k_n*p_n, +// A=(dp0/Q-p0)/2, B=-(dp0/Q+p0)/2, +// u(z)=sum_n p_n exp(i*k_n*z)+A exp(-Q*z)+B exp(-Q*(L-z)), +// v_m=p_m+A*(1-exp(-Q*L))/(L*(Q+i*k_m)) +// +B*(1-exp(-Q*L))/(L*(Q-i*k_m)). +// Inverse Fourier transformation of v_m, followed by the full xy Bloch phase, +// returns the PROJECTED potential, not the unprojected u(z_j). +// The implementation combines the two boundary projections algebraically: +// c=(1-exp(-Q*L))/(Q*L), K=sum_n k_n*p_n, +// v_m=p_m-c*(Q^2*p0-k_m*K)/(Q^2+k_m^2). +// Its m=0 evaluation separates p_0 from the nonzero-mode sum and computes +// 1-c by a stable small-QL series. This is the same Galerkin kernel, not an +// empirical diagonal shift, cusp correction or eigenvalue modification. +// potential_r contains FULL complex Bloch densities on entry and potentials +// in Ha on return, not cell-periodic envelopes. Metadata is preserved and +// max_abs is updated. The planar 1/area Fourier normalization becomes 1/Nxy; +// there is no further cell-area factor and no Ry factor of two. +// +// All representable planar AND z FFT modes are retained, with signed index +// i < (n+1)/2 ? i : i-n (negative even-grid Nyquist), exactly as in the ABFS +// periodic solver. q is used as supplied, without folding reciprocal shifts. +// z uses the same negative even-grid Nyquist convention; complex Nyquist +// coefficients are retained, without real-part clipping. +// The original fine grid has no duplicated endpoint. The z DFT defines a +// finite Fourier BASIS on [0,L]; it does not impose periodic-z Green images. +// There is no sampled-cusp rectangle convolution, minimum-image distance, +// Coulomb truncation, or direct sampling of the boundary exponentials. +// Complexity per channel is O(N*log(N)) for N=Nxy*nz, plus O(N) boundary +// projection, with one reusable full-grid FFT workspace and O(nz) scratch. +// +// Supported: grid.periodic=true for the in-plane Bloch sampling convention, +// nonsingular possibly skew a1,a2 in Cartesian xy, a3 parallel to Cartesian z, +// exactly qz=0, and non-Gamma planar q. Geometry roundoff tolerance is 1e-12 +// relative to each lattice-vector norm. Gamma modulo planar reciprocal +// integers (tolerance 1e-14), any zero Q, tilted cells, nonfinite data and +// incompatible grid/channel sizes are explicitly rejected, without fallback. +// An explicit lattice determines L and the metric; hx,hy,hz define the cell +// only when lattice_vectors is entirely zero, as in the existing grid helpers. +// Arithmetic overflow/nonfinite intermediates, FFT output or final magnitudes +// throw std::overflow_error. The failing channel and its max_abs are unchanged; +// earlier completed channels remain potentials (no whole-batch rollback). +// +// The kernel does NOT change ABF sampling or remove any periodic images +// already present in the input density. Physical admission therefore needs +// an unwrapped slab window with each compact ABF's z support contained inside +// it, or independent evidence that boundary density/image contamination is +// negligible. An atom near a z boundary can violate this prerequisite even +// though the discrete kernel is correct. Choose a suitable slab window in +// the caller; this API neither re-centers atoms nor modifies their densities. +// +// Exactness means the continuous Green operator projected into the stated +// FINITE FOURIER SPACE up to roundoff, not automatically exact continuum +// full-2D-Ewald matrix elements for the unsampled physical ABFs. Compare +// selected projected integrals below to same-q, same-ABFS full Ewald data and +// converge grid/window/support errors before admitting fineweak physics. +void solve_sternheimer_abf_strict2d_coulomb_in_place( + std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint); + +// Q -> 0 regular part of the open-z kernel after removing the divergent +// planar constant 2*pi/Q. This is intended only for an analytic Gamma +// head/wing construction; it is not a finite standalone Coulomb operator. +void solve_sternheimer_abf_strict2d_coulomb_finite_part_in_place( + std::vector& density_channels, + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerReducedKPoint& qpoint); + +// Cheap selected matrix elements, in the supplied pair order (duplicates OK): +// result[k] = dV * sum_r conj(densities[pairs[k].first].potential_r[r]) +// * potentials[pairs[k].second].potential_r[r], +// dV = |(a1 x a2).a3|/(nx*ny*nz). +// For the finite Fourier density and projected potential, this equal-weight +// sum is exactly their continuous cell inner product (discrete Parseval). +// Thus the projected Green operator is Hermitian positive, with no left-side +// quadrature of an unprojected exponential. No empirical diagonal subtraction. +// O(pairs.size()*grid.size()) work, no full auxiliary matrix or channel copy. +// Retain the original density samples before the in-place solve. Indices refer +// to the supplied vectors, not channel_index metadata; both fields must use +// the SAME q, grid, ABFS normalization and channel ordering. No symmetrization, +// eigenvalue clipping, Ewald rescaling, or physical pass/fail is performed. +// Only requested channels are inspected; unselected channel payloads may be +// absent. Density and potential vectors need not have the same channel count. +// Nonfinite products, running sums or volume-scaled results from finite inputs +// throw std::overflow_error; no partial result is returned. +// The caller compares these complex elements with the matching full Ewald +// entries; agreement/PSD alone does not establish response convergence. +std::vector> sternheimer_abf_strict2d_selected_coulomb_integrals( + const std::vector& densities, + const std::vector& potentials, + const SternheimerFDHamiltonian::Grid& grid, + const std::vector>& pairs); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_parallel.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_parallel.h new file mode 100644 index 00000000000..8beba4705b5 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_parallel.h @@ -0,0 +1,65 @@ +#ifndef STERNHEIMER_CHANNEL_PARALLEL_H +#define STERNHEIMER_CHANNEL_PARALLEL_H + +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace ModuleRI +{ + +template +std::vector run_sternheimer_channel_tasks(const int num_channels, Worker worker, const int max_workers = 0) +{ + if (num_channels < 0) + { + throw std::invalid_argument("Sternheimer channel task count must be non-negative."); + } + if (max_workers < 0) + { + throw std::invalid_argument("Sternheimer channel maximum worker count must be non-negative."); + } + + std::vector results(static_cast(num_channels)); + std::vector errors(static_cast(num_channels)); + if (num_channels == 0) + { + return results; + } + +#ifdef _OPENMP + const int worker_count + = max_workers > 0 ? std::min(num_channels, max_workers) : std::min(num_channels, omp_get_max_threads()); +#pragma omp parallel for schedule(dynamic) num_threads(worker_count) +#endif + for (int channel_index = 0; channel_index < num_channels; ++channel_index) + { + // A worker may run concurrently; shared captures must be read-only or indexed by channel. + try + { + results[static_cast(channel_index)] = worker(channel_index); + } + catch (...) + { + errors[static_cast(channel_index)] = std::current_exception(); + } + } + + for (const std::exception_ptr& error: errors) + { + if (error) + { + std::rethrow_exception(error); + } + } + return results; +} + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.cpp new file mode 100644 index 00000000000..616ff6ca3eb --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.cpp @@ -0,0 +1,793 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__GLIBC__) +#include +#endif + +#ifdef __MPI +#include +#endif + +namespace ModuleRI +{ +namespace +{ + +constexpr std::uint64_t kComplexVectorsPerWorker = 120; + +#ifdef __MPI +MPI_Comm sternheimer_local_communicator() +{ + static MPI_Comm local_communicator = MPI_COMM_NULL; + if (local_communicator == MPI_COMM_NULL) + { + MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, 0, MPI_INFO_NULL, &local_communicator); + } + // Keep the communicator alive until MPI_Finalize so late memory probes do not + // repeat topology discovery after the large response grids are allocated. + return local_communicator; +} +#endif + +std::uint64_t checked_multiply(const std::uint64_t lhs, const std::uint64_t rhs) +{ + if (lhs != 0 && rhs > std::numeric_limits::max() / lhs) + { + throw std::overflow_error("Sternheimer channel memory estimate overflow."); + } + return lhs * rhs; +} + +std::uint64_t checked_add(const std::uint64_t lhs, const std::uint64_t rhs) +{ + if (rhs > std::numeric_limits::max() - lhs) + { + throw std::overflow_error("Sternheimer shared memory reservation overflow."); + } + return lhs + rhs; +} + +std::uint64_t three_quarters(const std::uint64_t bytes) +{ + return (bytes / 4) * 3 + ((bytes % 4) * 3) / 4; +} + +std::runtime_error memory_budget_error(const char* reason, + const SternheimerMemorySnapshot& memory, + const std::uint64_t target) +{ + std::ostringstream message; + message << reason << " resource_source=" << memory.source + << " accounting_mode=" << sternheimer_memory_accounting_mode_name(memory.mode) + << " node_memory_limit_bytes=" << memory.limit_bytes + << " memory_current_bytes=" << memory.current_bytes + << " memory_target_bytes=" << target << " local_mpi_ranks=" << memory.local_mpi_ranks; + return std::runtime_error(message.str()); +} + +std::uint64_t checked_remaining(const std::uint64_t target, const SternheimerMemorySnapshot& memory) +{ + if (memory.current_bytes >= target) + { + throw memory_budget_error( + "Sternheimer channel memory baseline leaves no room for one worker within the 75 percent target.", + memory, + target); + } + return target - memory.current_bytes; +} + +std::string trim_copy(const std::string& text) +{ + std::size_t first = 0; + while (first < text.size() && std::isspace(static_cast(text[first]))) + { + ++first; + } + std::size_t last = text.size(); + while (last > first && std::isspace(static_cast(text[last - 1]))) + { + --last; + } + return text.substr(first, last - first); +} + +detail::SternheimerOptionalValue parse_unsigned_decimal(const std::string& text) +{ + if (text.empty()) + { + return {}; + } + std::uint64_t value = 0; + for (const char character: text) + { + if (!std::isdigit(static_cast(character))) + { + return {}; + } + const std::uint64_t digit = static_cast(character - '0'); + if (value > (std::numeric_limits::max() - digit) / 10) + { + return {}; + } + value = value * 10 + digit; + } + return value; +} + +detail::SternheimerOptionalValue read_text_file(const std::string& path) +{ + std::ifstream input(path); + if (!input) + { + return {}; + } + std::ostringstream contents; + contents << input.rdbuf(); + return contents.str(); +} + +std::string cgroup_file_path(const std::string& mount, const std::string& relative, const std::string& filename) +{ + std::string path = mount; + if (!relative.empty() && relative != "/") + { + if (relative.front() != '/') + { + path += '/'; + } + path += relative; + } + path += '/'; + path += filename; + return path; +} + +void read_live_memory_candidates(detail::SternheimerMemoryCandidates& candidates) +{ + const detail::SternheimerOptionalValue proc_cgroup = read_text_file("/proc/self/cgroup"); + if (proc_cgroup) + { + const detail::SternheimerOptionalValue v2_path + = detail::parse_sternheimer_cgroup_v2_path(*proc_cgroup); + if (v2_path) + { + const detail::SternheimerOptionalValue limit + = read_text_file(cgroup_file_path("/sys/fs/cgroup", *v2_path, "memory.max")); + const detail::SternheimerOptionalValue current + = read_text_file(cgroup_file_path("/sys/fs/cgroup", *v2_path, "memory.current")); + if (limit) + { + candidates.cgroup_limit_bytes = detail::parse_sternheimer_memory_bytes(*limit); + } + if (current) + { + candidates.cgroup_current_bytes = detail::parse_sternheimer_memory_bytes(*current); + } + candidates.cgroup_source = "cgroup_v2"; + } + else + { + const detail::SternheimerOptionalValue v1_path + = detail::parse_sternheimer_cgroup_v1_memory_path(*proc_cgroup); + if (v1_path) + { + const std::string mount = "/sys/fs/cgroup/memory"; + const detail::SternheimerOptionalValue limit + = read_text_file(cgroup_file_path(mount, *v1_path, "memory.limit_in_bytes")); + const detail::SternheimerOptionalValue current + = read_text_file(cgroup_file_path(mount, *v1_path, "memory.usage_in_bytes")); + if (limit) + { + candidates.cgroup_limit_bytes = detail::parse_sternheimer_memory_bytes(*limit); + } + if (current) + { + candidates.cgroup_current_bytes = detail::parse_sternheimer_memory_bytes(*current); + } + candidates.cgroup_source = "cgroup_v1"; + } + } + } + + if (const char* const slurm_memory = std::getenv("SLURM_MEM_PER_NODE")) + { + candidates.slurm_limit_bytes = detail::parse_sternheimer_slurm_mem_per_node(slurm_memory); + } + const detail::SternheimerOptionalValue meminfo = read_text_file("/proc/meminfo"); + if (meminfo) + { + candidates.mem_available_bytes = detail::parse_sternheimer_kib_field(*meminfo, "MemAvailable"); + candidates.physical_memory_bytes = detail::parse_sternheimer_kib_field(*meminfo, "MemTotal"); + } + const detail::SternheimerOptionalValue status = read_text_file("/proc/self/status"); + if (status) + { + candidates.process_rss_bytes = detail::parse_sternheimer_kib_field(*status, "VmRSS"); + } +} + +#ifdef __MPI +detail::SternheimerOptionalValue allreduce_optional_min( + const detail::SternheimerOptionalValue& value, + MPI_Comm communicator) +{ + const unsigned long long local + = value ? static_cast(*value) : std::numeric_limits::max(); + unsigned long long reduced = 0; + MPI_Allreduce(&local, &reduced, 1, MPI_UNSIGNED_LONG_LONG, MPI_MIN, communicator); + if (reduced == std::numeric_limits::max()) + { + return {}; + } + return static_cast(reduced); +} + +detail::SternheimerOptionalValue allreduce_optional_max( + const detail::SternheimerOptionalValue& value, + MPI_Comm communicator) +{ + const unsigned long long local = value ? static_cast(*value) : 0ULL; + unsigned long long reduced = 0; + MPI_Allreduce(&local, &reduced, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, communicator); + if (reduced == 0) + { + return {}; + } + return static_cast(reduced); +} +#endif + +} // namespace + +SternheimerHeapTrimStatus trim_sternheimer_process_heap() noexcept +{ +#if defined(__GLIBC__) + return malloc_trim(0) != 0 ? SternheimerHeapTrimStatus::pages_released + : SternheimerHeapTrimStatus::no_pages_released; +#else + return SternheimerHeapTrimStatus::unsupported; +#endif +} + +SternheimerMemorySnapshot reserve_sternheimer_shared_memory(const SternheimerMemorySnapshot& memory, + const std::uint64_t bytes_per_rank) +{ + if (memory.mode == SternheimerMemoryAccountingMode::fallback_one) + { + return memory; + } + if (memory.local_mpi_ranks <= 0) + { + throw std::invalid_argument("Sternheimer shared memory reservation requires a positive local MPI rank count."); + } + SternheimerMemorySnapshot adjusted = memory; + switch (memory.mode) + { + case SternheimerMemoryAccountingMode::node_aggregate: + adjusted.current_bytes = checked_add( + memory.current_bytes, checked_multiply(bytes_per_rank, static_cast(memory.local_mpi_ranks))); + break; + case SternheimerMemoryAccountingMode::per_rank: + adjusted.current_bytes = checked_add(memory.current_bytes, bytes_per_rank); + break; + case SternheimerMemoryAccountingMode::available: { + const std::uint64_t reserved_bytes + = checked_multiply(bytes_per_rank, static_cast(memory.local_mpi_ranks)); + if (reserved_bytes > memory.limit_bytes) + { + throw memory_budget_error("Sternheimer shared memory reservation exceeds available memory.", + memory, + three_quarters(memory.limit_bytes)); + } + adjusted.limit_bytes -= reserved_bytes; + break; + } + case SternheimerMemoryAccountingMode::fallback_one: + break; + } + return adjusted; +} + +std::uint64_t estimate_sternheimer_channel_worker_bytes(const std::size_t grid_size) +{ + return checked_multiply(checked_multiply(kComplexVectorsPerWorker, static_cast(grid_size)), + sizeof(std::complex)); +} + +std::vector make_sternheimer_channel_batches(const int num_channels, const int batch_width) +{ + if (num_channels < 0) + { + throw std::invalid_argument("Sternheimer channel batching requires a non-negative channel count."); + } + if (batch_width <= 0) + { + throw std::invalid_argument("Sternheimer channel batching requires a positive batch width."); + } + std::vector batches; + for (int begin = 0; begin < num_channels; begin += batch_width) + { + batches.push_back({begin, std::min(batch_width, num_channels - begin)}); + } + return batches; +} + +std::vector make_sternheimer_owned_channel_batches(const int num_channels, + const int ownership_batch_width, + const int worker_batch_width, + const int replica_count, + const int replica_index, + const int occupied_band_index) +{ + if (num_channels < 0 || ownership_batch_width <= 0 || worker_batch_width <= 0 + || replica_count <= 0 || replica_index < 0 || replica_index >= replica_count + || occupied_band_index < 0) + { + throw std::invalid_argument("Invalid Sternheimer owned-channel batch dimensions."); + } + std::vector batches; + if (num_channels == 0) + { + return batches; + } + const std::int64_t chunk_count = 1 + (num_channels - 1) / ownership_batch_width; + // Match sternheimer_channel_batch_replica_owner using the global requested-width count. + const std::int64_t band_offset = static_cast(occupied_band_index) * chunk_count % replica_count; + const std::int64_t first_chunk = (replica_index - band_offset + replica_count) % replica_count; + for (std::int64_t chunk = first_chunk; chunk < chunk_count; chunk += replica_count) + { + int begin = static_cast(chunk * ownership_batch_width); + const int end = begin + std::min(ownership_batch_width, num_channels - begin); + while (begin < end) + { + const int size = std::min(worker_batch_width, end - begin); + batches.push_back({begin, size}); + begin += size; + } + } + return batches; +} + +SternheimerChannelWorkerPlan plan_sternheimer_channel_workers(const int num_channels, + const int omp_threads, + const std::size_t grid_size, + const int user_cap, + const SternheimerMemorySnapshot& memory, + const int channel_batch_width) +{ + if (num_channels <= 0) + { + throw std::invalid_argument("Sternheimer channel worker planning requires a positive channel count."); + } + if (omp_threads <= 0) + { + throw std::invalid_argument("Sternheimer channel worker planning requires a positive OpenMP thread count."); + } + if (grid_size == 0) + { + throw std::invalid_argument("Sternheimer channel worker planning requires a non-empty real-space grid."); + } + if (user_cap < 0) + { + throw std::invalid_argument("Sternheimer channel worker user cap must be non-negative."); + } + if (memory.local_mpi_ranks <= 0) + { + throw std::invalid_argument("Sternheimer channel worker planning requires a positive local MPI rank count."); + } + if (channel_batch_width <= 0) + { + throw std::invalid_argument("Sternheimer channel worker planning requires a positive batch width."); + } + + SternheimerChannelWorkerPlan plan; + const std::uint64_t base_worker_bytes = estimate_sternheimer_channel_worker_bytes(grid_size); + plan.channel_batch_width = channel_batch_width; + plan.batch_tasks = (num_channels + channel_batch_width - 1) / channel_batch_width; + plan.memory_per_worker_bytes + = checked_multiply(base_worker_bytes, static_cast(channel_batch_width)); + if (memory.mode == SternheimerMemoryAccountingMode::fallback_one) + { + return plan; + } + if (memory.limit_bytes == 0) + { + throw std::invalid_argument("Sternheimer channel worker planning requires a positive detected memory value."); + } + + switch (memory.mode) + { + case SternheimerMemoryAccountingMode::node_aggregate: + plan.target_bytes = three_quarters(memory.limit_bytes); + plan.increment_bytes_per_rank = checked_remaining(plan.target_bytes, memory) + / static_cast(memory.local_mpi_ranks); + break; + case SternheimerMemoryAccountingMode::per_rank: { + const std::uint64_t rank_limit = memory.limit_bytes / static_cast(memory.local_mpi_ranks); + plan.target_bytes = three_quarters(rank_limit); + plan.increment_bytes_per_rank = checked_remaining(plan.target_bytes, memory); + break; + } + case SternheimerMemoryAccountingMode::available: + plan.target_bytes = three_quarters(memory.limit_bytes); + plan.increment_bytes_per_rank = plan.target_bytes / static_cast(memory.local_mpi_ranks); + break; + case SternheimerMemoryAccountingMode::fallback_one: + break; + } + + const std::uint64_t memory_batch_width = plan.increment_bytes_per_rank / base_worker_bytes; + if (memory_batch_width == 0) + { + throw memory_budget_error( + "Sternheimer channel memory budget cannot accommodate one worker within the 75 percent target.", + memory, + plan.target_bytes); + } + plan.channel_batch_width = std::min( + channel_batch_width, + static_cast(std::min(memory_batch_width, + static_cast(std::numeric_limits::max())))); + plan.batch_tasks = (num_channels + plan.channel_batch_width - 1) / plan.channel_batch_width; + plan.memory_per_worker_bytes + = checked_multiply(base_worker_bytes, static_cast(plan.channel_batch_width)); + const std::uint64_t memory_worker_count = plan.increment_bytes_per_rank / plan.memory_per_worker_bytes; + const std::uint64_t bounded_memory_workers + = std::min(memory_worker_count, static_cast(std::numeric_limits::max())); + plan.automatic_workers + = std::min({plan.batch_tasks, omp_threads, static_cast(bounded_memory_workers)}); + const int capped_workers = user_cap > 0 ? std::min(plan.automatic_workers, user_cap) : plan.automatic_workers; + // Use inner grid OpenMP only when independent channel tasks fill less than + // one quarter of the available thread team. Above that point the FD + // stencil is memory-bandwidth bound and channel parallelism is preferable. + const int minimum_channel_workers = (omp_threads + 3) / 4; + plan.effective_workers = capped_workers >= minimum_channel_workers ? capped_workers : 1; + return plan; +} + +SternheimerChannelWorkerPlan plan_sternheimer_owned_channel_workers(const int global_num_channels, + const int owned_num_channels, + const int omp_threads, + const std::size_t grid_size, + const int user_cap, + const SternheimerMemorySnapshot& memory, + const int channel_batch_width) +{ + if (global_num_channels <= 0) + { + throw std::invalid_argument("Sternheimer owned-channel planning requires a positive global channel count."); + } + if (owned_num_channels <= 0 || owned_num_channels > global_num_channels) + { + throw std::invalid_argument( + "Sternheimer owned-channel planning requires a positive local count no larger than the global count."); + } + return plan_sternheimer_channel_workers(owned_num_channels, + omp_threads, + grid_size, + user_cap, + memory, + channel_batch_width); +} + +std::string sternheimer_memory_accounting_mode_name(const SternheimerMemoryAccountingMode mode) +{ + switch (mode) + { + case SternheimerMemoryAccountingMode::node_aggregate: + return "node_aggregate"; + case SternheimerMemoryAccountingMode::per_rank: + return "per_rank"; + case SternheimerMemoryAccountingMode::available: + return "available"; + case SternheimerMemoryAccountingMode::fallback_one: + return "fallback_one"; + } + throw std::invalid_argument("Unknown Sternheimer memory accounting mode."); +} + +std::string format_sternheimer_channel_worker_diagnostic(const SternheimerMemorySnapshot& memory, + const SternheimerChannelWorkerPlan& plan, + const std::size_t grid_size, + const int user_cap) +{ + std::ostringstream diagnostic; + diagnostic << "resource_source=" << memory.source + << " accounting_mode=" << sternheimer_memory_accounting_mode_name(memory.mode) + << " node_memory_limit_bytes=" << memory.limit_bytes << " memory_current_bytes=" << memory.current_bytes + << " memory_target_bytes=" << plan.target_bytes << " local_mpi_ranks=" << memory.local_mpi_ranks + << " grid_size=" << grid_size << " memory_per_worker_bytes=" << plan.memory_per_worker_bytes + << " channel_batch_width=" << plan.channel_batch_width << " batch_tasks=" << plan.batch_tasks + << " automatic_workers=" << plan.automatic_workers << " user_cap=" << user_cap + << " effective_workers=" << plan.effective_workers; + return diagnostic.str(); +} + +SternheimerMemorySnapshot detect_sternheimer_memory_snapshot() +{ + detail::SternheimerMemoryCandidates candidates; + int local_mpi_ranks = 1; +#ifdef __MPI + const MPI_Comm local_communicator = sternheimer_local_communicator(); + MPI_Comm_size(local_communicator, &local_mpi_ranks); + MPI_Barrier(local_communicator); + read_live_memory_candidates(candidates); + candidates.cgroup_limit_bytes = allreduce_optional_min(candidates.cgroup_limit_bytes, local_communicator); + candidates.cgroup_current_bytes = allreduce_optional_max(candidates.cgroup_current_bytes, local_communicator); + candidates.slurm_limit_bytes = allreduce_optional_min(candidates.slurm_limit_bytes, local_communicator); + candidates.mem_available_bytes = allreduce_optional_min(candidates.mem_available_bytes, local_communicator); + candidates.process_rss_bytes = allreduce_optional_max(candidates.process_rss_bytes, local_communicator); + candidates.physical_memory_bytes = allreduce_optional_min(candidates.physical_memory_bytes, local_communicator); +#else + read_live_memory_candidates(candidates); +#endif + return detail::select_sternheimer_memory_snapshot(candidates, local_mpi_ranks); +} + +void initialize_sternheimer_memory_detection() +{ +#ifdef __MPI + (void)sternheimer_local_communicator(); +#endif +} + +namespace detail +{ + +SternheimerOptionalValue parse_sternheimer_memory_bytes(const std::string& input) +{ + std::string text = trim_copy(input); + if (text.empty() || text == "max") + { + return {}; + } + + std::uint64_t multiplier = 1; + const char suffix = text.back(); + if (!std::isdigit(static_cast(suffix))) + { + switch (static_cast(std::toupper(static_cast(suffix)))) + { + case 'K': + multiplier = 1024ULL; + break; + case 'M': + multiplier = 1024ULL * 1024ULL; + break; + case 'G': + multiplier = 1024ULL * 1024ULL * 1024ULL; + break; + default: + return {}; + } + text.pop_back(); + } + const SternheimerOptionalValue value = parse_unsigned_decimal(text); + if (!value) + { + return {}; + } + try + { + return checked_multiply(*value, multiplier); + } + catch (const std::overflow_error&) + { + return {}; + } +} + +SternheimerOptionalValue parse_sternheimer_slurm_mem_per_node(const std::string& input) +{ + const SternheimerOptionalValue mebibytes = parse_unsigned_decimal(trim_copy(input)); + if (!mebibytes) + { + return {}; + } + try + { + return checked_multiply(*mebibytes, 1024ULL * 1024ULL); + } + catch (const std::overflow_error&) + { + return {}; + } +} + +SternheimerOptionalValue parse_sternheimer_kib_field(const std::string& text, const std::string& key) +{ + std::size_t first = 0; + while (first <= text.size()) + { + const std::size_t end = text.find('\n', first); + const std::string line = text.substr(first, end == std::string::npos ? text.size() - first : end - first); + const std::size_t colon = line.find(':'); + if (colon != std::string::npos && trim_copy(line.substr(0, colon)) == key) + { + std::string value = trim_copy(line.substr(colon + 1)); + const std::size_t space = value.find_first_of(" \t"); + const std::string number = value.substr(0, space); + value = space == std::string::npos ? std::string{} : trim_copy(value.substr(space)); + if (value != "kB") + { + return {}; + } + const SternheimerOptionalValue kibibytes = parse_unsigned_decimal(number); + if (!kibibytes) + { + return {}; + } + try + { + return checked_multiply(*kibibytes, 1024ULL); + } + catch (const std::overflow_error&) + { + return {}; + } + } + if (end == std::string::npos) + { + break; + } + first = end + 1; + } + return {}; +} + +SternheimerOptionalValue parse_sternheimer_cgroup_v2_path(const std::string& text) +{ + std::size_t first = 0; + while (first <= text.size()) + { + const std::size_t end = text.find('\n', first); + const std::string line = text.substr(first, end == std::string::npos ? text.size() - first : end - first); + if (line.compare(0, 3, "0::") == 0 && line.size() > 3) + { + return std::string(line.substr(3)); + } + if (end == std::string::npos) + { + break; + } + first = end + 1; + } + return {}; +} + +SternheimerOptionalValue parse_sternheimer_cgroup_v1_memory_path(const std::string& text) +{ + std::size_t first = 0; + while (first <= text.size()) + { + const std::size_t end = text.find('\n', first); + const std::string line = text.substr(first, end == std::string::npos ? text.size() - first : end - first); + const std::size_t first_colon = line.find(':'); + const std::size_t second_colon + = first_colon == std::string::npos ? std::string::npos : line.find(':', first_colon + 1); + if (second_colon != std::string::npos) + { + const std::string controllers = line.substr(first_colon + 1, second_colon - first_colon - 1); + std::size_t controller_first = 0; + while (controller_first <= controllers.size()) + { + const std::size_t comma = controllers.find(',', controller_first); + const std::string controller = controllers.substr( + controller_first, + comma == std::string::npos ? controllers.size() - controller_first : comma - controller_first); + if (controller == "memory" && second_colon + 1 < line.size()) + { + return std::string(line.substr(second_colon + 1)); + } + if (comma == std::string::npos) + { + break; + } + controller_first = comma + 1; + } + } + if (end == std::string::npos) + { + break; + } + first = end + 1; + } + return {}; +} + +SternheimerMemorySnapshot select_sternheimer_memory_snapshot(const SternheimerMemoryCandidates& candidates, + const int local_mpi_ranks) +{ + if (local_mpi_ranks <= 0) + { + throw std::invalid_argument("Sternheimer memory detection requires a positive local MPI rank count."); + } + + SternheimerOptionalValue cgroup_limit = candidates.cgroup_limit_bytes; + if (cgroup_limit) + { + const bool huge_without_physical = !candidates.physical_memory_bytes && *cgroup_limit >= (1ULL << 60); + const bool huge_relative_to_physical = candidates.physical_memory_bytes && *candidates.physical_memory_bytes > 0 + && *cgroup_limit / *candidates.physical_memory_bytes >= 16; + if (huge_without_physical || huge_relative_to_physical) + { + cgroup_limit.reset(); + } + } + + SternheimerOptionalValue enforced_limit; + if (cgroup_limit) + { + enforced_limit = cgroup_limit; + } + if (candidates.slurm_limit_bytes) + { + enforced_limit + = enforced_limit ? std::min(*enforced_limit, *candidates.slurm_limit_bytes) : *candidates.slurm_limit_bytes; + } + + SternheimerMemorySnapshot snapshot; + snapshot.local_mpi_ranks = local_mpi_ranks; + if (enforced_limit && candidates.cgroup_current_bytes) + { + snapshot.mode = SternheimerMemoryAccountingMode::node_aggregate; + snapshot.limit_bytes = *enforced_limit; + snapshot.current_bytes = *candidates.cgroup_current_bytes; + if (cgroup_limit && candidates.slurm_limit_bytes) + { + snapshot.source = candidates.cgroup_source + "+slurm"; + } + else if (!candidates.cgroup_source.empty()) + { + snapshot.source = candidates.cgroup_source; + } + else + { + snapshot.source = "cgroup_current+slurm"; + } + return snapshot; + } + if (enforced_limit && candidates.process_rss_bytes) + { + snapshot.mode = SternheimerMemoryAccountingMode::per_rank; + snapshot.limit_bytes = *enforced_limit; + snapshot.current_bytes = *candidates.process_rss_bytes; + if (cgroup_limit && candidates.slurm_limit_bytes) + { + snapshot.source = candidates.cgroup_source + "+slurm+proc_status"; + } + else if (cgroup_limit) + { + snapshot.source = candidates.cgroup_source + "+proc_status"; + } + else + { + snapshot.source = "slurm+proc_status"; + } + return snapshot; + } + if (candidates.mem_available_bytes) + { + snapshot.mode = SternheimerMemoryAccountingMode::available; + snapshot.limit_bytes = *candidates.mem_available_bytes; + snapshot.current_bytes = 0; + snapshot.source = "proc_meminfo"; + return snapshot; + } + return snapshot; +} + +} // namespace detail + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.h new file mode 100644 index 00000000000..42d4c399866 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.h @@ -0,0 +1,196 @@ +#ifndef STERNHEIMER_CHANNEL_RESOURCES_H +#define STERNHEIMER_CHANNEL_RESOURCES_H + +#include +#include +#include +#include + +namespace ModuleRI +{ + +enum class SternheimerMemoryAccountingMode +{ + node_aggregate, + per_rank, + available, + fallback_one +}; + +enum class SternheimerHeapTrimStatus +{ + unsupported, + no_pages_released, + pages_released +}; + +// Best-effort allocator-cache release; never frees live buffers. Status is +// informational only: callers must take a fresh snapshot and retain the guard. +SternheimerHeapTrimStatus trim_sternheimer_process_heap() noexcept; + +struct SternheimerMemorySnapshot +{ + SternheimerMemoryAccountingMode mode = SternheimerMemoryAccountingMode::fallback_one; + std::uint64_t limit_bytes = 0; + std::uint64_t current_bytes = 0; + int local_mpi_ranks = 1; + std::string source = "unavailable"; +}; + +// Pure preallocation accounting; bytes_per_rank must already be overflow-checked. +// Fallback remains unknown. After allocation, plan again from an actual snapshot. +SternheimerMemorySnapshot reserve_sternheimer_shared_memory(const SternheimerMemorySnapshot& memory, + std::uint64_t bytes_per_rank); + +struct SternheimerChannelWorkerPlan +{ + int automatic_workers = 1; + int effective_workers = 1; + std::uint64_t target_bytes = 0; + std::uint64_t increment_bytes_per_rank = 0; + std::uint64_t memory_per_worker_bytes = 0; + int channel_batch_width = 1; + int batch_tasks = 1; +}; + +struct SternheimerChannelBatch +{ + int begin = 0; + int size = 0; +}; + +std::vector make_sternheimer_channel_batches(int num_channels, int batch_width); + +// Ownership uses (occupied_band_index * global_chunk_count + chunk_index) % replica_count. +// Only owned requested-width chunks are subdivided; local worker width never changes ownership. +// All replicas must use the same channel count, ownership width, replica count and band index. +std::vector make_sternheimer_owned_channel_batches(int num_channels, + int ownership_batch_width, + int worker_batch_width, + int replica_count, + int replica_index, + int occupied_band_index); + +std::uint64_t estimate_sternheimer_channel_worker_bytes(std::size_t grid_size); + +SternheimerChannelWorkerPlan plan_sternheimer_channel_workers(int num_channels, + int omp_threads, + std::size_t grid_size, + int user_cap, + const SternheimerMemorySnapshot& memory, + int channel_batch_width = 1); + +SternheimerChannelWorkerPlan plan_sternheimer_owned_channel_workers(int global_num_channels, + int owned_num_channels, + int omp_threads, + std::size_t grid_size, + int user_cap, + const SternheimerMemorySnapshot& memory, + int channel_batch_width = 1); + +SternheimerMemorySnapshot detect_sternheimer_memory_snapshot(); + +void initialize_sternheimer_memory_detection(); + +std::string sternheimer_memory_accounting_mode_name(SternheimerMemoryAccountingMode mode); + +std::string format_sternheimer_channel_worker_diagnostic(const SternheimerMemorySnapshot& memory, + const SternheimerChannelWorkerPlan& plan, + std::size_t grid_size, + int user_cap); + +namespace detail +{ + +template +class SternheimerOptionalValue +{ + public: + SternheimerOptionalValue() = default; + SternheimerOptionalValue(const T& value) : has_value_(true), value_(value) + { + } + + SternheimerOptionalValue& operator=(const T& value) + { + has_value_ = true; + value_ = value; + return *this; + } + + explicit operator bool() const + { + return has_value_; + } + + bool has_value() const + { + return has_value_; + } + + const T& operator*() const + { + return value_; + } + + T& operator*() + { + return value_; + } + + const T* operator->() const + { + return &value_; + } + + T* operator->() + { + return &value_; + } + + void reset() + { + has_value_ = false; + value_ = T{}; + } + + template + bool operator==(const U& rhs) const + { + return has_value_ && value_ == rhs; + } + + private: + bool has_value_ = false; + T value_{}; +}; + +struct SternheimerMemoryCandidates +{ + SternheimerOptionalValue cgroup_limit_bytes; + SternheimerOptionalValue cgroup_current_bytes; + SternheimerOptionalValue slurm_limit_bytes; + SternheimerOptionalValue mem_available_bytes; + SternheimerOptionalValue process_rss_bytes; + SternheimerOptionalValue physical_memory_bytes; + std::string cgroup_source; +}; + +SternheimerOptionalValue parse_sternheimer_memory_bytes(const std::string& text); + +SternheimerOptionalValue parse_sternheimer_slurm_mem_per_node(const std::string& text); + +SternheimerOptionalValue parse_sternheimer_kib_field(const std::string& text, const std::string& key); + +SternheimerOptionalValue parse_sternheimer_cgroup_v2_path(const std::string& text); + +SternheimerOptionalValue parse_sternheimer_cgroup_v1_memory_path(const std::string& text); + +SternheimerMemorySnapshot select_sternheimer_memory_snapshot(const SternheimerMemoryCandidates& candidates, + int local_mpi_ranks); + +} // namespace detail + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.cpp new file mode 100644 index 00000000000..2b4893702a9 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.cpp @@ -0,0 +1,80 @@ +#include "sternheimer_chi0_mpi.h" + +#ifdef __MPI + +#include +#include +#include + +namespace module_ri +{ +namespace sternheimer_chi0 +{ + +void reduce_branch_to_root(std::vector>& branch, const int root, MPI_Comm communicator) +{ + if (communicator == MPI_COMM_NULL) + { + throw std::invalid_argument("Sternheimer chi0 reduction requires a valid MPI communicator."); + } + + int rank = -1; + int rank_count = 0; + MPI_Comm_rank(communicator, &rank); + MPI_Comm_size(communicator, &rank_count); + if (root < 0 || root >= rank_count) + { + throw std::invalid_argument("Sternheimer chi0 reduction root is outside the MPI communicator."); + } + + const unsigned long long local_size = static_cast(branch.size()); + unsigned long long minimum_size = local_size; + unsigned long long maximum_size = local_size; + MPI_Allreduce(MPI_IN_PLACE, &minimum_size, 1, MPI_UNSIGNED_LONG_LONG, MPI_MIN, communicator); + MPI_Allreduce(MPI_IN_PLACE, &maximum_size, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, communicator); + if (minimum_size != maximum_size) + { + throw std::invalid_argument("Sternheimer chi0 branch sizes differ between MPI ranks."); + } + + std::vector> reduced; + if (rank == root) + { + reduced.assign(branch.size(), std::complex(0.0, 0.0)); + } + std::size_t offset = 0; + while (offset != branch.size()) + { + const std::size_t remaining = branch.size() - offset; + const int count = static_cast( + std::min(remaining, static_cast(std::numeric_limits::max()))); + std::complex* receive_buffer = rank == root ? reduced.data() + offset : nullptr; + if (MPI_Reduce(branch.data() + offset, + receive_buffer, + count, + MPI_DOUBLE_COMPLEX, + MPI_SUM, + root, + communicator) + != MPI_SUCCESS) + { + throw std::runtime_error("Sternheimer chi0 MPI reduction failed."); + } + offset += static_cast(count); + } + + if (rank == root) + { + branch.swap(reduced); + } + else + { + branch.clear(); + branch.shrink_to_fit(); + } +} + +} // namespace sternheimer_chi0 +} // namespace module_ri + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.h new file mode 100644 index 00000000000..43074f5d662 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.h @@ -0,0 +1,23 @@ +#ifndef STERNHEIMER_CHI0_MPI_H +#define STERNHEIMER_CHI0_MPI_H + +#include +#include + +#ifdef __MPI +#include +#endif + +namespace module_ri +{ +namespace sternheimer_chi0 +{ + +#ifdef __MPI +void reduce_branch_to_root(std::vector>& branch, int root, MPI_Comm communicator); +#endif + +} // namespace sternheimer_chi0 +} // namespace module_ri + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.cpp new file mode 100644 index 00000000000..6ec323a6d14 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.cpp @@ -0,0 +1,224 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.h" + +#include "source_base/module_external/lapack_connector.h" + +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +std::vector diagonalize_symmetric(std::vector& matrix, const int dimension) +{ + const char jobz = 'V'; + const char uplo = 'U'; + const int lda = dimension; + int info = 0; + std::vector eigenvalues(static_cast(dimension), 0.0); + double work_query = 0.0; + const int query = -1; + + dsyev_(&jobz, + &uplo, + &dimension, + matrix.data(), + &lda, + eigenvalues.data(), + &work_query, + &query, + &info); + if (info != 0 || !std::isfinite(work_query)) + { + throw std::runtime_error("Sternheimer Coulomb whitening eigensolver workspace query failed."); + } + const int lwork = std::max(1, static_cast(std::ceil(work_query))); + std::vector work(static_cast(lwork), 0.0); + dsyev_(&jobz, + &uplo, + &dimension, + matrix.data(), + &lda, + eigenvalues.data(), + work.data(), + &lwork, + &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer Coulomb whitening eigensolver failed."); + } + return eigenvalues; +} + +void validate_shape(const std::vector& values, const int rows, const int columns, const char* label) +{ + if (rows < 0 || columns < 0 + || values.size() != static_cast(rows) * static_cast(columns)) + { + throw std::invalid_argument(std::string(label) + " has inconsistent dimensions."); + } +} + +} // namespace + +SternheimerCoulombWhitening make_sternheimer_coulomb_whitening(const std::vector& metric, + const int dimension, + const double relative_threshold) +{ + if (dimension <= 0) + { + throw std::invalid_argument("Sternheimer Coulomb whitening dimension must be positive."); + } + validate_shape(metric, dimension, dimension, "Sternheimer Coulomb metric"); + if (!std::isfinite(relative_threshold) || relative_threshold <= 0.0 || relative_threshold >= 1.0) + { + throw std::invalid_argument( + "Sternheimer Coulomb whitening relative threshold must be finite and between zero and one."); + } + + std::vector symmetric(metric.size(), 0.0); + for (int row = 0; row != dimension; ++row) + { + for (int column = 0; column != dimension; ++column) + { + const double left = metric[static_cast(row * dimension + column)]; + const double right = metric[static_cast(column * dimension + row)]; + if (!std::isfinite(left) || !std::isfinite(right)) + { + throw std::invalid_argument("Sternheimer Coulomb metric contains a non-finite value."); + } + symmetric[static_cast(row * dimension + column)] = 0.5 * (left + right); + } + } + const std::vector original_symmetric = symmetric; + std::vector eigenvalues = diagonalize_symmetric(symmetric, dimension); + const double largest = eigenvalues.back(); + if (!std::isfinite(largest) || largest <= 0.0) + { + throw std::runtime_error("Sternheimer Coulomb metric has no positive eigenspace."); + } + const double cutoff = relative_threshold * largest; + const double roundoff_floor + = 64.0 * std::numeric_limits::epsilon() * static_cast(dimension) * largest; + const double negative_tolerance = std::max(cutoff, roundoff_floor); + if (eigenvalues.front() < -negative_tolerance) + { + throw std::runtime_error("Sternheimer Coulomb metric has a materially negative eigenvalue."); + } + + int first_retained = 0; + while (first_retained != dimension && eigenvalues[static_cast(first_retained)] <= cutoff) + { + ++first_retained; + } + if (first_retained == dimension) + { + throw std::runtime_error("Sternheimer Coulomb whitening threshold discarded every eigenvector."); + } + + SternheimerCoulombWhitening result; + result.raw_dimension = dimension; + result.retained_rank = dimension - first_retained; + result.discarded_rank = first_retained; + result.relative_threshold = relative_threshold; + result.largest_eigenvalue = largest; + result.smallest_retained_eigenvalue = eigenvalues[static_cast(first_retained)]; + result.eigenvalues = eigenvalues; + result.transform.assign(static_cast(dimension) + * static_cast(result.retained_rank), + 0.0); + + // DSYEV returns eigenvectors as columns in column-major storage. + for (int retained = 0; retained != result.retained_rank; ++retained) + { + const int eigenvector = first_retained + retained; + const double inverse_sqrt + = 1.0 / std::sqrt(eigenvalues[static_cast(eigenvector)]); + for (int raw = 0; raw != dimension; ++raw) + { + result.transform[static_cast(raw * result.retained_rank + retained)] + = symmetric[static_cast(eigenvector * dimension + raw)] * inverse_sqrt; + } + } + + std::vector metric_times_transform( + static_cast(dimension) * static_cast(result.retained_rank), + 0.0); + for (int row = 0; row != dimension; ++row) + { + for (int retained = 0; retained != result.retained_rank; ++retained) + { + for (int column = 0; column != dimension; ++column) + { + metric_times_transform[static_cast(row * result.retained_rank + retained)] + += original_symmetric[static_cast(row * dimension + column)] + * result.transform[static_cast(column * result.retained_rank + retained)]; + } + } + } + + double max_error = 0.0; + for (int left = 0; left != result.retained_rank; ++left) + { + for (int right = 0; right != result.retained_rank; ++right) + { + double value = 0.0; + for (int row = 0; row != dimension; ++row) + { + value += result.transform[static_cast(row * result.retained_rank + left)] + * metric_times_transform[static_cast(row * result.retained_rank + right)]; + } + const double expected = left == right ? 1.0 : 0.0; + max_error = std::max(max_error, std::abs(value - expected)); + } + } + result.max_orthonormality_error = max_error; + if (!std::isfinite(max_error) || max_error > 1.0e-8) + { + throw std::runtime_error("Sternheimer Coulomb whitening failed its W^T V W identity check."); + } + return result; +} + +std::vector apply_sternheimer_channel_transform(const std::vector& rows_by_raw_channel, + const int row_count, + const int raw_dimension, + const SternheimerCoulombWhitening& whitening) +{ + validate_shape(rows_by_raw_channel, + row_count, + raw_dimension, + "Sternheimer raw-channel matrix"); + if (raw_dimension != whitening.raw_dimension || whitening.retained_rank <= 0) + { + throw std::invalid_argument("Sternheimer channel transform dimension does not match its whitening metric."); + } + validate_shape(whitening.transform, + whitening.raw_dimension, + whitening.retained_rank, + "Sternheimer Coulomb whitening transform"); + + std::vector transformed( + static_cast(row_count) * static_cast(whitening.retained_rank), + 0.0); + for (int row = 0; row != row_count; ++row) + { + for (int retained = 0; retained != whitening.retained_rank; ++retained) + { + double value = 0.0; + for (int raw = 0; raw != raw_dimension; ++raw) + { + value += rows_by_raw_channel[static_cast(row * raw_dimension + raw)] + * whitening.transform[static_cast(raw * whitening.retained_rank + retained)]; + } + transformed[static_cast(row * whitening.retained_rank + retained)] = value; + } + } + return transformed; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.h new file mode 100644 index 00000000000..d40907c6bef --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.h @@ -0,0 +1,33 @@ +#ifndef STERNHEIMER_COULOMB_WHITENING_H +#define STERNHEIMER_COULOMB_WHITENING_H + +#include + +namespace ModuleRI +{ + +struct SternheimerCoulombWhitening +{ + int raw_dimension = 0; + int retained_rank = 0; + int discarded_rank = 0; + double relative_threshold = 0.0; + double largest_eigenvalue = 0.0; + double smallest_retained_eigenvalue = 0.0; + double max_orthonormality_error = 0.0; + std::vector eigenvalues; + std::vector transform; ///< row-major raw_dimension x retained_rank +}; + +SternheimerCoulombWhitening make_sternheimer_coulomb_whitening(const std::vector& metric, + int dimension, + double relative_threshold); + +std::vector apply_sternheimer_channel_transform(const std::vector& rows_by_raw_channel, + int row_count, + int raw_dimension, + const SternheimerCoulombWhitening& whitening); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_delta.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_delta.cpp new file mode 100644 index 00000000000..2b1bb785c38 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_delta.cpp @@ -0,0 +1,2787 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_delta.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.h" + +#include "source_base/module_external/blas_connector.h" +#include "source_base/module_external/lapack_connector.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_lapack_gesv.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" + +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +using Complex = SternheimerFDHamiltonian::Complex; +using Vector = SternheimerFDHamiltonian::Vector; + +void check_vector_size(const Vector& vector, const std::size_t expected_size, const std::string& context) +{ + if (vector.size() != expected_size) + { + throw std::invalid_argument(context + " vector size does not match the Sternheimer delta grid."); + } +} + +void axpy(const Complex alpha, const Vector& x, Vector& y) +{ + check_vector_size(x, y.size(), "Sternheimer delta axpy"); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != y.size(); ++ir) + { + y[ir] += alpha * x[ir]; + } +} + +double difference_norm(const Vector& lhs, const Vector& rhs, const double volume_element) +{ + check_vector_size(lhs, rhs.size(), "Sternheimer delta difference_norm"); + Vector difference(lhs.size(), Complex(0.0, 0.0)); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != lhs.size(); ++ir) + { + difference[ir] = lhs[ir] - rhs[ir]; + } + return sternheimer_fd_grid_norm(difference, volume_element); +} + +void scale_vector(Vector& vector, const Complex factor) +{ +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != vector.size(); ++ir) + { + vector[ir] *= factor; + } +} + +void validate_grid_function(const SternheimerDeltaGridFunction& function, + const std::size_t grid_size, + const std::string& context) +{ + check_vector_size(function.values, grid_size, context + " values"); + for (const Vector& gradient: function.gradients) + { + check_vector_size(gradient, grid_size, context + " gradient"); + } +} + +void validate_finite_grid_vector(const Vector& values, const std::string& context) +{ + for (const Complex value: values) + { + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + { + throw std::invalid_argument(context + " contains a nonfinite value."); + } + } +} + +void axpy_grid_function(const Complex alpha, + const SternheimerDeltaGridFunction& x, + SternheimerDeltaGridFunction& y) +{ + axpy(alpha, x.values, y.values); + for (int direction = 0; direction != 3; ++direction) + { + axpy(alpha, x.gradients[static_cast(direction)], + y.gradients[static_cast(direction)]); + } +} + +void scale_grid_function(SternheimerDeltaGridFunction& function, const Complex factor) +{ + scale_vector(function.values, factor); + for (Vector& gradient: function.gradients) + { + scale_vector(gradient, factor); + } +} + +std::vector collect_virtual_orbitals(const std::vector& virtual_states) +{ + std::vector orbitals; + orbitals.reserve(virtual_states.size()); + for (const SternheimerDeltaVirtualState& state: virtual_states) + { + orbitals.push_back(state.orbital); + } + return orbitals; +} + +std::vector collect_fixed_subspace(const std::vector& occupied_wavefunctions, + const std::vector& virtual_states) +{ + std::vector subspace = occupied_wavefunctions; + subspace.reserve(occupied_wavefunctions.size() + virtual_states.size()); + for (const SternheimerDeltaVirtualState& state: virtual_states) + { + subspace.push_back(state.orbital); + } + return subspace; +} + +std::vector diagonalize_complex_hermitian(std::vector& matrix, const int size) +{ + char jobz = 'V'; + char uplo = 'U'; + const int lda = size; + int info = 0; + std::vector eigenvalues(static_cast(size), 0.0); + Complex work_query(0.0, 0.0); + const int minus_one = -1; + std::vector rwork(static_cast(std::max(1, 3 * size - 2)), 0.0); + + zheev_(&jobz, + &uplo, + &size, + matrix.data(), + &lda, + eigenvalues.data(), + &work_query, + &minus_one, + rwork.data(), + &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer complex virtual-subspace diagonalization workspace query failed."); + } + const int lwork = std::max(1, static_cast(std::ceil(work_query.real()))); + std::vector work(static_cast(lwork), Complex(0.0, 0.0)); + zheev_(&jobz, + &uplo, + &size, + matrix.data(), + &lda, + eigenvalues.data(), + work.data(), + &lwork, + rwork.data(), + &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer complex virtual-subspace diagonalization failed."); + } + return eigenvalues; +} + +void validate_postprocess_input(const Vector& standard_delta_wavefunction, + const SternheimerDeltaPostprocessInput& input) +{ + if (standard_delta_wavefunction.empty()) + { + throw std::invalid_argument("Sternheimer delta postprocess requires a non-empty standard solution."); + } + if (input.volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer delta postprocess requires a positive grid volume element."); + } + if (input.virtual_states.size() != input.perturbation_matrix_elements.size()) + { + throw std::invalid_argument( + "Sternheimer delta postprocess requires one perturbation matrix element per virtual state."); + } + + const std::size_t grid_size = standard_delta_wavefunction.size(); + for (const Vector& occupied: input.occupied_wavefunctions) + { + check_vector_size(occupied, grid_size, "Sternheimer delta occupied state"); + } + for (const SternheimerDeltaVirtualState& state: input.virtual_states) + { + check_vector_size(state.orbital, grid_size, "Sternheimer delta virtual orbital"); + check_vector_size(state.residual, grid_size, "Sternheimer delta virtual residual"); + } +} + +void validate_virtual_states(const std::vector& virtual_states, + const std::size_t grid_size) +{ + for (const SternheimerDeltaVirtualState& state: virtual_states) + { + check_vector_size(state.orbital, grid_size, "Sternheimer delta virtual orbital"); + check_vector_size(state.residual, grid_size, "Sternheimer delta virtual residual"); + } +} + +SternheimerDeltaCoefficientComponents compute_delta_coefficient_components( + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + const Vector& out_wavefunction, + const double occupied_eigenvalue, + const double omega, + const double volume_element) +{ + SternheimerDeltaCoefficientComponents components; + components.sos.assign(virtual_states.size(), Complex(0.0, 0.0)); + components.pulay.assign(virtual_states.size(), Complex(0.0, 0.0)); + components.total.assign(virtual_states.size(), Complex(0.0, 0.0)); +#pragma omp parallel for schedule(static) + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + const Complex denominator(occupied_eigenvalue - virtual_states[ia].eigenvalue, -omega); + if (std::abs(denominator) < 1.0e-30) + { + throw std::runtime_error("Sternheimer delta coefficient evaluation found a singular denominator."); + } + const Complex residual_overlap + = sternheimer_fd_grid_dot(virtual_states[ia].residual, out_wavefunction, volume_element); + components.sos[ia] = perturbation_matrix_elements[ia] / denominator; + components.pulay[ia] = residual_overlap / denominator; + components.total[ia] = components.sos[ia] + components.pulay[ia]; + } + return components; +} + +void assemble_delta_wavefunction_components(const std::vector& virtual_states, + SternheimerDeltaPostprocessResult& result) +{ + result.in_sos_wavefunction.assign(result.out_wavefunction.size(), Complex(0.0, 0.0)); + result.in_pulay_wavefunction.assign(result.out_wavefunction.size(), Complex(0.0, 0.0)); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + axpy(result.sos_coefficients[ia], virtual_states[ia].orbital, result.in_sos_wavefunction); + axpy(result.pulay_coefficients[ia], virtual_states[ia].orbital, result.in_pulay_wavefunction); + } + + result.reconstructed_wavefunction = result.out_wavefunction; + axpy(Complex(1.0, 0.0), result.in_sos_wavefunction, result.reconstructed_wavefunction); + axpy(Complex(1.0, 0.0), result.in_pulay_wavefunction, result.reconstructed_wavefunction); +} + +void evaluate_full_grid_delta_hamiltonian_difference(const SternheimerFDHamiltonian& hamiltonian, + const double volume_element, + SternheimerDeltaSubspace& subspace) +{ + double reference_norm_squared = 0.0; + double difference_norm_squared = 0.0; + double max_abs_difference = 0.0; + std::vector h_virtual(subspace.virtual_states.size()); + for (std::size_t ket = 0; ket != subspace.virtual_states.size(); ++ket) + { + hamiltonian.apply(subspace.virtual_states[ket].orbital, h_virtual[ket]); + } + for (std::size_t ket = 0; ket != subspace.virtual_states.size(); ++ket) + { + for (std::size_t bra = 0; bra != subspace.virtual_states.size(); ++bra) + { + const Complex reference = bra == ket ? Complex(subspace.virtual_states[ket].eigenvalue, 0.0) + : Complex(0.0, 0.0); + const Complex full_grid = sternheimer_fd_grid_dot(subspace.virtual_states[bra].orbital, + h_virtual[ket], + volume_element); + const double difference = std::abs(full_grid - reference); + reference_norm_squared += std::norm(reference); + difference_norm_squared += difference * difference; + max_abs_difference = std::max(max_abs_difference, difference); + } + } + const double reference_norm = std::sqrt(reference_norm_squared); + const double difference_norm = std::sqrt(difference_norm_squared); + subspace.full_grid_hamiltonian_relative_difference + = reference_norm > 0.0 ? difference_norm / reference_norm : difference_norm; + subspace.full_grid_hamiltonian_max_abs_difference = max_abs_difference; +} + +constexpr std::size_t delta_grid_block_size = 4096; + +const Vector& grid_function_component(const SternheimerDeltaGridFunction& function, const int component) +{ + return component < 0 ? function.values : function.gradients[static_cast(component)]; +} + +Vector& grid_function_component(SternheimerDeltaGridFunction& function, const int component) +{ + return component < 0 ? function.values : function.gradients[static_cast(component)]; +} + +void pack_grid_function_component(const std::vector& functions, + const int component, + const std::size_t grid_begin, + const int grid_count, + std::vector& packed) +{ + packed.resize(static_cast(grid_count) * functions.size()); +#pragma omp parallel for schedule(static) + for (std::size_t function = 0; function != functions.size(); ++function) + { + const Vector& source = grid_function_component(functions[function], component); + Complex* destination = packed.data() + static_cast(grid_count) * function; + std::copy_n(source.data() + grid_begin, grid_count, destination); + } +} + +void pack_vector_component(const std::vector& vectors, + const std::size_t grid_begin, + const int grid_count, + std::vector& packed) +{ + packed.resize(static_cast(grid_count) * vectors.size()); +#pragma omp parallel for schedule(static) + for (std::size_t vector = 0; vector != vectors.size(); ++vector) + { + Complex* destination = packed.data() + static_cast(grid_count) * vector; + std::copy_n(vectors[vector]->data() + grid_begin, grid_count, destination); + } +} + +void accumulate_grid_component_product( + const std::vector& left, + const int left_component, + const std::vector& right, + const int right_component, + const std::vector* right_weights, + const Complex alpha, + std::vector& output) +{ + if (left.empty() || right.empty()) + { + return; + } + const std::size_t grid_size = grid_function_component(left.front(), left_component).size(); + const int left_size = static_cast(left.size()); + const int right_size = static_cast(right.size()); + if (output.size() != left.size() * right.size()) + { + throw std::invalid_argument("Sternheimer blocked grid product output size mismatch."); + } + if (right_weights != nullptr && right_weights->size() != grid_size) + { + throw std::invalid_argument("Sternheimer blocked grid product weight size mismatch."); + } + + std::vector left_packed; + std::vector right_packed; + for (std::size_t grid_begin = 0; grid_begin < grid_size; grid_begin += delta_grid_block_size) + { + const int grid_count = static_cast(std::min(delta_grid_block_size, grid_size - grid_begin)); + pack_grid_function_component(left, left_component, grid_begin, grid_count, left_packed); + const bool reuse_left = &left == &right && left_component == right_component + && right_weights == nullptr; + const Complex* right_data = left_packed.data(); + if (!reuse_left) + { + pack_grid_function_component(right, right_component, grid_begin, grid_count, right_packed); + if (right_weights != nullptr) + { +#pragma omp parallel for schedule(static) + for (std::size_t function = 0; function != right.size(); ++function) + { + Complex* column + = right_packed.data() + static_cast(grid_count) * function; + for (int ir = 0; ir != grid_count; ++ir) + { + column[ir] *= (*right_weights)[grid_begin + static_cast(ir)]; + } + } + } + right_data = right_packed.data(); + } + BlasConnector::gemm_cm('C', + 'N', + left_size, + right_size, + grid_count, + alpha, + left_packed.data(), + grid_count, + right_data, + grid_count, + Complex(1.0, 0.0), + output.data(), + left_size); + } +} + +void project_grid_functions_blocked( + const std::vector& occupied_functions, + std::vector& candidate_functions, + const double volume_element) +{ + if (occupied_functions.empty() || candidate_functions.empty()) + { + return; + } + const std::size_t grid_size = candidate_functions.front().values.size(); + const int occupied_count = static_cast(occupied_functions.size()); + const int candidate_count = static_cast(candidate_functions.size()); + std::vector coefficients(occupied_functions.size() * candidate_functions.size()); + std::vector occupied_packed; + std::vector corrections; + + for (int pass = 0; pass != 2; ++pass) + { + std::fill(coefficients.begin(), coefficients.end(), Complex(0.0, 0.0)); + accumulate_grid_component_product(occupied_functions, + -1, + candidate_functions, + -1, + nullptr, + Complex(volume_element, 0.0), + coefficients); + for (int component = -1; component != 3; ++component) + { + for (std::size_t grid_begin = 0; grid_begin < grid_size; + grid_begin += delta_grid_block_size) + { + const int grid_count + = static_cast(std::min(delta_grid_block_size, grid_size - grid_begin)); + pack_grid_function_component( + occupied_functions, component, grid_begin, grid_count, occupied_packed); + corrections.assign(static_cast(grid_count) * candidate_functions.size(), + Complex(0.0, 0.0)); + BlasConnector::gemm_cm('N', + 'N', + grid_count, + candidate_count, + occupied_count, + Complex(-1.0, 0.0), + occupied_packed.data(), + grid_count, + coefficients.data(), + occupied_count, + Complex(0.0, 0.0), + corrections.data(), + grid_count); +#pragma omp parallel for schedule(static) + for (std::size_t candidate = 0; candidate != candidate_functions.size(); ++candidate) + { + Vector& destination + = grid_function_component(candidate_functions[candidate], component); + const Complex* correction + = corrections.data() + static_cast(grid_count) * candidate; + for (int ir = 0; ir != grid_count; ++ir) + { + destination[grid_begin + static_cast(ir)] += correction[ir]; + } + } + } + } + } +} + +void hermitize_matrix(std::vector& matrix, const int size) +{ + for (int column = 0; column != size; ++column) + { + matrix[static_cast(column) + + static_cast(size) * static_cast(column)] + = Complex(matrix[static_cast(column) + + static_cast(size) * static_cast(column)] + .real(), + 0.0); + for (int row = 0; row != column; ++row) + { + const std::size_t upper + = static_cast(row) + + static_cast(size) * static_cast(column); + const std::size_t lower + = static_cast(column) + + static_cast(size) * static_cast(row); + const Complex value = 0.5 * (matrix[upper] + std::conj(matrix[lower])); + matrix[upper] = value; + matrix[lower] = std::conj(value); + } + } +} + +SternheimerDeltaGridMatrices assemble_delta_sternheimer_cross_matrices_blocked( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& left_functions, + const std::vector& right_functions, + const double volume_element) +{ + const std::size_t max_elements = Vector().max_size(); + const std::size_t max_blas_dimension = static_cast(std::numeric_limits::max()); + if (left_functions.size() > max_blas_dimension || right_functions.size() > max_blas_dimension + || (!right_functions.empty() && left_functions.size() > max_elements / right_functions.size())) + { + throw std::overflow_error("Sternheimer blocked cross matrix dimensions exceed BLAS or vector limits."); + } + const int left_size = static_cast(left_functions.size()); + const int right_size = static_cast(right_functions.size()); + const std::size_t grid_size = left_functions.empty() ? 0 : left_functions.front().values.size(); + const std::size_t packed_rows = std::min(delta_grid_block_size, grid_size); + if (!right_functions.empty() && packed_rows != 0 + && std::max(left_functions.size(), right_functions.size()) > max_elements / packed_rows) + { + throw std::overflow_error("Sternheimer blocked cross matrix packing size exceeds vector limits."); + } + const std::size_t matrix_size = left_functions.size() * right_functions.size(); + SternheimerDeltaGridMatrices matrices; + matrices.row_count = left_functions.size(); + matrices.column_count = right_functions.size(); + matrices.overlap.assign(matrix_size, Complex(0.0, 0.0)); + matrices.kinetic.assign(matrix_size, Complex(0.0, 0.0)); + matrices.local_potential.assign(matrix_size, Complex(0.0, 0.0)); + matrices.nonlocal.assign(matrix_size, Complex(0.0, 0.0)); + matrices.hamiltonian.assign(matrix_size, Complex(0.0, 0.0)); + if (left_functions.empty() || right_functions.empty()) + { + return matrices; + } + accumulate_grid_component_product(left_functions, + -1, + right_functions, + -1, + nullptr, + Complex(volume_element, 0.0), + matrices.overlap); + for (int direction = 0; direction != 3; ++direction) + { + accumulate_grid_component_product( + left_functions, + direction, + right_functions, + direction, + nullptr, + Complex(volume_element * hamiltonian.kinetic_prefactor(), 0.0), + matrices.kinetic); + } + accumulate_grid_component_product(left_functions, + -1, + right_functions, + -1, + &hamiltonian.local_potential(), + Complex(volume_element, 0.0), + matrices.local_potential); + + const SternheimerFDNonlocalProjector* nonlocal_projector = hamiltonian.nonlocal_projector(); + if (nonlocal_projector != nullptr) + { + if (std::abs(nonlocal_projector->volume_element() - volume_element) + > 1.0e-12 * std::max(nonlocal_projector->volume_element(), volume_element)) + { + throw std::invalid_argument( + "Sternheimer blocked nonlocal projector uses a different grid volume element."); + } + constexpr int nonlocal_block_size = 64; + std::vector basis_packed; + std::vector nonlocal_packed; + for (int block_begin = 0; block_begin < right_size;) + { + const int block_count = std::min(nonlocal_block_size, right_size - block_begin); + std::vector input_vectors; + input_vectors.reserve(static_cast(block_count)); + for (int column = 0; column != block_count; ++column) + { + input_vectors.push_back( + right_functions[static_cast(block_begin + column)].values); + } + std::vector nonlocal_vectors; + nonlocal_projector->apply_batch(input_vectors, nonlocal_vectors); + std::vector nonlocal_vector_pointers(static_cast(block_count)); + for (int column = 0; column != block_count; ++column) + { + nonlocal_vector_pointers[static_cast(column)] + = &nonlocal_vectors[static_cast(column)]; + } + Complex* output = matrices.nonlocal.data() + + static_cast(left_size) + * static_cast(block_begin); + for (std::size_t grid_begin = 0; grid_begin < grid_size; + grid_begin += delta_grid_block_size) + { + const int grid_count + = static_cast(std::min(delta_grid_block_size, grid_size - grid_begin)); + pack_grid_function_component( + left_functions, -1, grid_begin, grid_count, basis_packed); + pack_vector_component( + nonlocal_vector_pointers, grid_begin, grid_count, nonlocal_packed); + BlasConnector::gemm_cm('C', + 'N', + left_size, + block_count, + grid_count, + Complex(volume_element, 0.0), + basis_packed.data(), + grid_count, + nonlocal_packed.data(), + grid_count, + Complex(1.0, 0.0), + output, + left_size); + } + block_begin += block_count; + } + } + +#pragma omp parallel for schedule(static) + for (std::size_t index = 0; index != matrix_size; ++index) + { + matrices.hamiltonian[index] = matrices.kinetic[index] + matrices.local_potential[index] + + matrices.nonlocal[index]; + } + return matrices; +} + +SternheimerDeltaGridMatrices assemble_delta_sternheimer_grid_matrices_blocked( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& basis_functions, + const double volume_element) +{ + // Preserve the square path's packing, accumulation order, and S/H cleanup. + SternheimerDeltaGridMatrices matrices = assemble_delta_sternheimer_cross_matrices_blocked( + hamiltonian, basis_functions, basis_functions, volume_element); + hermitize_matrix(matrices.overlap, static_cast(basis_functions.size())); + hermitize_matrix(matrices.hamiltonian, static_cast(basis_functions.size())); + return matrices; +} + +std::vector diagonalize_complex_generalized_hermitian(std::vector& hamiltonian, + std::vector& overlap, + const int size) +{ + const int itype = 1; + const char jobz = 'V'; + const char uplo = 'U'; + const int lda = size; + const int ldb = size; + int info = 0; + std::vector eigenvalues(static_cast(size), 0.0); + Complex work_query(0.0, 0.0); + double rwork_query = 0.0; + int iwork_query = 0; + const int minus_one = -1; + zhegvd_(&itype, + &jobz, + &uplo, + &size, + hamiltonian.data(), + &lda, + overlap.data(), + &ldb, + eigenvalues.data(), + &work_query, + &minus_one, + &rwork_query, + &minus_one, + &iwork_query, + &minus_one, + &info); + if (info != 0) + { + throw std::runtime_error( + "Sternheimer blocked generalized eigensolver workspace query failed."); + } + const int lwork = std::max(1, static_cast(std::ceil(work_query.real()))); + const int lrwork = std::max(1, static_cast(std::ceil(rwork_query))); + const int liwork = std::max(1, iwork_query); + std::vector work(static_cast(lwork)); + std::vector rwork(static_cast(lrwork)); + std::vector iwork(static_cast(liwork)); + zhegvd_(&itype, + &jobz, + &uplo, + &size, + hamiltonian.data(), + &lda, + overlap.data(), + &ldb, + eigenvalues.data(), + work.data(), + &lwork, + rwork.data(), + &lrwork, + iwork.data(), + &liwork, + &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer blocked generalized eigensolver failed with info=" + + std::to_string(info) + "."); + } + return eigenvalues; +} + +void transform_grid_functions_blocked( + const std::vector& basis_functions, + const std::vector& coefficients, + const int output_size, + const bool retain_grid_functions, + SternheimerDeltaSubspace& subspace) +{ + const int basis_size = static_cast(basis_functions.size()); + const std::size_t grid_size = basis_functions.front().values.size(); + subspace.virtual_states.resize(static_cast(output_size)); + for (SternheimerDeltaVirtualState& state: subspace.virtual_states) + { + state.orbital.resize(grid_size); + } + if (retain_grid_functions) + { + subspace.grid_functions.resize(static_cast(output_size)); + for (SternheimerDeltaGridFunction& function: subspace.grid_functions) + { + function.values.resize(grid_size); + for (Vector& gradient: function.gradients) + { + gradient.resize(grid_size); + } + } + } + + std::vector basis_packed; + std::vector transformed; + const int last_component = retain_grid_functions ? 2 : -1; + for (int component = -1; component <= last_component; ++component) + { + for (std::size_t grid_begin = 0; grid_begin < grid_size; + grid_begin += delta_grid_block_size) + { + const int grid_count + = static_cast(std::min(delta_grid_block_size, grid_size - grid_begin)); + pack_grid_function_component( + basis_functions, component, grid_begin, grid_count, basis_packed); + transformed.resize(static_cast(grid_count) + * static_cast(output_size)); + BlasConnector::gemm_cm('N', + 'N', + grid_count, + output_size, + basis_size, + Complex(1.0, 0.0), + basis_packed.data(), + grid_count, + coefficients.data(), + basis_size, + Complex(0.0, 0.0), + transformed.data(), + grid_count); +#pragma omp parallel for schedule(static) + for (int state_index = 0; state_index != output_size; ++state_index) + { + const Complex* source + = transformed.data() + static_cast(grid_count) + * static_cast(state_index); + if (component < 0) + { + std::copy_n(source, + grid_count, + subspace.virtual_states[static_cast(state_index)] + .orbital.data() + + grid_begin); + if (retain_grid_functions) + { + std::copy_n(source, + grid_count, + subspace.grid_functions[static_cast(state_index)] + .values.data() + + grid_begin); + } + } + else + { + std::copy_n(source, + grid_count, + subspace.grid_functions[static_cast(state_index)] + .gradients[static_cast(component)] + .data() + + grid_begin); + } + } + } + } +} + +void project_vector_batch_blocked(const std::vector& basis, + const std::vector& vectors, + const double volume_element) +{ + if (basis.empty() || vectors.empty()) + { + return; + } + const std::size_t grid_size = basis.front()->size(); + const int basis_size = static_cast(basis.size()); + const int vector_count = static_cast(vectors.size()); + std::vector vector_sources(vectors.begin(), vectors.end()); + std::vector coefficients(basis.size() * vectors.size(), Complex(0.0, 0.0)); + std::vector basis_packed; + std::vector vectors_packed; + for (std::size_t grid_begin = 0; grid_begin < grid_size; grid_begin += delta_grid_block_size) + { + const int grid_count = static_cast(std::min(delta_grid_block_size, grid_size - grid_begin)); + pack_vector_component(basis, grid_begin, grid_count, basis_packed); + pack_vector_component(vector_sources, grid_begin, grid_count, vectors_packed); + BlasConnector::gemm_cm('C', + 'N', + basis_size, + vector_count, + grid_count, + Complex(volume_element, 0.0), + basis_packed.data(), + grid_count, + vectors_packed.data(), + grid_count, + Complex(1.0, 0.0), + coefficients.data(), + basis_size); + } + for (int basis_index = 0; basis_index != basis_size; ++basis_index) + { + const Complex norm + = sternheimer_fd_grid_dot(*basis[static_cast(basis_index)], + *basis[static_cast(basis_index)], + volume_element); + if (std::abs(norm) == 0.0) + { + for (int vector = 0; vector != vector_count; ++vector) + { + coefficients[static_cast(basis_index) + + basis.size() * static_cast(vector)] + = Complex(0.0, 0.0); + } + } + else + { + for (int vector = 0; vector != vector_count; ++vector) + { + coefficients[static_cast(basis_index) + + basis.size() * static_cast(vector)] + /= norm; + } + } + } + + std::vector corrections; + for (std::size_t grid_begin = 0; grid_begin < grid_size; grid_begin += delta_grid_block_size) + { + const int grid_count = static_cast(std::min(delta_grid_block_size, grid_size - grid_begin)); + pack_vector_component(basis, grid_begin, grid_count, basis_packed); + corrections.resize(static_cast(grid_count) * vectors.size()); + BlasConnector::gemm_cm('N', + 'N', + grid_count, + vector_count, + basis_size, + Complex(-1.0, 0.0), + basis_packed.data(), + grid_count, + coefficients.data(), + basis_size, + Complex(0.0, 0.0), + corrections.data(), + grid_count); +#pragma omp parallel for schedule(static) + for (std::size_t vector = 0; vector != vectors.size(); ++vector) + { + Complex* destination = vectors[vector]->data() + grid_begin; + const Complex* correction + = corrections.data() + static_cast(grid_count) * vector; + for (int ir = 0; ir != grid_count; ++ir) + { + destination[ir] += correction[ir]; + } + } + } +} + +SternheimerDeltaSubspace build_complete_reference_delta_sternheimer_subspace_blocked( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_functions, + std::vector candidate_functions, + const double volume_element, + const SternheimerDeltaSubspaceOptions& options) +{ + project_grid_functions_blocked(occupied_functions, candidate_functions, volume_element); + SternheimerDeltaGridMatrices matrices = assemble_delta_sternheimer_grid_matrices_blocked( + hamiltonian, candidate_functions, volume_element); + const int basis_size = static_cast(candidate_functions.size()); + for (int state = 0; state != basis_size; ++state) + { + const double norm = std::sqrt(std::max( + 0.0, + matrices.overlap[static_cast(state) + + candidate_functions.size() * static_cast(state)] + .real())); + if (norm <= options.norm_tolerance) + { + throw std::runtime_error( + "Sternheimer blocked complete subspace contains a linearly dependent candidate."); + } + } + const std::vector eigenvalues = diagonalize_complex_generalized_hermitian( + matrices.hamiltonian, matrices.overlap, basis_size); + + SternheimerDeltaSubspace subspace; + subspace.accepted_candidates = basis_size; + subspace.discarded_candidates = 0; + subspace.used_block_generalized_eigensolver = true; + transform_grid_functions_blocked(candidate_functions, + matrices.hamiltonian, + basis_size, + options.retain_grid_functions, + subspace); + for (int state = 0; state != basis_size; ++state) + { + subspace.virtual_states[static_cast(state)].eigenvalue + = eigenvalues[static_cast(state)]; + } + std::vector().swap(candidate_functions); + + if (options.evaluate_full_grid_difference) + { + evaluate_full_grid_delta_hamiltonian_difference(hamiltonian, volume_element, subspace); + } + else + { + subspace.full_grid_hamiltonian_relative_difference = -1.0; + subspace.full_grid_hamiltonian_max_abs_difference = -1.0; + } + + std::vector residual_projector; + residual_projector.reserve(occupied_functions.size() + subspace.virtual_states.size()); + for (const SternheimerDeltaGridFunction& occupied: occupied_functions) + { + residual_projector.push_back(&occupied.values); + } + for (const SternheimerDeltaVirtualState& state: subspace.virtual_states) + { + residual_projector.push_back(&state.orbital); + } + std::vector residuals; + residuals.reserve(subspace.virtual_states.size()); + for (SternheimerDeltaVirtualState& state: subspace.virtual_states) + { + hamiltonian.apply(state.orbital, state.residual); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != state.residual.size(); ++ir) + { + state.residual[ir] -= state.eigenvalue * state.orbital[ir]; + } + residuals.push_back(&state.residual); + } + project_vector_batch_blocked(residual_projector, residuals, volume_element); + return subspace; +} + +} // namespace + +SternheimerDeltaABlockMode parse_sternheimer_delta_a_block_mode(const std::string& name) +{ + if (name == "reference_value_gradient") + { + return SternheimerDeltaABlockMode::ReferenceValueGradient; + } + if (name == "grid") + { + return SternheimerDeltaABlockMode::FullGrid; + } + throw std::invalid_argument("Unknown Sternheimer Delta A-block mode: " + name); +} + +const char* sternheimer_delta_a_block_mode_name(const SternheimerDeltaABlockMode mode) +{ + switch (mode) + { + case SternheimerDeltaABlockMode::ReferenceValueGradient: + return "reference_value_gradient"; + case SternheimerDeltaABlockMode::FullGrid: + return "grid"; + } + throw std::invalid_argument("Invalid Sternheimer Delta A-block mode."); +} + +int sternheimer_delta_virtual_state_limit(const int requested_states, + const int candidate_states, + const int occupied_states) +{ + if (requested_states < 0 || candidate_states < 0 || occupied_states < 0) + { + throw std::invalid_argument("Sternheimer delta subspace dimensions must be non-negative."); + } + if (occupied_states > candidate_states) + { + throw std::invalid_argument("Sternheimer delta occupied dimension exceeds the candidate dimension."); + } + const int available_states = candidate_states - occupied_states; + return requested_states == 0 ? available_states : std::min(requested_states, available_states); +} + +SternheimerDeltaGridFunction make_delta_sternheimer_grid_function_with_fd_gradients( + const SternheimerFDHamiltonian::Vector& values, + const SternheimerFDHamiltonian::Grid& grid) +{ + if (grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0 || grid.hx <= 0.0 || grid.hy <= 0.0 + || grid.hz <= 0.0) + { + throw std::invalid_argument("Sternheimer delta grid gradient requires a valid uniform grid."); + } + if (static_cast(values.size()) != grid.size()) + { + throw std::invalid_argument("Sternheimer delta grid gradient value size does not match the grid."); + } + + SternheimerDeltaGridFunction function; + function.values = values; + for (Vector& gradient: function.gradients) + { + gradient.assign(values.size(), Complex(0.0, 0.0)); + } + const auto index = [&grid](const int ix, const int iy, const int iz) { + return (ix * grid.ny + iy) * grid.nz + iz; + }; + const auto derivative = [&values, &grid, &index](const int ix, + const int iy, + const int iz, + const int direction) { + const int coordinate = direction == 0 ? ix : (direction == 1 ? iy : iz); + const int count = direction == 0 ? grid.nx : (direction == 1 ? grid.ny : grid.nz); + const double spacing = direction == 0 ? grid.hx : (direction == 1 ? grid.hy : grid.hz); + if (count == 1) + { + return Complex(0.0, 0.0); + } + + int lower = coordinate - 1; + int upper = coordinate + 1; + double denominator = 2.0 * spacing; + if (grid.periodic) + { + lower = (lower + count) % count; + upper %= count; + } + else if (coordinate == 0) + { + lower = 0; + upper = 1; + denominator = spacing; + } + else if (coordinate == count - 1) + { + lower = count - 2; + upper = count - 1; + denominator = spacing; + } + + const int lower_index = direction == 0 ? index(lower, iy, iz) + : direction == 1 ? index(ix, lower, iz) + : index(ix, iy, lower); + const int upper_index = direction == 0 ? index(upper, iy, iz) + : direction == 1 ? index(ix, upper, iz) + : index(ix, iy, upper); + return (values[static_cast(upper_index)] + - values[static_cast(lower_index)]) + / denominator; + }; + + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const std::size_t ir = static_cast(index(ix, iy, iz)); + for (int direction = 0; direction != 3; ++direction) + { + function.gradients[static_cast(direction)][ir] + = derivative(ix, iy, iz, direction); + } + } + } + } + return function; +} + +void accumulate_delta_sternheimer_bloch_samples( + const std::vector& sampled_values, + const std::array, 3>& sampled_gradients, + const int sample_count, + const int orbital_count, + const std::size_t grid_begin, + const std::size_t function_begin, + const SternheimerReducedKPoint& kpoint, + const std::array& lattice_translation, + std::vector& functions) +{ + if (sample_count < 0 || orbital_count <= 0) + { + throw std::invalid_argument("Sternheimer Bloch samples require non-negative samples and positive orbitals."); + } + const std::size_t sample_size = static_cast(sample_count); + const std::size_t orbital_size = static_cast(orbital_count); + const std::size_t buffer_size = sample_size * orbital_size; + if (sampled_values.size() != buffer_size) + { + throw std::invalid_argument("Sternheimer Bloch value buffer size is inconsistent."); + } + for (const std::vector& gradient: sampled_gradients) + { + if (gradient.size() != buffer_size) + { + throw std::invalid_argument("Sternheimer Bloch gradient buffer size is inconsistent."); + } + } + if (function_begin > functions.size() || orbital_size > functions.size() - function_begin) + { + throw std::invalid_argument("Sternheimer Bloch sample function range is out of bounds."); + } + for (std::size_t orbital = 0; orbital != orbital_size; ++orbital) + { + const SternheimerDeltaGridFunction& function = functions[function_begin + orbital]; + if (grid_begin > function.values.size() || sample_size > function.values.size() - grid_begin) + { + throw std::invalid_argument("Sternheimer Bloch sample grid range is out of bounds."); + } + validate_grid_function(function, function.values.size(), "Sternheimer Bloch sample"); + } + + const Complex phase = sternheimer_bloch_phase(kpoint, lattice_translation); + for (std::size_t sample = 0; sample != sample_size; ++sample) + { + const std::size_t grid_index = grid_begin + sample; + for (std::size_t orbital = 0; orbital != orbital_size; ++orbital) + { + const std::size_t buffer_index = sample * orbital_size + orbital; + SternheimerDeltaGridFunction& function = functions[function_begin + orbital]; + function.values[grid_index] += phase * sampled_values[buffer_index]; + for (std::size_t direction = 0; direction != function.gradients.size(); ++direction) + { + function.gradients[direction][grid_index] + += phase * sampled_gradients[direction][buffer_index]; + } + } + } +} + +void accumulate_delta_sternheimer_lcao_state_samples( + const std::vector& sampled_values, + const std::array, 3>& sampled_gradients, + const int sample_count, + const int orbital_count, + const std::size_t grid_begin, + const std::size_t ao_begin, + const std::vector>& coefficients, + const SternheimerReducedKPoint& kpoint, + const std::array& lattice_translation, + std::vector& functions) +{ + if (sample_count < 0 || orbital_count <= 0 || coefficients.size() != functions.size()) + { + throw std::invalid_argument( + "Sternheimer direct LCAO sampling requires valid sample, orbital, coefficient, and state counts."); + } + const std::size_t sample_size = static_cast(sample_count); + const std::size_t orbital_size = static_cast(orbital_count); + const std::size_t buffer_size = sample_size * orbital_size; + if (sampled_values.size() != buffer_size) + { + throw std::invalid_argument("Sternheimer direct LCAO value buffer size is inconsistent."); + } + for (const std::vector& gradient: sampled_gradients) + { + if (gradient.size() != buffer_size) + { + throw std::invalid_argument("Sternheimer direct LCAO gradient buffer size is inconsistent."); + } + } + for (std::size_t state_index = 0; state_index != functions.size(); ++state_index) + { + if (ao_begin > coefficients[state_index].size() + || orbital_size > coefficients[state_index].size() - ao_begin) + { + throw std::invalid_argument("Sternheimer direct LCAO coefficient AO range is out of bounds."); + } + const SternheimerDeltaGridFunction& function = functions[state_index]; + if (grid_begin > function.values.size() || sample_size > function.values.size() - grid_begin) + { + throw std::invalid_argument("Sternheimer direct LCAO sample grid range is out of bounds."); + } + validate_grid_function(function, function.values.size(), "Sternheimer direct LCAO sample"); + } + + const Complex image_phase = sternheimer_bloch_phase(kpoint, lattice_translation); +#pragma omp parallel for schedule(static) + for (std::size_t sample = 0; sample != sample_size; ++sample) + { + const std::size_t grid_index = grid_begin + sample; + for (std::size_t state_index = 0; state_index != functions.size(); ++state_index) + { + SternheimerDeltaGridFunction& state = functions[state_index]; + Complex value(0.0, 0.0); + std::array gradient{{Complex(0.0, 0.0), + Complex(0.0, 0.0), + Complex(0.0, 0.0)}}; + for (std::size_t orbital = 0; orbital != orbital_size; ++orbital) + { + const std::size_t buffer_index = sample * orbital_size + orbital; + const Complex coefficient = coefficients[state_index][ao_begin + orbital]; + value += coefficient * sampled_values[buffer_index]; + for (std::size_t direction = 0; direction != gradient.size(); ++direction) + { + gradient[direction] += coefficient * sampled_gradients[direction][buffer_index]; + } + } + state.values[grid_index] += image_phase * value; + for (std::size_t direction = 0; direction != gradient.size(); ++direction) + { + state.gradients[direction][grid_index] += image_phase * gradient[direction]; + } + } + } +} + +SternheimerDeltaGridFunction linear_combination_delta_sternheimer_grid_functions( + const std::vector& basis_functions, + const std::vector& coefficients) +{ + if (basis_functions.size() != coefficients.size()) + { + throw std::invalid_argument("Sternheimer LCAO grid combination requires one coefficient per AO."); + } + + SternheimerDeltaGridFunction state; + if (basis_functions.empty()) + { + return state; + } + + const std::size_t grid_size = basis_functions.front().values.size(); + state.values.assign(grid_size, Complex(0.0, 0.0)); + for (Vector& gradient: state.gradients) + { + gradient.assign(grid_size, Complex(0.0, 0.0)); + } + for (std::size_t ia = 0; ia != basis_functions.size(); ++ia) + { + validate_grid_function(basis_functions[ia], grid_size, "Sternheimer LCAO AO grid function"); + axpy_grid_function(coefficients[ia], basis_functions[ia], state); + } + return state; +} + +std::vector orthonormalize_delta_sternheimer_grid_functions( + const std::vector& functions, + const double volume_element, + const double norm_tolerance) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer grid-function orthonormalization requires a positive volume element."); + } + if (norm_tolerance < 0.0) + { + throw std::invalid_argument("Sternheimer grid-function orthonormalization requires a non-negative tolerance."); + } + if (functions.empty()) + { + return {}; + } + + const std::size_t grid_size = functions.front().values.size(); + auto dot = [volume_element](const Vector& lhs, const Vector& rhs) { + return sternheimer_fd_grid_dot(lhs, rhs, volume_element); + }; + + std::vector orthonormal; + orthonormal.reserve(functions.size()); + for (SternheimerDeltaGridFunction function: functions) + { + validate_grid_function(function, grid_size, "Sternheimer grid-function orthonormalization"); + for (int pass = 0; pass != 2; ++pass) + { + for (const SternheimerDeltaGridFunction& accepted: orthonormal) + { + axpy_grid_function(-dot(accepted.values, function.values), accepted, function); + } + } + const double norm = sternheimer_fd_grid_norm(function.values, volume_element); + if (norm <= norm_tolerance) + { + throw std::runtime_error( + "Sternheimer grid-function orthonormalization found a linearly dependent state."); + } + scale_grid_function(function, Complex(1.0 / norm, 0.0)); + orthonormal.push_back(std::move(function)); + } + return orthonormal; +} + +std::vector> enumerate_delta_sternheimer_periodic_images( + const SternheimerFDHamiltonian::Grid& grid, + const std::array& atom_position, + const double cutoff_radius) +{ + if (grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0 || grid.hx <= 0.0 || grid.hy <= 0.0 + || grid.hz <= 0.0) + { + throw std::invalid_argument("Sternheimer delta periodic images require a valid uniform grid."); + } + if (!std::isfinite(cutoff_radius) || cutoff_radius < 0.0) + { + throw std::invalid_argument("Sternheimer delta periodic images require a non-negative finite cutoff."); + } + for (const double coordinate: atom_position) + { + if (!std::isfinite(coordinate)) + { + throw std::invalid_argument("Sternheimer delta periodic images require finite atom coordinates."); + } + } + if (!grid.periodic) + { + return {{0, 0, 0}}; + } + + const SternheimerFDLatticeVectors dual = sternheimer_fd_grid_dual_vectors(grid); + std::array image_min{}; + std::array image_max{}; + for (int direction = 0; direction != 3; ++direction) + { + double reduced_position = 0.0; + double dual_norm_squared = 0.0; + for (int component = 0; component != 3; ++component) + { + reduced_position += dual[direction][component] * atom_position[component]; + dual_norm_squared += dual[direction][component] * dual[direction][component]; + } + reduced_position -= std::floor(reduced_position); + const double reduced_cutoff = std::sqrt(dual_norm_squared) * cutoff_radius; + image_min[static_cast(direction)] + = static_cast(std::ceil(-reduced_position - reduced_cutoff)); + image_max[static_cast(direction)] + = static_cast(std::floor(1.0 - reduced_position + reduced_cutoff)); + } + + std::vector> images; + for (int ix = image_min[0]; ix <= image_max[0]; ++ix) + { + for (int iy = image_min[1]; iy <= image_max[1]; ++iy) + { + for (int iz = image_min[2]; iz <= image_max[2]; ++iz) + { + images.push_back({ix, iy, iz}); + } + } + } + return images; +} + +std::vector> recover_delta_grid_ao_coefficients( + const std::vector& basis, + const std::vector& states, + const double volume_element, + const double reconstruction_tolerance, + double& maximum_relative_error, + double* gram_reciprocal_condition) +{ + if (basis.empty() || states.empty() || !std::isfinite(volume_element) || volume_element <= 0. + || !std::isfinite(reconstruction_tolerance) || reconstruction_tolerance <= 0.) + { + throw std::invalid_argument("Invalid AO coordinate reconstruction request."); + } + const int nao = static_cast(basis.size()); + const int nstates = static_cast(states.size()); + const std::size_t grid_size = basis.front().values.size(); + if (grid_size == 0) + { + throw std::invalid_argument("Empty AO reconstruction grid."); + } + for (const auto& function: basis) + { + if (function.values.size() != grid_size) + { + throw std::invalid_argument("AO reconstruction grid dimensions differ."); + } + } + for (const auto* state: states) + { + if (state == nullptr || state->size() != grid_size) + { + throw std::invalid_argument("AO reconstruction state dimensions differ."); + } + } + std::vector gram(basis.size() * basis.size(), 0.); + accumulate_grid_component_product(basis, -1, basis, -1, nullptr, Complex(volume_element, 0.), gram); + for (const auto value: gram) + { + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + { + throw std::runtime_error("Non-finite sampled AO Gram matrix."); + } + } + // A small wavefunction residual cannot bound coordinates in a nearly null AO direction. + auto gram_eigenvectors = gram; + const auto gram_eigenvalues = diagonalize_complex_hermitian(gram_eigenvectors, nao); + const double reciprocal_condition = gram_eigenvalues.front() / gram_eigenvalues.back(); + if (!std::isfinite(reciprocal_condition) || gram_eigenvalues.back() <= 0. || reciprocal_condition < 1.e-8) + { + throw std::runtime_error("Sampled AO Gram matrix is ill-conditioned (rcond < 1e-8); " + "refine the grid or remove near-dependent orbitals."); + } + if (gram_reciprocal_condition != nullptr) + { + *gram_reciprocal_condition = reciprocal_condition; + } + std::vector coordinates(basis.size() * states.size(), 0.); + std::vector packed_basis, packed_states; + for (std::size_t begin = 0; begin < grid_size; begin += delta_grid_block_size) + { + const int count = static_cast(std::min(delta_grid_block_size, grid_size - begin)); + pack_grid_function_component(basis, -1, begin, count, packed_basis); + pack_vector_component(states, begin, count, packed_states); + BlasConnector::gemm_cm('C', + 'N', + nao, + nstates, + count, + Complex(volume_element, 0.), + packed_basis.data(), + count, + packed_states.data(), + count, + Complex(1., 0.), + coordinates.data(), + nao); + } + std::vector pivots(basis.size()); + int info = 0; + zgesv_(&nao, &nstates, gram.data(), &nao, pivots.data(), coordinates.data(), &nao, &info); + if (info != 0) + { + throw std::runtime_error("AO coordinate reconstruction Gram solve failed."); + } + std::vector errors(states.size(), 0.), norms(states.size(), 0.); + for (std::size_t begin = 0; begin < grid_size; begin += delta_grid_block_size) + { + const int count = static_cast(std::min(delta_grid_block_size, grid_size - begin)); + pack_grid_function_component(basis, -1, begin, count, packed_basis); + pack_vector_component(states, begin, count, packed_states); + std::vector reconstructed(static_cast(count) * states.size()); + BlasConnector::gemm_cm('N', + 'N', + count, + nstates, + nao, + Complex(1., 0.), + packed_basis.data(), + count, + coordinates.data(), + nao, + Complex(0., 0.), + reconstructed.data(), + count); + for (int state = 0; state < nstates; ++state) + { + for (int ir = 0; ir < count; ++ir) + { + const std::size_t index = static_cast(state) * count + ir; + errors[state] += std::norm(reconstructed[index] - packed_states[index]); + norms[state] += std::norm(packed_states[index]); + } + } + } + maximum_relative_error = 0.; + std::vector> result(states.size()); + for (int state = 0; state < nstates; ++state) + { + const double error = std::sqrt(errors[state] / norms[state]); + if (!std::isfinite(error) || error > reconstruction_tolerance) + { + throw std::runtime_error("Delta state cannot be reconstructed in the supplied AO span."); + } + maximum_relative_error = std::max(maximum_relative_error, error); + result[state].assign(coordinates.begin() + state * nao, coordinates.begin() + (state + 1) * nao); + } + return result; +} + +SternheimerDeltaCoefficientComponents solve_delta_sternheimer_subspace_coefficients( + const std::vector& hamiltonian_matrix, + const std::vector& overlap_matrix, + const std::vector& rhs, + const std::vector& hamiltonian_out_coupling, + const SternheimerFDHamiltonian::Complex shift) +{ + const int size = static_cast(rhs.size()); + if (hamiltonian_out_coupling.size() != rhs.size()) + { + throw std::invalid_argument("Sternheimer delta subspace coupling size does not match the right-hand side."); + } + const std::size_t matrix_size = static_cast(size) * static_cast(size); + if (hamiltonian_matrix.size() != matrix_size || overlap_matrix.size() != matrix_size) + { + throw std::invalid_argument("Sternheimer delta subspace matrices do not match the right-hand side size."); + } + + SternheimerDeltaCoefficientComponents result; + result.sos.assign(rhs.size(), Complex(0.0, 0.0)); + result.pulay.assign(rhs.size(), Complex(0.0, 0.0)); + result.total.assign(rhs.size(), Complex(0.0, 0.0)); + if (size == 0) + { + return result; + } + + std::vector shifted_operator(matrix_size, Complex(0.0, 0.0)); + for (std::size_t index = 0; index != matrix_size; ++index) + { + shifted_operator[index] = hamiltonian_matrix[index] - shift * overlap_matrix[index]; + } + constexpr int right_hand_side_count = 2; + std::vector solutions(static_cast(right_hand_side_count) * rhs.size(), Complex(0.0, 0.0)); + for (int index = 0; index != size; ++index) + { + solutions[static_cast(index)] = rhs[static_cast(index)]; + solutions[static_cast(size + index)] = -hamiltonian_out_coupling[static_cast(index)]; + } + + std::vector pivots(static_cast(size), 0); + int info = 0; + zgesv_(&size, + &right_hand_side_count, + shifted_operator.data(), + &size, + pivots.data(), + solutions.data(), + &size, + &info); + if (info < 0) + { + throw std::runtime_error("Sternheimer delta subspace solve received an invalid LAPACK argument."); + } + if (info > 0) + { + throw std::runtime_error("Sternheimer delta subspace shifted matrix is singular."); + } + + for (int index = 0; index != size; ++index) + { + const std::size_t coefficient_index = static_cast(index); + result.sos[coefficient_index] = solutions[coefficient_index]; + result.pulay[coefficient_index] = solutions[static_cast(size + index)]; + result.total[coefficient_index] = result.sos[coefficient_index] + result.pulay[coefficient_index]; + } + return result; +} + +SternheimerDeltaPostprocessResult postprocess_delta_sternheimer_solution( + const SternheimerFDHamiltonian::Vector& standard_delta_wavefunction, + const SternheimerDeltaPostprocessInput& input) +{ + validate_postprocess_input(standard_delta_wavefunction, input); + + auto dot = [&input](const Vector& lhs, const Vector& rhs) { + return sternheimer_fd_grid_dot(lhs, rhs, input.volume_element); + }; + + SternheimerDeltaPostprocessResult result; + result.out_wavefunction = standard_delta_wavefunction; + SternheimerRPA::project_out_subspace(input.occupied_wavefunctions, dot, result.out_wavefunction); + const std::vector virtual_orbitals = collect_virtual_orbitals(input.virtual_states); + SternheimerRPA::project_out_subspace(virtual_orbitals, dot, result.out_wavefunction); + + const SternheimerDeltaCoefficientComponents components + = compute_delta_coefficient_components(input.virtual_states, + input.perturbation_matrix_elements, + result.out_wavefunction, + input.occupied_eigenvalue, + input.omega, + input.volume_element); + result.sos_coefficients = components.sos; + result.pulay_coefficients = components.pulay; + result.coefficients = components.total; + assemble_delta_wavefunction_components(input.virtual_states, result); + result.out_norm = sternheimer_fd_grid_norm(result.out_wavefunction, input.volume_element); + result.reconstruction_error + = difference_norm(result.reconstructed_wavefunction, standard_delta_wavefunction, input.volume_element); + return result; +} + +SternheimerDeltaPulayOperatorComponents decompose_delta_sternheimer_pulay_operator_terms( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& fixed_local_potential, + const std::vector& occupied_wavefunctions, + const std::vector& virtual_states, + const Vector& out_wavefunction, + const double occupied_eigenvalue, + const double omega, + const double volume_element) +{ + const std::size_t grid_size = static_cast(hamiltonian.grid().size()); + if (fixed_local_potential.size() != grid_size || out_wavefunction.size() != grid_size) + { + throw std::invalid_argument( + "Sternheimer Pulay operator decomposition requires full-grid potentials and wavefunctions."); + } + if (volume_element <= 0.0) + { + throw std::invalid_argument( + "Sternheimer Pulay operator decomposition requires a positive grid volume element."); + } + validate_virtual_states(virtual_states, grid_size); + for (const Vector& occupied: occupied_wavefunctions) + { + check_vector_size(occupied, grid_size, "Sternheimer Pulay operator occupied state"); + } + + SternheimerDeltaPulayOperatorComponents result; + Vector* outputs[] = {&result.kinetic, + &result.fixed_local, + &result.hxc_local, + &result.nonlocal, + &result.eigenvalue, + &result.total}; + for (Vector* output: outputs) + { + output->assign(grid_size, Complex(0.0, 0.0)); + } + + const auto dot = [volume_element](const Vector& lhs, const Vector& rhs) { + return sternheimer_fd_grid_dot(lhs, rhs, volume_element); + }; + std::vector fixed_subspace = occupied_wavefunctions; + const std::vector virtual_orbitals = collect_virtual_orbitals(virtual_states); + fixed_subspace.insert(fixed_subspace.end(), virtual_orbitals.begin(), virtual_orbitals.end()); + const std::vector& full_local_potential = hamiltonian.local_potential(); + + for (const SternheimerDeltaVirtualState& state: virtual_states) + { + const Complex denominator(occupied_eigenvalue - state.eigenvalue, -omega); + if (std::abs(denominator) < 1.0e-30) + { + throw std::runtime_error( + "Sternheimer Pulay operator decomposition found a singular denominator."); + } + + std::array residual_terms; + for (Vector& term: residual_terms) + { + term.assign(grid_size, Complex(0.0, 0.0)); + } + hamiltonian.apply_kinetic(state.orbital, residual_terms[0]); + hamiltonian.apply_nonlocal(state.orbital, residual_terms[3]); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != grid_size; ++ir) + { + residual_terms[1][ir] = fixed_local_potential[ir] * state.orbital[ir]; + residual_terms[2][ir] + = (full_local_potential[ir] - fixed_local_potential[ir]) * state.orbital[ir]; + residual_terms[4][ir] = -state.eigenvalue * state.orbital[ir]; + } + + Vector* component_outputs[] = {&result.kinetic, + &result.fixed_local, + &result.hxc_local, + &result.nonlocal, + &result.eigenvalue}; + for (std::size_t iterm = 0; iterm != residual_terms.size(); ++iterm) + { + SternheimerRPA::project_out_subspace(fixed_subspace, dot, residual_terms[iterm]); + const Complex coefficient = dot(residual_terms[iterm], out_wavefunction) / denominator; + axpy(coefficient, state.orbital, *component_outputs[iterm]); + } + + const Complex total_coefficient = dot(state.residual, out_wavefunction) / denominator; + axpy(total_coefficient, state.orbital, result.total); + } + return result; +} + +SternheimerDeltaGridMatrices assemble_delta_sternheimer_grid_matrices_fast( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& basis_functions, + const double volume_element) +{ + if (!std::isfinite(volume_element) || volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer reference delta matrices require a finite positive grid volume element."); + } + for (const auto& function: basis_functions) + { + validate_grid_function(function, hamiltonian.grid().size(), "Sternheimer reference delta basis"); + } + return assemble_delta_sternheimer_grid_matrices_blocked(hamiltonian, basis_functions, volume_element); +} + +SternheimerDeltaGridMatrices assemble_delta_sternheimer_cross_matrices_fast( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& left_functions, + const std::vector& right_functions, + const double volume_element) +{ + if (!std::isfinite(volume_element) || volume_element <= 0.0 + || !std::isfinite(hamiltonian.kinetic_prefactor())) + { + throw std::invalid_argument( + "Sternheimer cross matrices require a finite positive grid volume element and finite kinetic prefactor."); + } + if (!std::isfinite(volume_element * hamiltonian.kinetic_prefactor())) + { + throw std::overflow_error("Sternheimer cross matrix kinetic quadrature weight overflow."); + } + const std::size_t grid_size = static_cast(hamiltonian.grid().size()); + for (const auto* functions: {&left_functions, &right_functions}) + { + const std::string context = functions == &left_functions + ? "Sternheimer cross matrix left basis" + : "Sternheimer cross matrix right basis"; + for (const auto& function: *functions) + { + validate_grid_function(function, grid_size, context); + for (int component = -1; component != 3; ++component) + { + validate_finite_grid_vector(grid_function_component(function, component), context); + } + } + } + for (const double value: hamiltonian.local_potential()) + { + if (!std::isfinite(value)) + { + throw std::invalid_argument("Sternheimer cross matrix local potential contains a nonfinite value."); + } + } + const SternheimerFDNonlocalProjector* projector = hamiltonian.nonlocal_projector(); + if (projector != nullptr) + { + if (!std::isfinite(projector->volume_element()) || projector->volume_element() <= 0.0 + || std::abs(projector->volume_element() - volume_element) + > 1.0e-12 * std::max(projector->volume_element(), volume_element)) + { + throw std::invalid_argument( + "Sternheimer cross matrix nonlocal projector uses an invalid or different grid volume element."); + } + for (const auto& block: projector->blocks()) + { + for (const Vector& values: block.projectors) + { + validate_finite_grid_vector(values, "Sternheimer cross matrix nonlocal projector"); + } + for (const Vector& row: block.d_matrix) + { + validate_finite_grid_vector(row, "Sternheimer cross matrix nonlocal D"); + } + } + } + SternheimerDeltaGridMatrices matrices = assemble_delta_sternheimer_cross_matrices_blocked( + hamiltonian, left_functions, right_functions, volume_element); + for (const Vector* component: {&matrices.overlap, &matrices.kinetic, &matrices.local_potential, + &matrices.nonlocal, &matrices.hamiltonian}) + { + for (const Complex value: *component) + { + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + { + throw std::overflow_error("Sternheimer cross matrix quadrature produced a nonfinite result."); + } + } + } + return matrices; +} + +SternheimerDeltaGridMatrices assemble_delta_sternheimer_grid_matrices( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& basis_functions, + const double volume_element) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer reference delta matrices require a positive grid volume element."); + } + const std::size_t grid_size = static_cast(hamiltonian.grid().size()); + for (const SternheimerDeltaGridFunction& function: basis_functions) + { + validate_grid_function(function, grid_size, "Sternheimer reference delta basis"); + } + + const SternheimerFDNonlocalProjector* nonlocal_projector = hamiltonian.nonlocal_projector(); + if (nonlocal_projector != nullptr + && std::abs(nonlocal_projector->volume_element() - volume_element) + > 1.0e-12 * std::max(nonlocal_projector->volume_element(), volume_element)) + { + throw std::invalid_argument( + "Sternheimer reference delta nonlocal projector uses a different grid volume element."); + } + + const int basis_size = static_cast(basis_functions.size()); + const std::size_t matrix_size + = static_cast(basis_size) * static_cast(basis_size); + SternheimerDeltaGridMatrices matrices; + matrices.row_count = basis_functions.size(); + matrices.column_count = basis_functions.size(); + matrices.overlap.assign(matrix_size, Complex(0.0, 0.0)); + matrices.kinetic.assign(matrix_size, Complex(0.0, 0.0)); + matrices.local_potential.assign(matrix_size, Complex(0.0, 0.0)); + matrices.nonlocal.assign(matrix_size, Complex(0.0, 0.0)); + matrices.hamiltonian.assign(matrix_size, Complex(0.0, 0.0)); + if (basis_size == 0) + { + return matrices; + } + + std::vector nonlocal_basis(static_cast(basis_size)); + if (nonlocal_projector != nullptr) + { + for (int ib = 0; ib != basis_size; ++ib) + { + nonlocal_projector->apply(basis_functions[static_cast(ib)].values, + nonlocal_basis[static_cast(ib)]); + } + } + + const std::vector& local_potential = hamiltonian.local_potential(); + const double kinetic_prefactor = hamiltonian.kinetic_prefactor(); + for (int ib = 0; ib != basis_size; ++ib) + { + const SternheimerDeltaGridFunction& ket = basis_functions[static_cast(ib)]; + for (int ia = 0; ia != basis_size; ++ia) + { + const SternheimerDeltaGridFunction& bra = basis_functions[static_cast(ia)]; + Complex overlap(0.0, 0.0); + Complex kinetic_element(0.0, 0.0); + Complex local_potential_element(0.0, 0.0); + Complex nonlocal_element(0.0, 0.0); + for (std::size_t ir = 0; ir != grid_size; ++ir) + { + overlap += volume_element * std::conj(bra.values[ir]) * ket.values[ir]; + Complex gradient_dot(0.0, 0.0); + for (int direction = 0; direction != 3; ++direction) + { + gradient_dot += std::conj(bra.gradients[static_cast(direction)][ir]) + * ket.gradients[static_cast(direction)][ir]; + } + kinetic_element += volume_element * kinetic_prefactor * gradient_dot; + local_potential_element += volume_element * std::conj(bra.values[ir]) + * local_potential[ir] * ket.values[ir]; + } + if (nonlocal_projector != nullptr) + { + nonlocal_element = sternheimer_fd_grid_dot( + bra.values, nonlocal_basis[static_cast(ib)], volume_element); + } + const std::size_t index + = static_cast(ia) + static_cast(basis_size) * static_cast(ib); + matrices.overlap[index] = overlap; + matrices.kinetic[index] = kinetic_element; + matrices.local_potential[index] = local_potential_element; + matrices.nonlocal[index] = nonlocal_element; + matrices.hamiltonian[index] = kinetic_element + local_potential_element + nonlocal_element; + } + } + return matrices; +} + +SternheimerDeltaSubspace build_reference_delta_sternheimer_subspace( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_functions, + std::vector candidate_functions, + const double volume_element, + const SternheimerDeltaSubspaceOptions& options) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer reference delta subspace requires a positive grid volume element."); + } + if (options.max_virtual_states < 0) + { + throw std::invalid_argument("Sternheimer reference delta max_virtual_states must be non-negative."); + } + if (options.norm_tolerance < 0.0) + { + throw std::invalid_argument("Sternheimer reference delta norm_tolerance must be non-negative."); + } + + const std::size_t grid_size = static_cast(hamiltonian.grid().size()); + for (const SternheimerDeltaGridFunction& occupied: occupied_functions) + { + validate_grid_function(occupied, grid_size, "Sternheimer reference delta occupied function"); + } + for (const SternheimerDeltaGridFunction& candidate: candidate_functions) + { + validate_grid_function(candidate, grid_size, "Sternheimer reference delta candidate function"); + } + + const int input_candidate_count = static_cast(candidate_functions.size()); + const bool complete_requested_space + = options.max_virtual_states == 0 || options.max_virtual_states >= input_candidate_count; + if (options.use_block_generalized_eigensolver && complete_requested_space + && input_candidate_count > 0) + { + return build_complete_reference_delta_sternheimer_subspace_blocked( + hamiltonian, + occupied_functions, + std::move(candidate_functions), + volume_element, + options); + } + + auto dot = [volume_element](const Vector& lhs, const Vector& rhs) { + return sternheimer_fd_grid_dot(lhs, rhs, volume_element); + }; + std::vector residual_candidates = std::move(candidate_functions); + for (SternheimerDeltaGridFunction& candidate: residual_candidates) + { + for (int pass = 0; pass != 2; ++pass) + { + for (const SternheimerDeltaGridFunction& occupied: occupied_functions) + { + axpy_grid_function(-dot(occupied.values, candidate.values), occupied, candidate); + } + } + } + + const int candidate_limit = options.max_virtual_states > 0 + ? std::min(options.max_virtual_states, + static_cast(residual_candidates.size())) + : static_cast(residual_candidates.size()); + std::vector selected(residual_candidates.size(), false); + std::vector orthonormal_candidates; + orthonormal_candidates.reserve(static_cast(candidate_limit)); + while (static_cast(orthonormal_candidates.size()) < candidate_limit) + { + int pivot = -1; + double pivot_norm = options.norm_tolerance; + for (std::size_t candidate_index = 0; candidate_index != residual_candidates.size(); ++candidate_index) + { + if (selected[candidate_index]) + { + continue; + } + const double norm + = sternheimer_fd_grid_norm(residual_candidates[candidate_index].values, volume_element); + if (norm > pivot_norm) + { + pivot = static_cast(candidate_index); + pivot_norm = norm; + } + } + if (pivot < 0) + { + break; + } + + const std::size_t pivot_index = static_cast(pivot); + selected[pivot_index] = true; + SternheimerDeltaGridFunction accepted = std::move(residual_candidates[pivot_index]); + scale_grid_function(accepted, Complex(1.0 / pivot_norm, 0.0)); + orthonormal_candidates.push_back(std::move(accepted)); + const SternheimerDeltaGridFunction& newest = orthonormal_candidates.back(); + for (std::size_t candidate_index = 0; candidate_index != residual_candidates.size(); ++candidate_index) + { + if (selected[candidate_index]) + { + continue; + } + for (int pass = 0; pass != 2; ++pass) + { + SternheimerDeltaGridFunction& candidate = residual_candidates[candidate_index]; + axpy_grid_function(-dot(newest.values, candidate.values), newest, candidate); + } + } + } + + SternheimerDeltaSubspace subspace; + subspace.accepted_candidates = static_cast(orthonormal_candidates.size()); + subspace.discarded_candidates + = input_candidate_count - subspace.accepted_candidates; + if (orthonormal_candidates.empty()) + { + return subspace; + } + + const SternheimerDeltaGridMatrices matrices + = assemble_delta_sternheimer_grid_matrices(hamiltonian, orthonormal_candidates, volume_element); + const int basis_size = static_cast(orthonormal_candidates.size()); + std::vector eigenvectors = matrices.hamiltonian; + const std::vector eigenvalues = diagonalize_complex_hermitian(eigenvectors, basis_size); + + subspace.grid_functions.reserve(static_cast(basis_size)); + subspace.virtual_states.reserve(static_cast(basis_size)); + for (int ia = 0; ia != basis_size; ++ia) + { + SternheimerDeltaGridFunction eigenfunction; + eigenfunction.values.assign(grid_size, Complex(0.0, 0.0)); + for (Vector& gradient: eigenfunction.gradients) + { + gradient.assign(grid_size, Complex(0.0, 0.0)); + } + for (int ib = 0; ib != basis_size; ++ib) + { + const Complex coefficient + = eigenvectors[static_cast(ib) + + static_cast(basis_size) * static_cast(ia)]; + axpy_grid_function(coefficient, + orthonormal_candidates[static_cast(ib)], + eigenfunction); + } + + SternheimerDeltaVirtualState state; + state.orbital = options.retain_grid_functions ? eigenfunction.values + : std::move(eigenfunction.values); + state.eigenvalue = eigenvalues[static_cast(ia)]; + if (options.retain_grid_functions) + { + subspace.grid_functions.push_back(std::move(eigenfunction)); + } + subspace.virtual_states.push_back(std::move(state)); + } + + if (options.evaluate_full_grid_difference) + { + evaluate_full_grid_delta_hamiltonian_difference(hamiltonian, volume_element, subspace); + } + else + { + subspace.full_grid_hamiltonian_relative_difference = -1.0; + subspace.full_grid_hamiltonian_max_abs_difference = -1.0; + } + + std::vector residual_projector; + residual_projector.reserve(occupied_functions.size() + subspace.virtual_states.size()); + for (const SternheimerDeltaGridFunction& occupied: occupied_functions) + { + residual_projector.push_back(occupied.values); + } + for (const SternheimerDeltaVirtualState& state: subspace.virtual_states) + { + residual_projector.push_back(state.orbital); + } + for (SternheimerDeltaVirtualState& state: subspace.virtual_states) + { + hamiltonian.apply(state.orbital, state.residual); + for (std::size_t ir = 0; ir != state.residual.size(); ++ir) + { + state.residual[ir] -= state.eigenvalue * state.orbital[ir]; + } + SternheimerRPA::project_out_subspace(residual_projector, dot, state.residual); + } + return subspace; +} + +SternheimerDeltaSubspace build_delta_sternheimer_subspace( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + std::vector candidate_orbitals, + const double volume_element, + const SternheimerDeltaSubspaceOptions& options) +{ + const int grid_size = hamiltonian.grid().size(); + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer delta subspace requires a positive grid volume element."); + } + if (options.max_virtual_states < 0) + { + throw std::invalid_argument("Sternheimer delta subspace max_virtual_states must be non-negative."); + } + if (options.norm_tolerance < 0.0) + { + throw std::invalid_argument("Sternheimer delta subspace norm_tolerance must be non-negative."); + } + for (const Vector& occupied: occupied_wavefunctions) + { + check_vector_size(occupied, static_cast(grid_size), "Sternheimer delta occupied state"); + } + + auto dot = [volume_element](const Vector& lhs, const Vector& rhs) { + return sternheimer_fd_grid_dot(lhs, rhs, volume_element); + }; + + std::vector orthonormal_candidates; + const int input_candidate_count = static_cast(candidate_orbitals.size()); + for (Vector& candidate: candidate_orbitals) + { + check_vector_size(candidate, static_cast(grid_size), "Sternheimer delta candidate orbital"); + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, candidate); + SternheimerRPA::project_out_subspace(orthonormal_candidates, dot, candidate); + const double norm = sternheimer_fd_grid_norm(candidate, volume_element); + if (norm <= options.norm_tolerance) + { + continue; + } + scale_vector(candidate, Complex(1.0 / norm, 0.0)); + orthonormal_candidates.push_back(std::move(candidate)); + if (options.max_virtual_states > 0 + && static_cast(orthonormal_candidates.size()) >= options.max_virtual_states) + { + break; + } + } + + SternheimerDeltaSubspace subspace; + subspace.accepted_candidates = static_cast(orthonormal_candidates.size()); + subspace.discarded_candidates + = input_candidate_count - subspace.accepted_candidates; + if (orthonormal_candidates.empty()) + { + return subspace; + } + + const int nvirtual = static_cast(orthonormal_candidates.size()); + std::vector h_candidates(nvirtual); + for (int j = 0; j != nvirtual; ++j) + { + hamiltonian.apply(orthonormal_candidates[j], h_candidates[j]); + } + + std::vector h_matrix(static_cast(nvirtual) * static_cast(nvirtual), + Complex(0.0, 0.0)); + for (int j = 0; j != nvirtual; ++j) + { + for (int i = 0; i != nvirtual; ++i) + { + h_matrix[static_cast(i) + static_cast(nvirtual) * static_cast(j)] + = dot(orthonormal_candidates[i], h_candidates[j]); + } + } + const std::vector eigenvalues = diagonalize_complex_hermitian(h_matrix, nvirtual); + + subspace.virtual_states.reserve(static_cast(nvirtual)); + for (int ia = 0; ia != nvirtual; ++ia) + { + Vector eta(static_cast(grid_size), Complex(0.0, 0.0)); + for (int j = 0; j != nvirtual; ++j) + { + const Complex coefficient + = h_matrix[static_cast(j) + static_cast(nvirtual) * static_cast(ia)]; + axpy(coefficient, orthonormal_candidates[j], eta); + } + + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, eta); + const double eta_norm = sternheimer_fd_grid_norm(eta, volume_element); + if (eta_norm <= options.norm_tolerance) + { + ++subspace.discarded_candidates; + continue; + } + scale_vector(eta, Complex(1.0 / eta_norm, 0.0)); + + Vector residual; + hamiltonian.apply(eta, residual); + for (std::size_t ir = 0; ir != residual.size(); ++ir) + { + residual[ir] -= eigenvalues[static_cast(ia)] * eta[ir]; + } + + SternheimerDeltaVirtualState state; + state.orbital = std::move(eta); + state.eigenvalue = eigenvalues[static_cast(ia)]; + subspace.virtual_states.push_back(std::move(state)); + } + + if (options.evaluate_full_grid_difference) + { + evaluate_full_grid_delta_hamiltonian_difference(hamiltonian, volume_element, subspace); + } + else + { + subspace.full_grid_hamiltonian_relative_difference = -1.0; + subspace.full_grid_hamiltonian_max_abs_difference = -1.0; + } + + const std::vector virtual_orbitals = collect_virtual_orbitals(subspace.virtual_states); + std::vector residual_projector = occupied_wavefunctions; + residual_projector.insert(residual_projector.end(), virtual_orbitals.begin(), virtual_orbitals.end()); + for (SternheimerDeltaVirtualState& state: subspace.virtual_states) + { + hamiltonian.apply(state.orbital, state.residual); + for (std::size_t ir = 0; ir != state.residual.size(); ++ir) + { + state.residual[ir] -= state.eigenvalue * state.orbital[ir]; + } + SternheimerRPA::project_out_subspace(residual_projector, dot, state.residual); + } + return subspace; +} + +SternheimerDeltaSubspace build_delta_sternheimer_subspace_by_mode( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_functions, + std::vector candidate_functions, + const double volume_element, + const SternheimerDeltaSubspaceOptions& options, + const SternheimerDeltaABlockMode mode) +{ + if (mode == SternheimerDeltaABlockMode::ReferenceValueGradient) + { + return build_reference_delta_sternheimer_subspace( + hamiltonian, occupied_functions, std::move(candidate_functions), volume_element, options); + } + if (mode == SternheimerDeltaABlockMode::FullGrid) + { + std::vector occupied_values; + occupied_values.reserve(occupied_functions.size()); + for (const SternheimerDeltaGridFunction& function: occupied_functions) + { + occupied_values.push_back(function.values); + } + std::vector candidate_values; + candidate_values.reserve(candidate_functions.size()); + for (SternheimerDeltaGridFunction& function: candidate_functions) + { + candidate_values.push_back(std::move(function.values)); + } + return build_delta_sternheimer_subspace( + hamiltonian, occupied_values, std::move(candidate_values), volume_element, options); + } + throw std::invalid_argument("Invalid Sternheimer Delta A-block mode."); +} + +std::vector delta_sternheimer_perturbation_matrix_elements( + const std::vector& virtual_states, + const std::vector& perturbation_potential, + const SternheimerFDHamiltonian::Vector& occupied_wavefunction, + const double volume_element) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer delta perturbation elements require a positive grid volume element."); + } + if (perturbation_potential.size() != occupied_wavefunction.size()) + { + throw std::invalid_argument("Sternheimer delta perturbation potential size does not match wavefunction."); + } + validate_virtual_states(virtual_states, occupied_wavefunction.size()); + + std::vector elements(virtual_states.size(), Complex(0.0, 0.0)); +#pragma omp parallel for schedule(static) + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + Complex value(0.0, 0.0); + for (std::size_t ir = 0; ir != occupied_wavefunction.size(); ++ir) + { + value += std::conj(virtual_states[ia].orbital[ir]) * perturbation_potential[ir] + * occupied_wavefunction[ir]; + } + elements[ia] = volume_element * value; + } + return elements; +} + +std::vector delta_sternheimer_perturbation_matrix_elements( + const std::vector& virtual_states, + const SternheimerFDHamiltonian::Vector& perturbation_potential, + const SternheimerFDHamiltonian::Vector& occupied_wavefunction, + const double volume_element) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer delta perturbation elements require a positive grid volume element."); + } + if (perturbation_potential.size() != occupied_wavefunction.size()) + { + throw std::invalid_argument("Sternheimer delta perturbation potential size does not match wavefunction."); + } + validate_virtual_states(virtual_states, occupied_wavefunction.size()); + + std::vector elements(virtual_states.size(), Complex(0.0, 0.0)); +#pragma omp parallel for schedule(static) + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + Complex value(0.0, 0.0); + for (std::size_t ir = 0; ir != occupied_wavefunction.size(); ++ir) + { + value += std::conj(virtual_states[ia].orbital[ir]) * perturbation_potential[ir] + * occupied_wavefunction[ir]; + } + elements[ia] = volume_element * value; + } + return elements; +} + +SternheimerFDHamiltonian::Vector build_delta_sternheimer_sos_wavefunction( + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + const double occupied_eigenvalue, + const double omega) +{ + if (virtual_states.empty()) + { + throw std::invalid_argument("Sternheimer direct SOS requires at least one virtual state."); + } + if (virtual_states.size() != perturbation_matrix_elements.size()) + { + throw std::invalid_argument( + "Sternheimer direct SOS requires one perturbation matrix element per virtual state."); + } + + const std::size_t grid_size = virtual_states.front().orbital.size(); + if (grid_size == 0) + { + throw std::invalid_argument("Sternheimer direct SOS virtual states must be non-empty."); + } + Vector response(grid_size, Complex(0.0, 0.0)); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + check_vector_size(virtual_states[ia].orbital, grid_size, "Sternheimer direct SOS virtual state"); + const Complex denominator(occupied_eigenvalue - virtual_states[ia].eigenvalue, -omega); + if (std::abs(denominator) < 1.0e-30) + { + throw std::runtime_error("Sternheimer direct SOS found a singular virtual-state denominator."); + } + axpy(perturbation_matrix_elements[ia] / denominator, virtual_states[ia].orbital, response); + } + return response; +} + +SternheimerDeltaFixedSubspace build_delta_sternheimer_fixed_subspace( + const std::vector& occupied_wavefunctions, + const std::vector& virtual_states) +{ + SternheimerDeltaFixedSubspace fixed_subspace; + fixed_subspace.functions = collect_fixed_subspace(occupied_wavefunctions, virtual_states); + return fixed_subspace; +} + +SternheimerDeltaLinearResponse solve_delta_sternheimer_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerDeltaFixedSubspace& fixed_subspace, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options) +{ + for (const Vector& function: fixed_subspace.functions) + { + check_vector_size(function, static_cast(hamiltonian.grid().size()), + "Sternheimer delta fixed-subspace state"); + } + const SternheimerSubspaceProjector projector(fixed_subspace.functions, volume_element); + return solve_delta_sternheimer_linear_response(hamiltonian, projector, reference_eigenvalue, + rhs, virtual_states, perturbation_matrix_elements, omega, volume_element, options); +} + +SternheimerDeltaLinearResponse solve_delta_sternheimer_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerSubspaceProjector& fixed_projector, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options) +{ + const int grid_size = hamiltonian.grid().size(); + check_vector_size(rhs, static_cast(grid_size), "Sternheimer delta rhs"); + if (virtual_states.size() != perturbation_matrix_elements.size()) + { + throw std::invalid_argument( + "Sternheimer delta linear response requires one perturbation matrix element per virtual state."); + } + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer delta linear response requires a positive grid volume element."); + } + validate_virtual_states(virtual_states, static_cast(grid_size)); + + auto dot = [volume_element](const Vector& lhs, const Vector& rhs_vec) { + return sternheimer_fd_grid_dot(lhs, rhs_vec, volume_element); + }; + + std::vector denominators(virtual_states.size(), Complex(0.0, 0.0)); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + denominators[ia] = Complex(reference_eigenvalue - virtual_states[ia].eigenvalue, -omega); + if (std::abs(denominators[ia]) < 1.0e-30) + { + throw std::runtime_error("Sternheimer delta linear response found a singular virtual-state denominator."); + } + } + + Vector projected_rhs = rhs; + fixed_projector.project(projected_rhs); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + axpy(-perturbation_matrix_elements[ia] / denominators[ia], virtual_states[ia].residual, projected_rhs); + } + + SternheimerRPA::LinearProblem problem; + problem.dot = dot; + problem.apply = [&hamiltonian, &fixed_projector, &virtual_states, &denominators, reference_eigenvalue, omega, dot]( + const Vector& input, + Vector& output) { + Vector q_input = input; + fixed_projector.project(q_input); + + hamiltonian.apply(q_input, output); + const Complex shift(-reference_eigenvalue, omega); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != output.size(); ++ir) + { + output[ir] += shift * q_input[ir]; + } + fixed_projector.project(output); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + const Complex coupling = dot(virtual_states[ia].residual, q_input) / denominators[ia]; + axpy(coupling, virtual_states[ia].residual, output); + } + }; + std::shared_ptr spectral_preconditioner; + if (options.use_fd_spectral_preconditioner) + { + spectral_preconditioner = get_thread_local_sternheimer_fd_spectral_preconditioner( + hamiltonian, + reference_eigenvalue, + omega, + options.fd_spectral_preconditioner_regularization); + problem.precondition = [spectral_preconditioner, &fixed_projector]( + const Vector& input, + Vector& output) { + spectral_preconditioner->apply(input, output); + fixed_projector.project(output); + }; + } + + SternheimerDeltaLinearResponse result; + result.response.out_wavefunction.assign(static_cast(grid_size), Complex(0.0, 0.0)); + if (fixed_projector.basis_size() >= static_cast(grid_size)) + { + const double projected_rhs_norm = sternheimer_fd_grid_norm(projected_rhs, volume_element); + const double rhs_norm = sternheimer_fd_grid_norm(rhs, volume_element); + const double zero_tolerance + = std::max(options.breakdown_tol, options.residual_tol) * std::max(1.0, rhs_norm); + if (projected_rhs_norm > zero_tolerance) + { + throw std::runtime_error( + "Sternheimer delta fixed subspace fills the grid but leaves a nonzero Q-space rhs."); + } + result.solver.converged = true; + result.solver.absolute_residual = projected_rhs_norm; + result.solver.relative_residual = projected_rhs_norm / std::max(1.0, rhs_norm); + } + else + { + result.solver + = SternheimerRPA::solve_gmres(problem, projected_rhs, result.response.out_wavefunction, options); + } + fixed_projector.project(result.response.out_wavefunction); + + const SternheimerDeltaCoefficientComponents components + = compute_delta_coefficient_components(virtual_states, + perturbation_matrix_elements, + result.response.out_wavefunction, + reference_eigenvalue, + omega, + volume_element); + result.response.sos_coefficients = components.sos; + result.response.pulay_coefficients = components.pulay; + result.response.coefficients = components.total; + assemble_delta_wavefunction_components(virtual_states, result.response); + result.response.out_norm = sternheimer_fd_grid_norm(result.response.out_wavefunction, volume_element); + + Vector q_residual; + hamiltonian.apply(result.response.out_wavefunction, q_residual); + const Complex shift(-reference_eigenvalue, omega); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != q_residual.size(); ++ir) + { + q_residual[ir] += shift * result.response.out_wavefunction[ir]; + } + fixed_projector.project(q_residual); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + axpy(result.response.coefficients[ia], virtual_states[ia].residual, q_residual); + } + + Vector q_rhs = rhs; + fixed_projector.project(q_rhs); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != q_residual.size(); ++ir) + { + q_residual[ir] -= q_rhs[ir]; + } + const double q_residual_norm = sternheimer_fd_grid_norm(q_residual, volume_element); + double residual_norm_squared = q_residual_norm * q_residual_norm; + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + const Complex delta_block(virtual_states[ia].eigenvalue - reference_eigenvalue, omega); + const Complex eta_residual = delta_block * result.response.coefficients[ia] + + dot(virtual_states[ia].residual, result.response.out_wavefunction) + + perturbation_matrix_elements[ia]; + residual_norm_squared += std::norm(eta_residual); + } + result.residual_norm = std::sqrt(residual_norm_squared); + result.response.reconstruction_error = result.residual_norm; + return result; +} + +std::vector solve_delta_sternheimer_linear_response_batch( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerDeltaFixedSubspace& fixed_subspace, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Matrix& rhs, + const std::vector& virtual_states, + const std::vector>& perturbation_matrix_elements, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options) +{ + if (rhs.empty()) + { + return {}; + } + for (const Vector& function: fixed_subspace.functions) + { + check_vector_size(function, static_cast(hamiltonian.grid().size()), + "Sternheimer delta batch fixed-subspace state"); + } + const SternheimerSubspaceProjector projector(fixed_subspace.functions, volume_element, true); + return solve_delta_sternheimer_linear_response_batch(hamiltonian, projector, reference_eigenvalue, + rhs, virtual_states, perturbation_matrix_elements, omega, volume_element, options); +} + +std::vector solve_delta_sternheimer_linear_response_batch( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerSubspaceProjector& fixed_projector, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Matrix& rhs, + const std::vector& virtual_states, + const std::vector>& perturbation_matrix_elements, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options) +{ + using Matrix = SternheimerFDHamiltonian::Matrix; + if (rhs.empty()) + { + return {}; + } + if (rhs.size() != perturbation_matrix_elements.size()) + { + throw std::invalid_argument( + "Sternheimer delta batch response requires one perturbation-element vector per rhs."); + } + const int grid_size = hamiltonian.grid().size(); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + check_vector_size(rhs[column], static_cast(grid_size), "Sternheimer delta batch rhs"); + if (virtual_states.size() != perturbation_matrix_elements[column].size()) + { + throw std::invalid_argument( + "Sternheimer delta batch response requires one perturbation matrix element per virtual state."); + } + } + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer delta batch response requires a positive grid volume element."); + } + validate_virtual_states(virtual_states, static_cast(grid_size)); + + const auto dot = [volume_element](const Vector& lhs, const Vector& rhs_vec) { + return sternheimer_fd_grid_dot(lhs, rhs_vec, volume_element); + }; + + std::vector denominators(virtual_states.size(), Complex(0.0, 0.0)); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + denominators[ia] = Complex(reference_eigenvalue - virtual_states[ia].eigenvalue, -omega); + if (std::abs(denominators[ia]) < 1.0e-30) + { + throw std::runtime_error( + "Sternheimer delta batch response found a singular virtual-state denominator."); + } + } + + Matrix projected_rhs = rhs; + fixed_projector.project_batch(projected_rhs); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + for (std::size_t column = 0; column != rhs.size(); ++column) + { + axpy(-perturbation_matrix_elements[column][ia] / denominators[ia], + virtual_states[ia].residual, + projected_rhs[column]); + } + } + + SternheimerRPA::BatchLinearProblem problem; + problem.dot = dot; + problem.apply = [&hamiltonian, + &fixed_projector, + &virtual_states, + &denominators, + reference_eigenvalue, + omega, + dot](const Matrix& input, Matrix& output) { + Matrix q_input = input; + fixed_projector.project_batch(q_input); + hamiltonian.apply_batch(q_input, output); + const Complex shift(-reference_eigenvalue, omega); + for (std::size_t column = 0; column != output.size(); ++column) + { +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != output[column].size(); ++ir) + { + output[column][ir] += shift * q_input[column][ir]; + } + } + fixed_projector.project_batch(output); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + for (std::size_t column = 0; column != output.size(); ++column) + { + const Complex coupling = dot(virtual_states[ia].residual, q_input[column]) / denominators[ia]; + axpy(coupling, virtual_states[ia].residual, output[column]); + } + } + }; + std::shared_ptr spectral_preconditioner; + if (options.use_fd_spectral_preconditioner) + { + spectral_preconditioner = get_thread_local_sternheimer_fd_spectral_preconditioner( + hamiltonian, + reference_eigenvalue, + omega, + options.fd_spectral_preconditioner_regularization); + problem.precondition = [spectral_preconditioner, &fixed_projector](const Matrix& input, Matrix& output) { + spectral_preconditioner->apply_batch(input, output); + fixed_projector.project_batch(output); + }; + } + + std::vector results(rhs.size()); + Matrix out_wavefunctions(rhs.size(), Vector(static_cast(grid_size), Complex(0.0, 0.0))); + if (fixed_projector.basis_size() >= static_cast(grid_size)) + { + for (std::size_t column = 0; column != rhs.size(); ++column) + { + const double projected_rhs_norm = sternheimer_fd_grid_norm(projected_rhs[column], volume_element); + const double rhs_norm = sternheimer_fd_grid_norm(rhs[column], volume_element); + const double zero_tolerance + = std::max(options.breakdown_tol, options.residual_tol) * std::max(1.0, rhs_norm); + if (projected_rhs_norm > zero_tolerance) + { + throw std::runtime_error( + "Sternheimer delta batch fixed subspace fills the grid but leaves a nonzero Q-space rhs."); + } + results[column].solver.converged = true; + results[column].solver.absolute_residual = projected_rhs_norm; + results[column].solver.relative_residual = projected_rhs_norm / std::max(1.0, rhs_norm); + } + } + else + { + const std::vector solver_results + = SternheimerRPA::solve_gmres_batch(problem, projected_rhs, out_wavefunctions, options); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + results[column].solver = solver_results[column]; + } + } + fixed_projector.project_batch(out_wavefunctions); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + results[column].response.out_wavefunction = out_wavefunctions[column]; + const SternheimerDeltaCoefficientComponents components + = compute_delta_coefficient_components(virtual_states, + perturbation_matrix_elements[column], + results[column].response.out_wavefunction, + reference_eigenvalue, + omega, + volume_element); + results[column].response.sos_coefficients = components.sos; + results[column].response.pulay_coefficients = components.pulay; + results[column].response.coefficients = components.total; + assemble_delta_wavefunction_components(virtual_states, results[column].response); + results[column].response.out_norm + = sternheimer_fd_grid_norm(results[column].response.out_wavefunction, volume_element); + } + + Matrix q_residual; + hamiltonian.apply_batch(out_wavefunctions, q_residual); + const Complex shift(-reference_eigenvalue, omega); + for (std::size_t column = 0; column != q_residual.size(); ++column) + { +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != q_residual[column].size(); ++ir) + { + q_residual[column][ir] += shift * out_wavefunctions[column][ir]; + } + } + fixed_projector.project_batch(q_residual); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + for (std::size_t column = 0; column != q_residual.size(); ++column) + { + axpy(results[column].response.coefficients[ia], virtual_states[ia].residual, q_residual[column]); + } + } + + Matrix q_rhs = rhs; + fixed_projector.project_batch(q_rhs); + for (std::size_t column = 0; column != rhs.size(); ++column) + { +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != q_residual[column].size(); ++ir) + { + q_residual[column][ir] -= q_rhs[column][ir]; + } + const double q_residual_norm = sternheimer_fd_grid_norm(q_residual[column], volume_element); + double residual_norm_squared = q_residual_norm * q_residual_norm; + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + const Complex delta_block(virtual_states[ia].eigenvalue - reference_eigenvalue, omega); + const Complex eta_residual = delta_block * results[column].response.coefficients[ia] + + dot(virtual_states[ia].residual, results[column].response.out_wavefunction) + + perturbation_matrix_elements[column][ia]; + residual_norm_squared += std::norm(eta_residual); + } + results[column].residual_norm = std::sqrt(residual_norm_squared); + results[column].response.reconstruction_error = results[column].residual_norm; + } + return results; +} + +SternheimerDeltaLinearResponse solve_delta_sternheimer_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options) +{ + const SternheimerDeltaFixedSubspace fixed_subspace + = build_delta_sternheimer_fixed_subspace(occupied_wavefunctions, virtual_states); + return solve_delta_sternheimer_linear_response(hamiltonian, + fixed_subspace, + reference_eigenvalue, + rhs, + virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + options); +} + +SternheimerFDHamiltonian::Complex accumulate_delta_sternheimer_response( + const SternheimerFDHamiltonian::Vector& probe_wavefunction, + const SternheimerDeltaPostprocessResult& response, + const std::vector& virtual_states, + const double volume_element) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer delta response requires a positive grid volume element."); + } + if (response.coefficients.size() != virtual_states.size()) + { + throw std::invalid_argument("Sternheimer delta response coefficient count does not match virtual states."); + } + check_vector_size(response.out_wavefunction, probe_wavefunction.size(), "Sternheimer delta response out state"); + + Complex value = sternheimer_fd_grid_dot(probe_wavefunction, response.out_wavefunction, volume_element); + for (std::size_t ia = 0; ia != virtual_states.size(); ++ia) + { + check_vector_size(virtual_states[ia].orbital, probe_wavefunction.size(), "Sternheimer delta response virtual"); + value += response.coefficients[ia] + * sternheimer_fd_grid_dot(probe_wavefunction, virtual_states[ia].orbital, volume_element); + } + return value; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_delta.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_delta.h new file mode 100644 index 00000000000..f30d570d786 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_delta.h @@ -0,0 +1,357 @@ +#ifndef STERNHEIMER_DELTA_H +#define STERNHEIMER_DELTA_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_kq.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" + +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +struct SternheimerDeltaVirtualState +{ + SternheimerFDHamiltonian::Vector orbital; + SternheimerFDHamiltonian::Vector residual; + double eigenvalue = 0.0; +}; + +struct SternheimerDeltaPostprocessInput +{ + std::vector occupied_wavefunctions; + std::vector virtual_states; + std::vector perturbation_matrix_elements; + double occupied_eigenvalue = 0.0; + double omega = 0.0; + double volume_element = 0.0; +}; + +struct SternheimerDeltaPostprocessResult +{ + SternheimerFDHamiltonian::Vector out_wavefunction; + std::vector sos_coefficients; + std::vector pulay_coefficients; + std::vector coefficients; + SternheimerFDHamiltonian::Vector in_sos_wavefunction; + SternheimerFDHamiltonian::Vector in_pulay_wavefunction; + SternheimerFDHamiltonian::Vector reconstructed_wavefunction; + double out_norm = 0.0; + double reconstruction_error = 0.0; +}; + +struct SternheimerDeltaCoefficientComponents +{ + std::vector sos; + std::vector pulay; + std::vector total; +}; + +struct SternheimerDeltaPulayOperatorComponents +{ + SternheimerFDHamiltonian::Vector kinetic; + SternheimerFDHamiltonian::Vector fixed_local; + SternheimerFDHamiltonian::Vector hxc_local; + SternheimerFDHamiltonian::Vector nonlocal; + SternheimerFDHamiltonian::Vector eigenvalue; + SternheimerFDHamiltonian::Vector total; +}; + +struct SternheimerDeltaSubspaceOptions +{ + int max_virtual_states = 0; + double norm_tolerance = 1.0e-10; + bool retain_grid_functions = true; + bool evaluate_full_grid_difference = true; + // Complete candidate spaces can use a generalized Hermitian solve and + // blocked grid contractions instead of pivoted vector-by-vector MGS. + bool use_block_generalized_eigensolver = true; +}; + +enum class SternheimerDeltaABlockMode +{ + ReferenceValueGradient, + FullGrid +}; + +SternheimerDeltaABlockMode parse_sternheimer_delta_a_block_mode(const std::string& name); +const char* sternheimer_delta_a_block_mode_name(SternheimerDeltaABlockMode mode); + +int sternheimer_delta_virtual_state_limit(int requested_states, + int candidate_states, + int occupied_states); + +struct SternheimerDeltaGridFunction +{ + SternheimerFDHamiltonian::Vector values; + std::array gradients; +}; + +struct SternheimerDeltaGridMatrices +{ + // LAPACK column-major storage: element (row, column) is row + row_count * column. + std::vector overlap; + std::vector kinetic; + std::vector local_potential; + std::vector nonlocal; + std::vector hamiltonian; + std::size_t row_count = 0; + std::size_t column_count = 0; +}; + +// Add one interleaved AO image sample to grid functions. Function values and +// all analytic gradients receive the same ABACUS Bloch phase. +void accumulate_delta_sternheimer_bloch_samples( + const std::vector& sampled_values, + const std::array, 3>& sampled_gradients, + int sample_count, + int orbital_count, + std::size_t grid_begin, + std::size_t function_begin, + const SternheimerReducedKPoint& kpoint, + const std::array& lattice_translation, + std::vector& functions); + +// Contract sampled AO chunks directly into selected LCAO states. This avoids +// retaining one full value-and-gradient grid function per AO. +void accumulate_delta_sternheimer_lcao_state_samples( + const std::vector& sampled_values, + const std::array, 3>& sampled_gradients, + int sample_count, + int orbital_count, + std::size_t grid_begin, + std::size_t ao_begin, + const std::vector>& coefficients, + const SternheimerReducedKPoint& kpoint, + const std::array& lattice_translation, + std::vector& functions); + +SternheimerDeltaGridFunction make_delta_sternheimer_grid_function_with_fd_gradients( + const SternheimerFDHamiltonian::Vector& values, + const SternheimerFDHamiltonian::Grid& grid); + +// Form one LCAO KS state on the uniform grid. The same AO coefficients are +// applied to function values and every analytic-gradient component. +SternheimerDeltaGridFunction linear_combination_delta_sternheimer_grid_functions( + const std::vector& basis_functions, + const std::vector& coefficients); + +// Build an orthonormal grid-metric basis spanning the same functions. Values +// and analytic gradients are transformed with identical MGS coefficients. +std::vector orthonormalize_delta_sternheimer_grid_functions( + const std::vector& functions, + double volume_element, + double norm_tolerance); + +// Enumerate Gamma-point periodic copies whose cutoff spheres intersect the +// primary orthogonal cell. Nonperiodic grids return only the zero image. +std::vector> enumerate_delta_sternheimer_periodic_images( + const SternheimerFDHamiltonian::Grid& grid, + const std::array& atom_position, + double cutoff_radius); + +struct SternheimerDeltaSubspace +{ + std::vector virtual_states; + std::vector grid_functions; + int accepted_candidates = 0; + int discarded_candidates = 0; + bool used_block_generalized_eigensolver = false; + double full_grid_hamiltonian_relative_difference = 0.0; + double full_grid_hamiltonian_max_abs_difference = 0.0; +}; + +struct SternheimerDeltaLinearResponse +{ + SternheimerDeltaPostprocessResult response; + SternheimerRPA::SolverResult solver; + double residual_norm = 0.0; +}; + +struct SternheimerDeltaFixedSubspace +{ + std::vector functions; +}; + +SternheimerDeltaPostprocessResult postprocess_delta_sternheimer_solution( + const SternheimerFDHamiltonian::Vector& standard_delta_wavefunction, + const SternheimerDeltaPostprocessInput& input); + +// Split the Pulay first-order wavefunction into the operator terms that form +// Q_out (H_grid - epsilon_a) eta_a. This is a diagnostic decomposition; the +// sum of the five operator terms must reproduce total. +SternheimerDeltaPulayOperatorComponents decompose_delta_sternheimer_pulay_operator_terms( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& fixed_local_potential, + const std::vector& occupied_wavefunctions, + const std::vector& virtual_states, + const SternheimerFDHamiltonian::Vector& out_wavefunction, + double occupied_eigenvalue, + double omega, + double volume_element); + +// Matrices use LAPACK column-major storage. This solves +// (H_delta - shift S_delta) c = rhs_delta - h_out by two shared-LU right-hand sides. +SternheimerDeltaCoefficientComponents solve_delta_sternheimer_subspace_coefficients( + const std::vector& hamiltonian_matrix, + const std::vector& overlap_matrix, + const std::vector& rhs, + const std::vector& hamiltonian_out_coupling, + SternheimerFDHamiltonian::Complex shift); + +// Reject ill-conditioned sampled Gram matrices and states outside the AO span. +std::vector> recover_delta_grid_ao_coefficients( + const std::vector& basis, + const std::vector& states, + double volume_element, + double reconstruction_tolerance, + double& maximum_relative_error, + double* gram_reciprocal_condition = nullptr); + +SternheimerDeltaSubspace build_delta_sternheimer_subspace( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + std::vector candidate_orbitals, + double volume_element, + const SternheimerDeltaSubspaceOptions& options = SternheimerDeltaSubspaceOptions()); + +// Assemble the reference-code Delta matrices from function values and analytic +// gradients. The finite-difference kinetic operator is deliberately not used. +SternheimerDeltaGridMatrices assemble_delta_sternheimer_grid_matrices( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& basis_functions, + double volume_element); + +// Same reference integrals, using the production subspace's blocked BLAS path. +SternheimerDeltaGridMatrices assemble_delta_sternheimer_grid_matrices_fast( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& basis_functions, + double volume_element); + +// Rectangular weak forms on the same fine grid/Hamiltonian. +// Gradients are caller-supplied analytic Cartesian derivatives of the sampled +// functions, including their Bloch phase, not fractional or periodic-part derivatives. +// T uses hamiltonian.kinetic_prefactor() (1.0 for Ry); no FD kinetic is applied. +// No projection, normalization, or Hermitian cleanup is performed, even for +// equal-sized bases. Either basis may be empty; dimensions are retained. +// Invalid dimensions/nonfinite inputs throw invalid_argument; unrepresentable +// sizes or nonfinite arithmetic results throw overflow_error. +SternheimerDeltaGridMatrices assemble_delta_sternheimer_cross_matrices_fast( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& left_functions, + const std::vector& right_functions, + double volume_element); + +// Reference-code path: project values and gradients with identical coefficients, +// apply two-pass modified Gram-Schmidt, then diagonalize the independently +// assembled Hermitian Delta Hamiltonian. +SternheimerDeltaSubspace build_reference_delta_sternheimer_subspace( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_functions, + std::vector candidate_functions, + double volume_element, + const SternheimerDeltaSubspaceOptions& options = SternheimerDeltaSubspaceOptions()); + +SternheimerDeltaSubspace build_delta_sternheimer_subspace_by_mode( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_functions, + std::vector candidate_functions, + double volume_element, + const SternheimerDeltaSubspaceOptions& options, + SternheimerDeltaABlockMode mode); + +std::vector delta_sternheimer_perturbation_matrix_elements( + const std::vector& virtual_states, + const std::vector& perturbation_potential, + const SternheimerFDHamiltonian::Vector& occupied_wavefunction, + double volume_element); + +std::vector delta_sternheimer_perturbation_matrix_elements( + const std::vector& virtual_states, + const SternheimerFDHamiltonian::Vector& perturbation_potential, + const SternheimerFDHamiltonian::Vector& occupied_wavefunction, + double volume_element); + +SternheimerDeltaFixedSubspace build_delta_sternheimer_fixed_subspace( + const std::vector& occupied_wavefunctions, + const std::vector& virtual_states); + +// Assemble the positive-frequency SOS branch from explicit orthonormal +// virtual states. Energies and omega must use the same units. +SternheimerFDHamiltonian::Vector build_delta_sternheimer_sos_wavefunction( + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + double occupied_eigenvalue, + double omega); + +// All overloads use the supplied perturbation elements for the AO-block rhs (-g). +// Only Q*rhs is taken from the grid. The reconstructed residual checks this hybrid equation. +SternheimerDeltaLinearResponse solve_delta_sternheimer_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerDeltaFixedSubspace& fixed_subspace, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions()); + +std::vector solve_delta_sternheimer_linear_response_batch( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerDeltaFixedSubspace& fixed_subspace, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Matrix& rhs, + const std::vector& virtual_states, + const std::vector>& perturbation_matrix_elements, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions()); + +// Reuse an immutable projector across channel workers. Its basis must outlive the solve. +SternheimerDeltaLinearResponse solve_delta_sternheimer_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerSubspaceProjector& fixed_projector, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions()); + +std::vector solve_delta_sternheimer_linear_response_batch( + const SternheimerFDHamiltonian& hamiltonian, + const SternheimerSubspaceProjector& fixed_projector, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Matrix& rhs, + const std::vector& virtual_states, + const std::vector>& perturbation_matrix_elements, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions()); + +SternheimerDeltaLinearResponse solve_delta_sternheimer_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions()); + +SternheimerFDHamiltonian::Complex accumulate_delta_sternheimer_response( + const SternheimerFDHamiltonian::Vector& probe_wavefunction, + const SternheimerDeltaPostprocessResult& response, + const std::vector& virtual_states, + double volume_element); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.cpp new file mode 100644 index 00000000000..c949a750483 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.cpp @@ -0,0 +1,51 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.h" + +#include +#include + +namespace ModuleRI +{ + +SternheimerDFTZeroOrderComparison compare_sternheimer_fd_zero_order_to_dft( + const SternheimerFDZeroOrderStates& fd_states, + const std::vector& dft_eigenvalues, + const double eigenvalue_tolerance) +{ + if (eigenvalue_tolerance < 0.0) + { + throw std::invalid_argument("Sternheimer DFT zero-order comparison requires a non-negative tolerance."); + } + + if (fd_states.eigenvalues.size() != fd_states.residual_norms.size()) + { + throw std::invalid_argument("Sternheimer DFT zero-order comparison has inconsistent FD state sizes."); + } + + if (fd_states.eigenvalues.size() != dft_eigenvalues.size()) + { + throw std::invalid_argument("Sternheimer DFT zero-order comparison requires matching band counts."); + } + + SternheimerDFTZeroOrderComparison comparison; + comparison.bands.reserve(fd_states.eigenvalues.size()); + + for (std::size_t ib = 0; ib != fd_states.eigenvalues.size(); ++ib) + { + SternheimerDFTZeroOrderBandComparison band; + band.band_index = static_cast(ib); + band.fd_eigenvalue = fd_states.eigenvalues[ib]; + band.dft_eigenvalue = dft_eigenvalues[ib]; + band.fd_minus_dft = band.fd_eigenvalue - band.dft_eigenvalue; + band.fd_residual_norm = fd_states.residual_norms[ib]; + band.eigenvalue_within_tolerance = std::abs(band.fd_minus_dft) <= eigenvalue_tolerance; + + comparison.max_abs_fd_minus_dft = std::max(comparison.max_abs_fd_minus_dft, std::abs(band.fd_minus_dft)); + comparison.all_eigenvalues_within_tolerance + = comparison.all_eigenvalues_within_tolerance && band.eigenvalue_within_tolerance; + comparison.bands.push_back(band); + } + + return comparison; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.h new file mode 100644 index 00000000000..0e0fcd22166 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.h @@ -0,0 +1,35 @@ +#ifndef STERNHEIMER_DFT_ZERO_ORDER_H +#define STERNHEIMER_DFT_ZERO_ORDER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h" + +#include + +namespace ModuleRI +{ + +struct SternheimerDFTZeroOrderBandComparison +{ + int band_index = 0; + double fd_eigenvalue = 0.0; + double dft_eigenvalue = 0.0; + double fd_minus_dft = 0.0; + double fd_residual_norm = 0.0; + bool eigenvalue_within_tolerance = false; +}; + +struct SternheimerDFTZeroOrderComparison +{ + std::vector bands; + double max_abs_fd_minus_dft = 0.0; + bool all_eigenvalues_within_tolerance = true; +}; + +SternheimerDFTZeroOrderComparison compare_sternheimer_fd_zero_order_to_dft( + const SternheimerFDZeroOrderStates& fd_states, + const std::vector& dft_eigenvalues, + double eigenvalue_tolerance); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.cpp new file mode 100644 index 00000000000..ab92054c26a --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.cpp @@ -0,0 +1,766 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include "source_base/module_external/lapack_connector.h" + +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace ModuleRI +{ + +int SternheimerFDHamiltonian::Grid::size() const +{ + return nx * ny * nz; +} + +SternheimerFDHamiltonian::SternheimerFDHamiltonian( + Grid grid, + std::vector local_potential, + const double kinetic_prefactor, + std::shared_ptr nonlocal_projector, + const int finite_difference_order) + : grid_(grid), local_potential_(std::move(local_potential)), kinetic_prefactor_(kinetic_prefactor), + finite_difference_order_(finite_difference_order), nonlocal_projector_(std::move(nonlocal_projector)) +{ + if (grid_.nx <= 0 || grid_.ny <= 0 || grid_.nz <= 0) + { + throw std::invalid_argument("SternheimerFDHamiltonian requires positive grid dimensions."); + } + if (grid_.hx <= 0.0 || grid_.hy <= 0.0 || grid_.hz <= 0.0) + { + throw std::invalid_argument("SternheimerFDHamiltonian requires positive grid spacings."); + } + if (static_cast(local_potential_.size()) != grid_.size()) + { + throw std::invalid_argument("SternheimerFDHamiltonian local potential size does not match the grid."); + } + if (kinetic_prefactor_ < 0.0) + { + throw std::invalid_argument("SternheimerFDHamiltonian requires a non-negative kinetic prefactor."); + } + if (finite_difference_order_ != 2 && finite_difference_order_ != 4 && finite_difference_order_ != 6 + && finite_difference_order_ != 8) + { + throw std::invalid_argument("SternheimerFDHamiltonian finite-difference order must be 2, 4, 6, or 8."); + } + bool has_nonzero_twist = false; + for (const double coordinate: grid_.kpoint) + { + if (!std::isfinite(coordinate)) + { + throw std::invalid_argument("SternheimerFDHamiltonian requires finite reduced k-point coordinates."); + } + has_nonzero_twist = has_nonzero_twist || coordinate != 0.0; + } + if (!grid_.periodic && has_nonzero_twist) + { + throw std::invalid_argument("SternheimerFDHamiltonian nonperiodic grids cannot use a Bloch twist."); + } + if (nonlocal_projector_ != nullptr && nonlocal_projector_->grid_size() != grid_.size()) + { + throw std::invalid_argument("SternheimerFDHamiltonian nonlocal projector size does not match the grid."); + } + initialize_operator_cache(); +} + +void SternheimerFDHamiltonian::initialize_operator_cache() +{ + const SternheimerFDStencilWeights weights = sternheimer_fd_stencil_weights(finite_difference_order_); + fd_radius_ = weights.radius; + fd_second_weights_ = weights.second; + fd_first_weights_ = weights.first; + + const SternheimerFDLatticeVectors dual = sternheimer_fd_grid_dual_vectors(grid_); + const std::array dimensions{static_cast(grid_.nx), + static_cast(grid_.ny), + static_cast(grid_.nz)}; + for (int left = 0; left != 3; ++left) + { + for (int right = 0; right != 3; ++right) + { + for (int component = 0; component != 3; ++component) + { + laplacian_coefficients_[left][right] + += dual[left][component] * dual[right][component] * dimensions[left] * dimensions[right]; + } + } + } + laplacian_center_coefficient_ + = fd_second_weights_[0] + * (laplacian_coefficients_[0][0] + laplacian_coefficients_[1][1] + + laplacian_coefficients_[2][2]); + + for (int left = 0; left != 3; ++left) + { + for (int right = left + 1; right != 3; ++right) + { + if (laplacian_coefficients_[left][right] == 0.0) + { + continue; + } + active_mixed_derivative_pairs_[static_cast(active_mixed_derivative_pair_count_)] + = {left, right}; + ++active_mixed_derivative_pair_count_; + if (grid_.periodic && fd_radius_ > 1) + { + cached_first_derivative_directions_[right] = true; + } + } + } + + for (const double coordinate: grid_.kpoint) + { + has_zero_bloch_twist_ = has_zero_bloch_twist_ && coordinate == 0.0; + } +} + +const SternheimerFDHamiltonian::Grid& SternheimerFDHamiltonian::grid() const +{ + return grid_; +} + +const std::vector& SternheimerFDHamiltonian::local_potential() const +{ + return local_potential_; +} + +double SternheimerFDHamiltonian::kinetic_prefactor() const +{ + return kinetic_prefactor_; +} + +int SternheimerFDHamiltonian::finite_difference_order() const +{ + return finite_difference_order_; +} + +const SternheimerReducedKPoint& SternheimerFDHamiltonian::kpoint() const +{ + return grid_.kpoint; +} + +const SternheimerFDNonlocalProjector* SternheimerFDHamiltonian::nonlocal_projector() const +{ + return nonlocal_projector_.get(); +} + +int SternheimerFDHamiltonian::active_mixed_derivative_pair_count() const +{ + return active_mixed_derivative_pair_count_; +} + +const std::array& SternheimerFDHamiltonian::cached_first_derivative_directions() const +{ + return cached_first_derivative_directions_; +} + +int SternheimerFDHamiltonian::index(const int ix, const int iy, const int iz) const +{ + return (ix * grid_.ny + iy) * grid_.nz + iz; +} + +SternheimerFDHamiltonian::ShiftedGridPoint SternheimerFDHamiltonian::shifted_grid_point(int ix, int iy, int iz) const +{ + if (grid_.periodic) + { + if (ix >= 0 && ix < grid_.nx && iy >= 0 && iy < grid_.ny && iz >= 0 && iz < grid_.nz) + { + return {index(ix, iy, iz), Complex(1.0, 0.0)}; + } + const std::array dimensions{grid_.nx, grid_.ny, grid_.nz}; + std::array coordinates{ix, iy, iz}; + std::array lattice_translation{}; + for (std::size_t direction = 0; direction != coordinates.size(); ++direction) + { + const int dimension = dimensions[direction]; + const int wrapped = (coordinates[direction] % dimension + dimension) % dimension; + lattice_translation[direction] = (coordinates[direction] - wrapped) / dimension; + coordinates[direction] = wrapped; + } + const Complex phase = has_zero_bloch_twist_ ? Complex(1.0, 0.0) + : sternheimer_bloch_phase(grid_.kpoint, lattice_translation); + return {index(coordinates[0], coordinates[1], coordinates[2]), phase}; + } + + if (ix < 0 || ix >= grid_.nx || iy < 0 || iy >= grid_.ny || iz < 0 || iz >= grid_.nz) + { + return {-1, Complex(1.0, 0.0)}; + } + return {index(ix, iy, iz), Complex(1.0, 0.0)}; +} + +void SternheimerFDHamiltonian::apply(const Vector& psi, Vector& hpsi) const +{ + apply(psi, hpsi, nullptr); +} + +void SternheimerFDHamiltonian::apply(const Vector& psi, Vector& hpsi, int* threads_used) const +{ + if (static_cast(psi.size()) != grid_.size()) + { + throw std::invalid_argument("SternheimerFDHamiltonian::apply input size does not match the grid."); + } + + apply_grid_terms(psi, hpsi, true, threads_used); + if (nonlocal_projector_ != nullptr) + { + nonlocal_projector_->add_to(psi, hpsi); + } +} + +void SternheimerFDHamiltonian::apply_batch(const Matrix& psi, Matrix& hpsi) const +{ + apply_batch(psi, hpsi, nullptr); +} + +void SternheimerFDHamiltonian::apply_batch(const Matrix& psi, Matrix& hpsi, int* threads_used) const +{ + apply_grid_terms_batch(psi, hpsi, true, threads_used); + if (nonlocal_projector_ != nullptr && !psi.empty()) + { + nonlocal_projector_->add_to_batch(psi, hpsi); + } +} + +void SternheimerFDHamiltonian::apply_kinetic(const Vector& psi, Vector& kinetic_psi) const +{ + apply_kinetic(psi, kinetic_psi, nullptr); +} + +void SternheimerFDHamiltonian::apply_kinetic(const Vector& psi, Vector& kinetic_psi, int* threads_used) const +{ + apply_grid_terms(psi, kinetic_psi, false, threads_used); +} + +void SternheimerFDHamiltonian::apply_grid_terms(const Vector& psi, + Vector& output, + const bool include_local_potential, + int* threads_used) const +{ + if (static_cast(psi.size()) != grid_.size()) + { + throw std::invalid_argument("SternheimerFDHamiltonian::apply_kinetic input size does not match the grid."); + } + + output.assign(psi.size(), Complex(0.0, 0.0)); + const bool use_separable_mixed_derivatives = grid_.periodic && fd_radius_ > 1; + std::array first_derivatives; + if (use_separable_mixed_derivatives) + { + for (int direction = 0; direction != 3; ++direction) + { + if (!cached_first_derivative_directions_[direction]) + { + continue; + } + Vector& derivative = first_derivatives[direction]; + derivative.assign(psi.size(), Complex(0.0, 0.0)); +#ifdef _OPENMP +#pragma omp parallel for collapse(2) schedule(static) +#endif + for (int ix = 0; ix != grid_.nx; ++ix) + { + for (int iy = 0; iy != grid_.ny; ++iy) + { + for (int iz = 0; iz != grid_.nz; ++iz) + { + Complex value(0.0, 0.0); + for (int offset = 1; offset <= fd_radius_; ++offset) + { + std::array shift{}; + shift[direction] = offset; + const ShiftedGridPoint positive + = shifted_grid_point(ix + shift[0], iy + shift[1], iz + shift[2]); + const ShiftedGridPoint negative + = shifted_grid_point(ix - shift[0], iy - shift[1], iz - shift[2]); + const double coefficient = fd_first_weights_[static_cast(offset - 1)]; + if (positive.index >= 0) + { + value += coefficient * positive.phase * psi[positive.index]; + } + if (negative.index >= 0) + { + value -= coefficient * negative.phase * psi[negative.index]; + } + } + derivative[static_cast(index(ix, iy, iz))] = value; + } + } + } + } + } + +#ifdef _OPENMP +#pragma omp parallel + { +#pragma omp single + { + if (threads_used != nullptr) + { + *threads_used = omp_get_num_threads(); + } + } + +#pragma omp for collapse(2) schedule(static) +#endif + for (int ix = 0; ix != grid_.nx; ++ix) + { + for (int iy = 0; iy != grid_.ny; ++iy) + { + for (int iz = 0; iz != grid_.nz; ++iz) + { + const int center = index(ix, iy, iz); + const Complex psi_center = psi[center]; + + Complex laplacian = laplacian_center_coefficient_ * psi_center; + const auto add_shift = [&](const int dx, const int dy, const int dz, const double coefficient) { + const ShiftedGridPoint shifted = shifted_grid_point(ix + dx, iy + dy, iz + dz); + if (shifted.index >= 0) + { + laplacian += coefficient * shifted.phase * psi[shifted.index]; + } + }; + for (int direction = 0; direction != 3; ++direction) + { + for (int offset = 1; offset <= fd_radius_; ++offset) + { + std::array shift{}; + shift[direction] = offset; + const double coefficient = fd_second_weights_[static_cast(offset)] + * laplacian_coefficients_[direction][direction]; + add_shift(shift[0], shift[1], shift[2], coefficient); + add_shift(-shift[0], -shift[1], -shift[2], coefficient); + } + } + + for (int pair_index = 0; pair_index != active_mixed_derivative_pair_count_; ++pair_index) + { + const int left = active_mixed_derivative_pairs_[static_cast(pair_index)][0]; + const int right = active_mixed_derivative_pairs_[static_cast(pair_index)][1]; + if (use_separable_mixed_derivatives) + { + Complex mixed_derivative(0.0, 0.0); + for (int offset = 1; offset <= fd_radius_; ++offset) + { + std::array shift{}; + shift[left] = offset; + const ShiftedGridPoint positive + = shifted_grid_point(ix + shift[0], iy + shift[1], iz + shift[2]); + const ShiftedGridPoint negative + = shifted_grid_point(ix - shift[0], iy - shift[1], iz - shift[2]); + const double coefficient = fd_first_weights_[static_cast(offset - 1)]; + if (positive.index >= 0) + { + mixed_derivative + += coefficient * positive.phase * first_derivatives[right][positive.index]; + } + if (negative.index >= 0) + { + mixed_derivative + -= coefficient * negative.phase * first_derivatives[right][negative.index]; + } + } + laplacian += 2.0 * laplacian_coefficients_[left][right] * mixed_derivative; + } + else + { + for (int left_offset = 1; left_offset <= fd_radius_; ++left_offset) + { + for (int right_offset = 1; right_offset <= fd_radius_; ++right_offset) + { + const double coefficient + = 2.0 * laplacian_coefficients_[left][right] + * fd_first_weights_[static_cast(left_offset - 1)] + * fd_first_weights_[static_cast(right_offset - 1)]; + for (const int left_sign: {-1, 1}) + { + for (const int right_sign: {-1, 1}) + { + std::array shift{}; + shift[left] = left_sign * left_offset; + shift[right] = right_sign * right_offset; + add_shift(shift[0], + shift[1], + shift[2], + coefficient * left_sign * right_sign); + } + } + } + } + } + } + + output[center] = -kinetic_prefactor_ * laplacian; + if (include_local_potential) + { + output[center] += local_potential_[center] * psi_center; + } + } + } + } +#ifdef _OPENMP + } +#else + if (threads_used != nullptr) + { + *threads_used = 1; + } +#endif +} + +void SternheimerFDHamiltonian::apply_grid_terms_batch(const Matrix& psi, + Matrix& output, + const bool include_local_potential, + int* threads_used) const +{ + if (psi.empty()) + { + output.clear(); + if (threads_used != nullptr) + { + *threads_used = 1; + } + return; + } + for (const Vector& column: psi) + { + if (static_cast(column.size()) != grid_.size()) + { + throw std::invalid_argument( + "SternheimerFDHamiltonian::apply_grid_terms_batch input size does not match the grid."); + } + } + + const std::size_t batch_size = psi.size(); + const std::size_t grid_size = static_cast(grid_.size()); + output.assign(batch_size, Vector(grid_size, Complex(0.0, 0.0))); + const bool use_separable_mixed_derivatives = grid_.periodic && fd_radius_ > 1; + std::array first_derivatives; + if (use_separable_mixed_derivatives) + { + for (int direction = 0; direction != 3; ++direction) + { + if (!cached_first_derivative_directions_[direction]) + { + continue; + } + Matrix& derivatives = first_derivatives[direction]; + derivatives.assign(batch_size, Vector(grid_size, Complex(0.0, 0.0))); +#ifdef _OPENMP +#pragma omp parallel +#endif + { + std::vector values(batch_size, Complex(0.0, 0.0)); +#ifdef _OPENMP +#pragma omp for collapse(2) schedule(static) +#endif + for (int ix = 0; ix != grid_.nx; ++ix) + { + for (int iy = 0; iy != grid_.ny; ++iy) + { + for (int iz = 0; iz != grid_.nz; ++iz) + { + std::fill(values.begin(), values.end(), Complex(0.0, 0.0)); + for (int offset = 1; offset <= fd_radius_; ++offset) + { + std::array shift{}; + shift[direction] = offset; + const ShiftedGridPoint positive + = shifted_grid_point(ix + shift[0], iy + shift[1], iz + shift[2]); + const ShiftedGridPoint negative + = shifted_grid_point(ix - shift[0], iy - shift[1], iz - shift[2]); + const double coefficient + = fd_first_weights_[static_cast(offset - 1)]; + for (std::size_t column = 0; column != batch_size; ++column) + { + if (positive.index >= 0) + { + values[column] + += coefficient * positive.phase * psi[column][positive.index]; + } + if (negative.index >= 0) + { + values[column] + -= coefficient * negative.phase * psi[column][negative.index]; + } + } + } + const std::size_t center = static_cast(index(ix, iy, iz)); + for (std::size_t column = 0; column != batch_size; ++column) + { + derivatives[column][center] = values[column]; + } + } + } + } + } + } + } + +#ifdef _OPENMP +#pragma omp parallel +#endif + { + std::vector laplacians(batch_size, Complex(0.0, 0.0)); + std::vector mixed_derivatives(batch_size, Complex(0.0, 0.0)); +#ifdef _OPENMP +#pragma omp single + { + if (threads_used != nullptr) + { + *threads_used = omp_get_num_threads(); + } + } +#pragma omp for collapse(2) schedule(static) +#endif + for (int ix = 0; ix != grid_.nx; ++ix) + { + for (int iy = 0; iy != grid_.ny; ++iy) + { + for (int iz = 0; iz != grid_.nz; ++iz) + { + const int center = index(ix, iy, iz); + for (std::size_t column = 0; column != batch_size; ++column) + { + laplacians[column] = laplacian_center_coefficient_ * psi[column][center]; + } + + const auto add_shift + = [&](const int dx, const int dy, const int dz, const double coefficient) { + const ShiftedGridPoint shifted = shifted_grid_point(ix + dx, iy + dy, iz + dz); + if (shifted.index >= 0) + { + for (std::size_t column = 0; column != batch_size; ++column) + { + laplacians[column] + += coefficient * shifted.phase * psi[column][shifted.index]; + } + } + }; + for (int direction = 0; direction != 3; ++direction) + { + for (int offset = 1; offset <= fd_radius_; ++offset) + { + std::array shift{}; + shift[direction] = offset; + const double coefficient = fd_second_weights_[static_cast(offset)] + * laplacian_coefficients_[direction][direction]; + add_shift(shift[0], shift[1], shift[2], coefficient); + add_shift(-shift[0], -shift[1], -shift[2], coefficient); + } + } + + for (int pair_index = 0; pair_index != active_mixed_derivative_pair_count_; ++pair_index) + { + const int left + = active_mixed_derivative_pairs_[static_cast(pair_index)][0]; + const int right + = active_mixed_derivative_pairs_[static_cast(pair_index)][1]; + if (use_separable_mixed_derivatives) + { + std::fill(mixed_derivatives.begin(), mixed_derivatives.end(), Complex(0.0, 0.0)); + for (int offset = 1; offset <= fd_radius_; ++offset) + { + std::array shift{}; + shift[left] = offset; + const ShiftedGridPoint positive + = shifted_grid_point(ix + shift[0], iy + shift[1], iz + shift[2]); + const ShiftedGridPoint negative + = shifted_grid_point(ix - shift[0], iy - shift[1], iz - shift[2]); + const double coefficient + = fd_first_weights_[static_cast(offset - 1)]; + for (std::size_t column = 0; column != batch_size; ++column) + { + if (positive.index >= 0) + { + mixed_derivatives[column] + += coefficient * positive.phase + * first_derivatives[right][column][positive.index]; + } + if (negative.index >= 0) + { + mixed_derivatives[column] + -= coefficient * negative.phase + * first_derivatives[right][column][negative.index]; + } + } + } + for (std::size_t column = 0; column != batch_size; ++column) + { + laplacians[column] += 2.0 * laplacian_coefficients_[left][right] + * mixed_derivatives[column]; + } + } + else + { + for (int left_offset = 1; left_offset <= fd_radius_; ++left_offset) + { + for (int right_offset = 1; right_offset <= fd_radius_; ++right_offset) + { + const double coefficient + = 2.0 * laplacian_coefficients_[left][right] + * fd_first_weights_[static_cast(left_offset - 1)] + * fd_first_weights_[static_cast(right_offset - 1)]; + for (const int left_sign: {-1, 1}) + { + for (const int right_sign: {-1, 1}) + { + std::array shift{}; + shift[left] = left_sign * left_offset; + shift[right] = right_sign * right_offset; + add_shift(shift[0], + shift[1], + shift[2], + coefficient * left_sign * right_sign); + } + } + } + } + } + } + + for (std::size_t column = 0; column != batch_size; ++column) + { + output[column][center] = -kinetic_prefactor_ * laplacians[column]; + if (include_local_potential) + { + output[column][center] += local_potential_[center] * psi[column][center]; + } + } + } + } + } + } +#ifndef _OPENMP + if (threads_used != nullptr) + { + *threads_used = 1; + } +#endif +} + +void SternheimerFDHamiltonian::apply_local_potential(const Vector& psi, Vector& local_psi) const +{ + if (static_cast(psi.size()) != grid_.size()) + { + throw std::invalid_argument( + "SternheimerFDHamiltonian::apply_local_potential input size does not match the grid."); + } + local_psi.resize(psi.size()); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != psi.size(); ++ir) + { + local_psi[ir] = local_potential_[ir] * psi[ir]; + } +} + +void SternheimerFDHamiltonian::apply_nonlocal(const Vector& psi, Vector& nonlocal_psi) const +{ + if (static_cast(psi.size()) != grid_.size()) + { + throw std::invalid_argument("SternheimerFDHamiltonian::apply_nonlocal input size does not match the grid."); + } + nonlocal_psi.assign(psi.size(), Complex(0.0, 0.0)); + if (nonlocal_projector_ != nullptr) + { + nonlocal_projector_->add_to(psi, nonlocal_psi); + } +} + +SternheimerFDHamiltonian::Matrix SternheimerFDHamiltonian::dense_matrix(const int max_size) const +{ + const int size = grid_.size(); + if (size > max_size) + { + throw std::invalid_argument("SternheimerFDHamiltonian::dense_matrix is only intended for small debug grids."); + } + + Matrix matrix(size, Vector(size, Complex(0.0, 0.0))); + Vector basis(size, Complex(0.0, 0.0)); + Vector h_basis; + for (int col = 0; col != size; ++col) + { + std::fill(basis.begin(), basis.end(), Complex(0.0, 0.0)); + basis[col] = Complex(1.0, 0.0); + apply(basis, h_basis); + for (int row = 0; row != size; ++row) + { + matrix[row][col] = h_basis[row]; + } + } + return matrix; +} + +SternheimerFDHamiltonian::Eigenpairs SternheimerFDHamiltonian::diagonalize_dense(const int max_size) const +{ + const int size = grid_.size(); + if (size > max_size) + { + throw std::invalid_argument( + "SternheimerFDHamiltonian::diagonalize_dense is only intended for small debug grids."); + } + + const Matrix matrix = dense_matrix(max_size); + std::vector lapack_matrix(size * size, Complex(0.0, 0.0)); + for (int col = 0; col != size; ++col) + { + for (int row = 0; row != size; ++row) + { + lapack_matrix[row + size * col] = matrix[row][col]; + } + } + + char jobz = 'V'; + char uplo = 'U'; + const int n = size; + const int lda = size; + const int minus_one = -1; + int info = 0; + std::vector eigenvalues(size, 0.0); + Complex work_query(0.0, 0.0); + std::vector rwork(std::max(1, 3 * n - 2), 0.0); + + zheev_(&jobz, + &uplo, + &n, + lapack_matrix.data(), + &lda, + eigenvalues.data(), + &work_query, + &minus_one, + rwork.data(), + &info); + if (info != 0) + { + throw std::runtime_error("SternheimerFDHamiltonian::diagonalize_dense LAPACK workspace query failed."); + } + + const int lwork = std::max(1, static_cast(std::ceil(work_query.real()))); + std::vector work(lwork, Complex(0.0, 0.0)); + zheev_(&jobz, &uplo, &n, lapack_matrix.data(), &lda, eigenvalues.data(), work.data(), &lwork, rwork.data(), &info); + if (info != 0) + { + throw std::runtime_error("SternheimerFDHamiltonian::diagonalize_dense LAPACK diagonalization failed."); + } + + Eigenpairs eigenpairs; + eigenpairs.eigenvalues = std::move(eigenvalues); + eigenpairs.eigenvectors.assign(size, Vector(size, Complex(0.0, 0.0))); + for (int band = 0; band != size; ++band) + { + for (int row = 0; row != size; ++row) + { + eigenpairs.eigenvectors[band][row] = lapack_matrix[row + size * band]; + } + } + return eigenpairs; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h new file mode 100644 index 00000000000..7b4786cba22 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h @@ -0,0 +1,433 @@ +#ifndef STERNHEIMER_FD_HAMILTONIAN_H +#define STERNHEIMER_FD_HAMILTONIAN_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_kq.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +class SternheimerFDHamiltonian +{ + public: + using Complex = std::complex; + using Vector = std::vector; + using Matrix = std::vector; + + struct Eigenpairs + { + std::vector eigenvalues; + std::vector eigenvectors; + }; + + struct Grid + { + int nx = 0; + int ny = 0; + int nz = 0; + double hx = 0.0; + double hy = 0.0; + double hz = 0.0; + bool periodic = true; + SternheimerReducedKPoint kpoint{0.0, 0.0, 0.0}; + std::array, 3> lattice_vectors{}; + + int size() const; + }; + + // kinetic_prefactor = 0.5 gives the Hartree convention -1/2 laplacian. + // ABACUS PW eigenvalues are in Ry, so ABACUS bindings pass 1.0. + SternheimerFDHamiltonian(Grid grid, + std::vector local_potential, + double kinetic_prefactor = 0.5, + std::shared_ptr nonlocal_projector = nullptr, + int finite_difference_order = 2); + + const Grid& grid() const; + const std::vector& local_potential() const; + double kinetic_prefactor() const; + int finite_difference_order() const; + const SternheimerReducedKPoint& kpoint() const; + const SternheimerFDNonlocalProjector* nonlocal_projector() const; + int active_mixed_derivative_pair_count() const; + const std::array& cached_first_derivative_directions() const; + + void apply(const Vector& psi, Vector& hpsi) const; + void apply(const Vector& psi, Vector& hpsi, int* threads_used) const; + void apply_batch(const Matrix& psi, Matrix& hpsi) const; + void apply_batch(const Matrix& psi, Matrix& hpsi, int* threads_used) const; + void apply_kinetic(const Vector& psi, Vector& kinetic_psi) const; + void apply_kinetic(const Vector& psi, Vector& kinetic_psi, int* threads_used) const; + void apply_local_potential(const Vector& psi, Vector& local_psi) const; + void apply_nonlocal(const Vector& psi, Vector& nonlocal_psi) const; + + Matrix dense_matrix(int max_size = 4096) const; + + // Diagonalizes the zero-order Hermitian FD Hamiltonian on small debug grids. + // The shifted Sternheimer operator H - eps + i omega is non-Hermitian and + // must be handled as a linear operator, not by this routine. + Eigenpairs diagonalize_dense(int max_size = 4096) const; + + int index(int ix, int iy, int iz) const; + + private: + struct ShiftedGridPoint + { + int index = -1; + Complex phase{1.0, 0.0}; + }; + + void initialize_operator_cache(); + ShiftedGridPoint shifted_grid_point(int ix, int iy, int iz) const; + void apply_grid_terms(const Vector& psi, Vector& output, bool include_local_potential, int* threads_used) const; + void apply_grid_terms_batch(const Matrix& psi, + Matrix& output, + bool include_local_potential, + int* threads_used) const; + + Grid grid_; + std::vector local_potential_; + double kinetic_prefactor_ = 0.5; + int finite_difference_order_ = 2; + std::shared_ptr nonlocal_projector_; + int fd_radius_ = 0; + std::array fd_second_weights_{}; + std::array fd_first_weights_{}; + std::array, 3> laplacian_coefficients_{}; + double laplacian_center_coefficient_ = 0.0; + std::array, 3> active_mixed_derivative_pairs_{}; + int active_mixed_derivative_pair_count_ = 0; + std::array cached_first_derivative_directions_{}; + bool has_zero_bloch_twist_ = true; +}; + +using SternheimerFDLatticeVectors = std::array, 3>; +using SternheimerFDReducedRotation = std::array, 3>; + +struct SternheimerFDStencilWeights +{ + int radius = 0; + std::array second{}; + std::array first{}; +}; + +inline SternheimerFDStencilWeights sternheimer_fd_stencil_weights(const int order) +{ + SternheimerFDStencilWeights weights; + weights.radius = order / 2; + switch (order) + { + case 2: + weights.second = {{-2.0, 1.0, 0.0, 0.0, 0.0}}; + weights.first = {{1.0 / 2.0, 0.0, 0.0, 0.0}}; + break; + case 4: + weights.second = {{-5.0 / 2.0, 4.0 / 3.0, -1.0 / 12.0, 0.0, 0.0}}; + weights.first = {{2.0 / 3.0, -1.0 / 12.0, 0.0, 0.0}}; + break; + case 6: + weights.second = {{-49.0 / 18.0, 3.0 / 2.0, -3.0 / 20.0, 1.0 / 90.0, 0.0}}; + weights.first = {{3.0 / 4.0, -3.0 / 20.0, 1.0 / 60.0, 0.0}}; + break; + case 8: + weights.second = {{-205.0 / 72.0, 8.0 / 5.0, -1.0 / 5.0, 8.0 / 315.0, -1.0 / 560.0}}; + weights.first = {{4.0 / 5.0, -1.0 / 5.0, 4.0 / 105.0, -1.0 / 280.0}}; + break; + default: + throw std::invalid_argument("Unsupported Sternheimer finite-difference order."); + } + return weights; +} + +inline bool sternheimer_fd_grid_rotation_is_permutation( + const SternheimerFDHamiltonian::Grid& grid, + const SternheimerFDReducedRotation& rotation, + const double tolerance = 1.0e-10) +{ + if (grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0 || tolerance <= 0.0) + { + throw std::invalid_argument("Invalid grid or tolerance for Sternheimer FD grid symmetry."); + } + const std::array dimensions{grid.nx, grid.ny, grid.nz}; + std::vector seen(static_cast(grid.size()), false); + const auto flatten = [&dimensions](const std::array& point) { + return point[0] + dimensions[0] * (point[1] + dimensions[1] * point[2]); + }; + for (int iz = 0; iz != grid.nz; ++iz) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int ix = 0; ix != grid.nx; ++ix) + { + const std::array source{ix, iy, iz}; + std::array mapped{}; + for (int target = 0; target != 3; ++target) + { + double mapped_coordinate = 0.0; + for (int source_direction = 0; source_direction != 3; ++source_direction) + { + mapped_coordinate += static_cast(source[source_direction]) + / static_cast(dimensions[source_direction]) + * rotation[source_direction][target] + * static_cast(dimensions[target]); + } + const int rounded = static_cast(std::llround(mapped_coordinate)); + if (std::abs(mapped_coordinate - static_cast(rounded)) > tolerance) + { + return false; + } + const int dimension = dimensions[target]; + mapped[target] = ((rounded % dimension) + dimension) % dimension; + } + const int mapped_index = flatten(mapped); + if (mapped_index < 0 || mapped_index >= grid.size() + || seen[static_cast(mapped_index)]) + { + return false; + } + seen[static_cast(mapped_index)] = true; + } + } + } + return true; +} + +inline SternheimerFDLatticeVectors sternheimer_fd_grid_lattice_vectors(const SternheimerFDHamiltonian::Grid& grid) +{ + bool has_explicit_lattice = false; + for (const auto& vector: grid.lattice_vectors) + { + for (const double component: vector) + { + has_explicit_lattice = has_explicit_lattice || component != 0.0; + } + } + if (has_explicit_lattice) + { + return grid.lattice_vectors; + } + return {{{grid.nx * grid.hx, 0.0, 0.0}, {0.0, grid.ny * grid.hy, 0.0}, {0.0, 0.0, grid.nz * grid.hz}}}; +} + +inline SternheimerFDLatticeVectors sternheimer_fd_grid_dual_vectors(const SternheimerFDHamiltonian::Grid& grid) +{ + const SternheimerFDLatticeVectors lattice = sternheimer_fd_grid_lattice_vectors(grid); + const auto cross = [](const std::array& lhs, const std::array& rhs) { + return std::array{lhs[1] * rhs[2] - lhs[2] * rhs[1], + lhs[2] * rhs[0] - lhs[0] * rhs[2], + lhs[0] * rhs[1] - lhs[1] * rhs[0]}; + }; + const auto dot = [](const std::array& lhs, const std::array& rhs) { + return lhs[0] * rhs[0] + lhs[1] * rhs[1] + lhs[2] * rhs[2]; + }; + SternheimerFDLatticeVectors dual{ + {cross(lattice[1], lattice[2]), cross(lattice[2], lattice[0]), cross(lattice[0], lattice[1])}}; + const double determinant = dot(lattice[0], dual[0]); + for (auto& vector: dual) + { + for (double& component: vector) + { + component /= determinant; + } + } + return dual; +} + +inline std::vector sternheimer_fd_stencil_symmetry_indices( + const SternheimerFDHamiltonian::Grid& grid, + const int finite_difference_order, + const std::vector& rotations, + const double tolerance = 1.0e-10) +{ + if (grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0 || tolerance <= 0.0) + { + throw std::invalid_argument("Invalid grid or tolerance for Sternheimer FD stencil symmetry."); + } + + using Offset = std::array; + std::map stencil; + const SternheimerFDLatticeVectors dual = sternheimer_fd_grid_dual_vectors(grid); + const std::array dimensions{static_cast(grid.nx), + static_cast(grid.ny), + static_cast(grid.nz)}; + const SternheimerFDStencilWeights fd_weights + = sternheimer_fd_stencil_weights(finite_difference_order); + std::array, 3> coefficients{}; + for (int left = 0; left != 3; ++left) + { + for (int right = 0; right != 3; ++right) + { + for (int component = 0; component != 3; ++component) + { + coefficients[left][right] + += dual[left][component] * dual[right][component] * dimensions[left] * dimensions[right]; + } + } + } + const auto add = [&stencil](const Offset& offset, const double weight) { stencil[offset] += weight; }; + for (int direction = 0; direction != 3; ++direction) + { + for (int offset = 1; offset <= fd_weights.radius; ++offset) + { + Offset positive{0, 0, 0}; + Offset negative{0, 0, 0}; + positive[direction] = offset; + negative[direction] = -offset; + const double weight = fd_weights.second[static_cast(offset)] + * coefficients[direction][direction]; + add(positive, weight); + add(negative, weight); + } + } + for (int left = 0; left != 3; ++left) + { + for (int right = left + 1; right != 3; ++right) + { + for (int left_offset = 1; left_offset <= fd_weights.radius; ++left_offset) + { + for (int right_offset = 1; right_offset <= fd_weights.radius; ++right_offset) + { + const double weight = 2.0 * coefficients[left][right] + * fd_weights.first[static_cast(left_offset - 1)] + * fd_weights.first[static_cast(right_offset - 1)]; + for (const int left_sign: {-1, 1}) + { + for (const int right_sign: {-1, 1}) + { + Offset offset{0, 0, 0}; + offset[left] = left_sign * left_offset; + offset[right] = right_sign * right_offset; + add(offset, weight * static_cast(left_sign * right_sign)); + } + } + } + } + } + } + double weight_scale = 0.0; + for (const auto& entry: stencil) + { + const double weight = entry.second; + weight_scale = std::max(weight_scale, std::abs(weight)); + } + for (auto iter = stencil.begin(); iter != stencil.end();) + { + if (std::abs(iter->second) <= tolerance * std::max(1.0, weight_scale)) + { + iter = stencil.erase(iter); + } + else + { + ++iter; + } + } + + std::vector preserving; + for (std::size_t isym = 0; isym != rotations.size(); ++isym) + { + bool valid = true; + if (!sternheimer_fd_grid_rotation_is_permutation(grid, rotations[isym], tolerance)) + { + continue; + } + std::set mapped_offsets; + for (const auto& entry: stencil) + { + const Offset& offset = entry.first; + const double weight = entry.second; + Offset mapped{}; + for (int target = 0; target != 3; ++target) + { + double mapped_index = 0.0; + for (int source = 0; source != 3; ++source) + { + mapped_index += static_cast(offset[source]) / dimensions[source] + * rotations[isym][source][target] * dimensions[target]; + } + mapped[target] = static_cast(std::llround(mapped_index)); + if (std::abs(mapped_index - static_cast(mapped[target])) > tolerance) + { + valid = false; + break; + } + } + if (!valid) + { + break; + } + const auto mapped_iter = stencil.find(mapped); + if (mapped_iter == stencil.end() + || std::abs(mapped_iter->second - weight) + > tolerance + * std::max(std::max(1.0, weight_scale), + std::max(std::abs(weight), std::abs(mapped_iter->second))) + || !mapped_offsets.insert(mapped).second) + { + valid = false; + break; + } + } + if (valid && mapped_offsets.size() == stencil.size()) + { + preserving.push_back(static_cast(isym)); + } + } + return preserving; +} + +inline std::vector sternheimer_fd_second_order_stencil_symmetry_indices( + const SternheimerFDHamiltonian::Grid& grid, + const std::vector& rotations, + const double tolerance = 1.0e-10) +{ + return sternheimer_fd_stencil_symmetry_indices(grid, 2, rotations, tolerance); +} + +inline std::array sternheimer_fd_grid_cartesian_position(const SternheimerFDHamiltonian::Grid& grid, + const int ix, + const int iy, + const int iz) +{ + const SternheimerFDLatticeVectors lattice = sternheimer_fd_grid_lattice_vectors(grid); + const std::array reduced{static_cast(ix) / grid.nx, + static_cast(iy) / grid.ny, + static_cast(iz) / grid.nz}; + std::array cartesian{}; + for (int direction = 0; direction != 3; ++direction) + { + for (int component = 0; component != 3; ++component) + { + cartesian[component] += reduced[direction] * lattice[direction][component]; + } + } + return cartesian; +} + +inline std::array sternheimer_fd_grid_lattice_translation(const SternheimerFDHamiltonian::Grid& grid, + const std::array& translation) +{ + const SternheimerFDLatticeVectors lattice = sternheimer_fd_grid_lattice_vectors(grid); + std::array cartesian{}; + for (int direction = 0; direction != 3; ++direction) + { + for (int component = 0; component != 3; ++component) + { + cartesian[component] += static_cast(translation[direction]) * lattice[direction][component]; + } + } + return cartesian; +} + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.cpp new file mode 100644 index 00000000000..1dc27d325c8 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.cpp @@ -0,0 +1,246 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h" + +#include +#include + +namespace ModuleRI +{ + +SternheimerFDNonlocalProjector::SternheimerFDNonlocalProjector(const int grid_size, + const double volume_element, + std::vector blocks) + : grid_size_(grid_size), volume_element_(volume_element), blocks_(std::move(blocks)) +{ + if (grid_size_ <= 0) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector requires a positive grid size."); + } + if (volume_element_ <= 0.0) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector requires a positive volume element."); + } + for (const ProjectorBlock& block: blocks_) + { + validate_block(block); + SparseProjectorBlock sparse_block; + sparse_block.projectors.reserve(block.projectors.size()); + sparse_block.d_matrix = block.d_matrix; + for (const Vector& projector: block.projectors) + { + SparseProjector sparse_projector; + for (int ir = 0; ir != grid_size_; ++ir) + { + const Complex value = projector[static_cast(ir)]; + ++projector_value_count_; + if (value != Complex(0.0, 0.0)) + { + sparse_projector.indices.push_back(ir); + sparse_projector.values.push_back(value); + ++nonzero_projector_value_count_; + } + } + sparse_block.projectors.push_back(std::move(sparse_projector)); + } + sparse_blocks_.push_back(std::move(sparse_block)); + } +} + +int SternheimerFDNonlocalProjector::grid_size() const +{ + return grid_size_; +} + +double SternheimerFDNonlocalProjector::volume_element() const +{ + return volume_element_; +} + +const std::vector& SternheimerFDNonlocalProjector::blocks() const +{ + return blocks_; +} + +std::size_t SternheimerFDNonlocalProjector::projector_value_count() const +{ + return projector_value_count_; +} + +std::size_t SternheimerFDNonlocalProjector::nonzero_projector_value_count() const +{ + return nonzero_projector_value_count_; +} + +void SternheimerFDNonlocalProjector::validate_block(const ProjectorBlock& block) const +{ + const int num_projectors = static_cast(block.projectors.size()); + if (num_projectors <= 0) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector block requires at least one projector."); + } + if (static_cast(block.d_matrix.size()) != num_projectors) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector D matrix row count does not match projectors."); + } + + for (const Vector& projector: block.projectors) + { + if (static_cast(projector.size()) != grid_size_) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector projector size does not match the grid."); + } + } + + for (const Vector& row: block.d_matrix) + { + if (static_cast(row.size()) != num_projectors) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector D matrix must be square."); + } + } +} + +void SternheimerFDNonlocalProjector::apply(const Vector& psi, Vector& vpsi) const +{ + if (static_cast(psi.size()) != grid_size_) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector::apply input size does not match the grid."); + } + + vpsi.assign(grid_size_, Complex(0.0, 0.0)); + add_to(psi, vpsi); +} + +void SternheimerFDNonlocalProjector::add_to(const Vector& psi, Vector& hpsi) const +{ + if (static_cast(psi.size()) != grid_size_ || static_cast(hpsi.size()) != grid_size_) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector::add_to vector sizes do not match the grid."); + } + + for (const SparseProjectorBlock& block: sparse_blocks_) + { + const int num_projectors = static_cast(block.projectors.size()); + Vector coefficients(num_projectors, Complex(0.0, 0.0)); +#pragma omp parallel for schedule(static) + for (int ip = 0; ip != num_projectors; ++ip) + { + const SparseProjector& beta = block.projectors[static_cast(ip)]; + for (std::size_t entry = 0; entry != beta.indices.size(); ++entry) + { + coefficients[static_cast(ip)] += volume_element_ * std::conj(beta.values[entry]) + * psi[static_cast(beta.indices[entry])]; + } + } + + Vector weighted_coefficients(num_projectors, Complex(0.0, 0.0)); + for (int ip = 0; ip != num_projectors; ++ip) + { + for (int jp = 0; jp != num_projectors; ++jp) + { + weighted_coefficients[ip] += block.d_matrix[ip][jp] * coefficients[jp]; + } + } + + for (int ip = 0; ip != num_projectors; ++ip) + { + const SparseProjector& beta = block.projectors[static_cast(ip)]; + const Complex coefficient = weighted_coefficients[static_cast(ip)]; + for (std::size_t entry = 0; entry != beta.indices.size(); ++entry) + { + hpsi[static_cast(beta.indices[entry])] += beta.values[entry] * coefficient; + } + } + } +} + +void SternheimerFDNonlocalProjector::apply_batch(const Matrix& psi, Matrix& vpsi) const +{ + if (psi.empty()) + { + vpsi.clear(); + return; + } + for (const Vector& column: psi) + { + if (static_cast(column.size()) != grid_size_) + { + throw std::invalid_argument( + "SternheimerFDNonlocalProjector::apply_batch input size does not match the grid."); + } + } + + vpsi.assign(psi.size(), Vector(static_cast(grid_size_), Complex(0.0, 0.0))); + add_to_batch(psi, vpsi); +} + +void SternheimerFDNonlocalProjector::add_to_batch(const Matrix& psi, Matrix& hpsi) const +{ + if (psi.size() != hpsi.size()) + { + throw std::invalid_argument("SternheimerFDNonlocalProjector::add_to_batch column counts do not match."); + } + for (std::size_t column = 0; column != psi.size(); ++column) + { + if (static_cast(psi[column].size()) != grid_size_ || static_cast(hpsi[column].size()) != grid_size_) + { + throw std::invalid_argument( + "SternheimerFDNonlocalProjector::add_to_batch vector sizes do not match the grid."); + } + } + + const std::size_t batch_size = psi.size(); + for (const SparseProjectorBlock& block: sparse_blocks_) + { + const int num_projectors = static_cast(block.projectors.size()); + Matrix coefficients(batch_size, Vector(static_cast(num_projectors), Complex(0.0, 0.0))); +#pragma omp parallel for collapse(2) schedule(static) + for (int ip = 0; ip != num_projectors; ++ip) + { + for (int column = 0; column != static_cast(batch_size); ++column) + { + const SparseProjector& beta = block.projectors[static_cast(ip)]; + Complex coefficient(0.0, 0.0); + for (std::size_t entry = 0; entry != beta.indices.size(); ++entry) + { + coefficient + += volume_element_ * std::conj(beta.values[entry]) + * psi[static_cast(column)][static_cast(beta.indices[entry])]; + } + coefficients[static_cast(column)][static_cast(ip)] = coefficient; + } + } + + Matrix weighted_coefficients(batch_size, Vector(static_cast(num_projectors), Complex(0.0, 0.0))); +#pragma omp parallel for schedule(static) + for (int column = 0; column != static_cast(batch_size); ++column) + { + for (int ip = 0; ip != num_projectors; ++ip) + { + for (int jp = 0; jp != num_projectors; ++jp) + { + weighted_coefficients[static_cast(column)][static_cast(ip)] + += block.d_matrix[static_cast(ip)][static_cast(jp)] + * coefficients[static_cast(column)][static_cast(jp)]; + } + } + } + +#pragma omp parallel for schedule(static) + for (int column = 0; column != static_cast(batch_size); ++column) + { + for (int ip = 0; ip != num_projectors; ++ip) + { + const SparseProjector& beta = block.projectors[static_cast(ip)]; + const Complex coefficient + = weighted_coefficients[static_cast(column)][static_cast(ip)]; + for (std::size_t entry = 0; entry != beta.indices.size(); ++entry) + { + hpsi[static_cast(column)][static_cast(beta.indices[entry])] + += beta.values[entry] * coefficient; + } + } + } + } +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h new file mode 100644 index 00000000000..65d24e66b06 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h @@ -0,0 +1,62 @@ +#ifndef STERNHEIMER_FD_NONLOCAL_PROJECTOR_H +#define STERNHEIMER_FD_NONLOCAL_PROJECTOR_H + +#include +#include +#include + +namespace ModuleRI +{ + +class SternheimerFDNonlocalProjector +{ + public: + using Complex = std::complex; + using Vector = std::vector; + using Matrix = std::vector; + + struct ProjectorBlock + { + std::vector projectors; + Matrix d_matrix; + }; + + SternheimerFDNonlocalProjector(int grid_size, double volume_element, std::vector blocks); + + int grid_size() const; + double volume_element() const; + const std::vector& blocks() const; + std::size_t projector_value_count() const; + std::size_t nonzero_projector_value_count() const; + + void apply(const Vector& psi, Vector& vpsi) const; + void add_to(const Vector& psi, Vector& hpsi) const; + void apply_batch(const Matrix& psi, Matrix& vpsi) const; + void add_to_batch(const Matrix& psi, Matrix& hpsi) const; + + private: + struct SparseProjector + { + std::vector indices; + Vector values; + }; + + struct SparseProjectorBlock + { + std::vector projectors; + Matrix d_matrix; + }; + + void validate_block(const ProjectorBlock& block) const; + + int grid_size_ = 0; + double volume_element_ = 0.0; + std::vector blocks_; + std::vector sparse_blocks_; + std::size_t projector_value_count_ = 0; + std::size_t nonzero_projector_value_count_ = 0; +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.cpp new file mode 100644 index 00000000000..dbb309d7ea0 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.cpp @@ -0,0 +1,466 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.h" + +#include "source_base/constants.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace +{ + +struct FiniteDifferenceWeights +{ + int radius = 0; + std::array second{}; + std::array first{}; +}; + +FiniteDifferenceWeights finite_difference_weights(const int order) +{ + FiniteDifferenceWeights weights; + weights.radius = order / 2; + switch (order) + { + case 2: + weights.second = {{-2.0, 1.0, 0.0, 0.0, 0.0}}; + weights.first = {{1.0 / 2.0, 0.0, 0.0, 0.0}}; + break; + case 4: + weights.second = {{-5.0 / 2.0, 4.0 / 3.0, -1.0 / 12.0, 0.0, 0.0}}; + weights.first = {{2.0 / 3.0, -1.0 / 12.0, 0.0, 0.0}}; + break; + case 6: + weights.second = {{-49.0 / 18.0, 3.0 / 2.0, -3.0 / 20.0, 1.0 / 90.0, 0.0}}; + weights.first = {{3.0 / 4.0, -3.0 / 20.0, 1.0 / 60.0, 0.0}}; + break; + case 8: + weights.second = {{-205.0 / 72.0, 8.0 / 5.0, -1.0 / 5.0, 8.0 / 315.0, -1.0 / 560.0}}; + weights.first = {{4.0 / 5.0, -1.0 / 5.0, 4.0 / 105.0, -1.0 / 280.0}}; + break; + default: + throw std::logic_error("Unsupported Sternheimer finite-difference order."); + } + return weights; +} + +int signed_fft_index(const int index, const int dimension) +{ + return index <= dimension / 2 ? index : index - dimension; +} + +struct KineticSymbolData +{ + std::array, 3> laplacian_coefficients{}; + std::array, 3> first_symbols; + std::array, 3> second_symbols; +}; + +KineticSymbolData make_kinetic_symbol_data(const ModuleRI::SternheimerFDHamiltonian& hamiltonian) +{ + const auto& grid = hamiltonian.grid(); + const FiniteDifferenceWeights weights = finite_difference_weights(hamiltonian.finite_difference_order()); + const ModuleRI::SternheimerFDLatticeVectors dual = ModuleRI::sternheimer_fd_grid_dual_vectors(grid); + const std::array dimensions{static_cast(grid.nx), + static_cast(grid.ny), + static_cast(grid.nz)}; + KineticSymbolData data; + for (int left = 0; left != 3; ++left) + { + for (int right = 0; right != 3; ++right) + { + for (int component = 0; component != 3; ++component) + { + data.laplacian_coefficients[left][right] + += dual[left][component] * dual[right][component] + * dimensions[left] * dimensions[right]; + } + } + } + + const std::array grid_dimensions{grid.nx, grid.ny, grid.nz}; + for (int direction = 0; direction != 3; ++direction) + { + data.first_symbols[direction].resize(static_cast(grid_dimensions[direction])); + data.second_symbols[direction].resize(static_cast(grid_dimensions[direction])); + for (int index = 0; index != grid_dimensions[direction]; ++index) + { + const double reduced_mode = static_cast(signed_fft_index(index, grid_dimensions[direction])) + + (grid.periodic ? grid.kpoint[direction] : 0.0); + const double angle = ModuleBase::TWO_PI * reduced_mode / grid_dimensions[direction]; + double first_symbol = 0.0; + double second_symbol = weights.second[0]; + for (int offset = 1; offset <= weights.radius; ++offset) + { + second_symbol += 2.0 * weights.second[static_cast(offset)] + * std::cos(offset * angle); + first_symbol += 2.0 * weights.first[static_cast(offset - 1)] + * std::sin(offset * angle); + } + data.first_symbols[direction][static_cast(index)] = first_symbol; + data.second_symbols[direction][static_cast(index)] = second_symbol; + } + } + return data; +} + +double kinetic_symbol(const KineticSymbolData& data, + const ModuleRI::SternheimerFDHamiltonian& hamiltonian, + const int ix, + const int iy, + const int iz) +{ + const std::array indices{ix, iy, iz}; + + double laplacian_symbol = 0.0; + for (int direction = 0; direction != 3; ++direction) + { + laplacian_symbol + += data.laplacian_coefficients[direction][direction] + * data.second_symbols[direction][static_cast(indices[direction])]; + } + for (int left = 0; left != 3; ++left) + { + for (int right = left + 1; right != 3; ++right) + { + laplacian_symbol -= 2.0 * data.laplacian_coefficients[left][right] + * data.first_symbols[left][static_cast(indices[left])] + * data.first_symbols[right][static_cast(indices[right])]; + } + } + return -hamiltonian.kinetic_prefactor() * laplacian_symbol; +} + +} // namespace + +namespace ModuleRI +{ + +struct SternheimerFDSpectralPreconditioner::Impl +{ + struct FFTContext + { + explicit FFTContext(const SternheimerFDHamiltonian::Grid& grid) + { + buffer = fftw_alloc_complex(static_cast(grid.size())); + if (buffer == nullptr) + { + throw std::bad_alloc(); + } +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + forward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, + buffer, buffer, FFTW_FORWARD, FFTW_ESTIMATE); + backward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, + buffer, buffer, FFTW_BACKWARD, FFTW_ESTIMATE); + } + if (forward == nullptr || backward == nullptr) + { + destroy(); + throw std::runtime_error("Failed to initialize the Sternheimer spectral preconditioner FFT."); + } + } + + ~FFTContext() + { + destroy(); + } + + FFTContext(const FFTContext&) = delete; + FFTContext& operator=(const FFTContext&) = delete; + + void destroy() + { +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + if (forward != nullptr) + { + fftw_destroy_plan(forward); + forward = nullptr; + } + if (backward != nullptr) + { + fftw_destroy_plan(backward); + backward = nullptr; + } + } + if (buffer != nullptr) + { + fftw_free(buffer); + buffer = nullptr; + } + } + + fftw_complex* buffer = nullptr; + fftw_plan forward = nullptr; + fftw_plan backward = nullptr; + }; + + explicit Impl(const SternheimerFDHamiltonian& hamiltonian, + const double reference_eigenvalue, + const double omega, + const double regularization) + : grid(hamiltonian.grid()), + kinetic_prefactor(hamiltonian.kinetic_prefactor()), + finite_difference_order(hamiltonian.finite_difference_order()), + reference_eigenvalue(reference_eigenvalue), + omega(omega), + regularization(regularization), + inverse_denominator(static_cast(grid.size())) + { + if (!std::isfinite(reference_eigenvalue) || !std::isfinite(omega) + || !std::isfinite(regularization) || regularization < 0.0) + { + throw std::invalid_argument("Invalid Sternheimer FD spectral preconditioner shift."); + } + scalar_context.reset(new FFTContext(grid)); + has_bloch_phase = grid.periodic + && (grid.kpoint[0] != 0.0 || grid.kpoint[1] != 0.0 || grid.kpoint[2] != 0.0); + if (has_bloch_phase) + { + bloch_phase.resize(static_cast(grid.size())); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const int index = (ix * grid.ny + iy) * grid.nz + iz; + const double phase_angle + = ModuleBase::TWO_PI + * (grid.kpoint[0] * static_cast(ix) / grid.nx + + grid.kpoint[1] * static_cast(iy) / grid.ny + + grid.kpoint[2] * static_cast(iz) / grid.nz); + bloch_phase[static_cast(index)] + = std::exp(std::complex(0.0, phase_angle)); + } + } + } + } + + const KineticSymbolData symbol_data = make_kinetic_symbol_data(hamiltonian); + const double normalization = 1.0 / static_cast(grid.size()); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const int index = (ix * grid.ny + iy) * grid.nz + iz; + const std::complex denominator( + kinetic_symbol(symbol_data, hamiltonian, ix, iy, iz) + - reference_eigenvalue + regularization, + omega); + if (std::abs(denominator) < 1.0e-14) + { + throw std::runtime_error("Sternheimer spectral preconditioner found a singular mode."); + } + inverse_denominator[static_cast(index)] = normalization / denominator; + } + } + } + } + + void ensure_batch_contexts(const int count) const + { + while (static_cast(batch_contexts.size()) < count) + { + batch_contexts.emplace_back(new FFTContext(grid)); + } + } + + void apply(const SternheimerFDHamiltonian::Vector& input, + SternheimerFDHamiltonian::Vector& output, + FFTContext& context) const + { + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const int index = (ix * grid.ny + iy) * grid.nz + iz; + std::complex value = input[static_cast(index)]; + if (has_bloch_phase) + { + value *= std::conj(bloch_phase[static_cast(index)]); + } + context.buffer[index][0] = value.real(); + context.buffer[index][1] = value.imag(); + } + } + } + fftw_execute(context.forward); + for (int index = 0; index != grid.size(); ++index) + { + const std::complex value(context.buffer[index][0], context.buffer[index][1]); + const std::complex preconditioned + = inverse_denominator[static_cast(index)] * value; + context.buffer[index][0] = preconditioned.real(); + context.buffer[index][1] = preconditioned.imag(); + } + fftw_execute(context.backward); + + output.resize(input.size()); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const int index = (ix * grid.ny + iy) * grid.nz + iz; + const std::complex value(context.buffer[index][0], context.buffer[index][1]); + output[static_cast(index)] + = has_bloch_phase + ? bloch_phase[static_cast(index)] * value + : value; + } + } + } + } + + SternheimerFDHamiltonian::Grid grid; + double kinetic_prefactor = 0.0; + int finite_difference_order = 0; + double reference_eigenvalue = 0.0; + double omega = 0.0; + double regularization = 0.0; + bool has_bloch_phase = false; + std::vector> bloch_phase; + std::unique_ptr scalar_context; + mutable std::vector> batch_contexts; + std::vector> inverse_denominator; +}; + +SternheimerFDSpectralPreconditioner::SternheimerFDSpectralPreconditioner( + const SternheimerFDHamiltonian& hamiltonian, + const double reference_eigenvalue, + const double omega, + const double regularization) + : impl_(new Impl(hamiltonian, reference_eigenvalue, omega, regularization)) +{ +} + +SternheimerFDSpectralPreconditioner::~SternheimerFDSpectralPreconditioner() = default; + +void SternheimerFDSpectralPreconditioner::apply(const SternheimerFDHamiltonian::Vector& input, + SternheimerFDHamiltonian::Vector& output) const +{ + if (input.size() != static_cast(impl_->grid.size())) + { + throw std::invalid_argument("Sternheimer spectral preconditioner input size does not match the grid."); + } + + impl_->apply(input, output, *impl_->scalar_context); +} + +void SternheimerFDSpectralPreconditioner::apply_batch(const SternheimerFDHamiltonian::Matrix& input, + SternheimerFDHamiltonian::Matrix& output) const +{ + apply_batch(input, output, nullptr); +} + +void SternheimerFDSpectralPreconditioner::apply_batch(const SternheimerFDHamiltonian::Matrix& input, + SternheimerFDHamiltonian::Matrix& output, + int* threads_used) const +{ + for (const auto& column: input) + { + if (column.size() != static_cast(impl_->grid.size())) + { + throw std::invalid_argument("Sternheimer spectral preconditioner input size does not match the grid."); + } + } + if (input.empty()) + { + output.clear(); + if (threads_used != nullptr) + { + *threads_used = 0; + } + return; + } + + int worker_count = 1; +#ifdef _OPENMP + worker_count = std::min(static_cast(input.size()), omp_get_max_threads()); +#endif + impl_->ensure_batch_contexts(worker_count); + output.resize(input.size()); +#ifdef _OPENMP +#pragma omp parallel num_threads(worker_count) + { +#pragma omp single + { + if (threads_used != nullptr) + { + *threads_used = omp_get_num_threads(); + } + } + const int worker = omp_get_thread_num(); +#pragma omp for schedule(static) + for (std::size_t column = 0; column < input.size(); ++column) + { + impl_->apply(input[column], output[column], *impl_->batch_contexts[static_cast(worker)]); + } + } +#else + if (threads_used != nullptr) + { + *threads_used = 1; + } + for (std::size_t column = 0; column != input.size(); ++column) + { + impl_->apply(input[column], output[column], *impl_->batch_contexts.front()); + } +#endif +} + +bool SternheimerFDSpectralPreconditioner::is_compatible( + const SternheimerFDHamiltonian& hamiltonian, + const double reference_eigenvalue, + const double omega, + const double regularization) const +{ + const auto& grid = hamiltonian.grid(); + return impl_->grid.nx == grid.nx && impl_->grid.ny == grid.ny && impl_->grid.nz == grid.nz + && impl_->grid.hx == grid.hx && impl_->grid.hy == grid.hy && impl_->grid.hz == grid.hz + && impl_->grid.periodic == grid.periodic && impl_->grid.kpoint == grid.kpoint + && impl_->grid.lattice_vectors == grid.lattice_vectors + && impl_->kinetic_prefactor == hamiltonian.kinetic_prefactor() + && impl_->finite_difference_order == hamiltonian.finite_difference_order() + && impl_->reference_eigenvalue == reference_eigenvalue && impl_->omega == omega + && impl_->regularization == regularization; +} + +std::shared_ptr +get_thread_local_sternheimer_fd_spectral_preconditioner(const SternheimerFDHamiltonian& hamiltonian, + const double reference_eigenvalue, + const double omega, + const double regularization) +{ + static thread_local std::shared_ptr cached; + if (!cached || !cached->is_compatible(hamiltonian, reference_eigenvalue, omega, regularization)) + { + cached = std::make_shared( + hamiltonian, reference_eigenvalue, omega, regularization); + } + return cached; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.h new file mode 100644 index 00000000000..337d2a400d8 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.h @@ -0,0 +1,49 @@ +#ifndef STERNHEIMER_FD_PRECONDITIONER_H +#define STERNHEIMER_FD_PRECONDITIONER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include + +namespace ModuleRI +{ + +class SternheimerFDSpectralPreconditioner +{ + public: + SternheimerFDSpectralPreconditioner(const SternheimerFDHamiltonian& hamiltonian, + double reference_eigenvalue, + double omega, + double regularization); + ~SternheimerFDSpectralPreconditioner(); + + SternheimerFDSpectralPreconditioner(const SternheimerFDSpectralPreconditioner&) = delete; + SternheimerFDSpectralPreconditioner& operator=(const SternheimerFDSpectralPreconditioner&) = delete; + + void apply(const SternheimerFDHamiltonian::Vector& input, + SternheimerFDHamiltonian::Vector& output) const; + void apply_batch(const SternheimerFDHamiltonian::Matrix& input, + SternheimerFDHamiltonian::Matrix& output) const; + void apply_batch(const SternheimerFDHamiltonian::Matrix& input, + SternheimerFDHamiltonian::Matrix& output, + int* threads_used) const; + + bool is_compatible(const SternheimerFDHamiltonian& hamiltonian, + double reference_eigenvalue, + double omega, + double regularization) const; + + private: + struct Impl; + std::unique_ptr impl_; +}; + +std::shared_ptr +get_thread_local_sternheimer_fd_spectral_preconditioner(const SternheimerFDHamiltonian& hamiltonian, + double reference_eigenvalue, + double omega, + double regularization); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.cpp new file mode 100644 index 00000000000..94250813639 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.cpp @@ -0,0 +1,364 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.h" + +#include "source_base/math_ylmreal.h" + +#include +#include +#include +#include +#include + +namespace +{ + +struct ProjectorChannel +{ + int radial_index = 0; + int angular_momentum = 0; + int magnetic_index = 0; + int ylm_index = 0; +}; + +int grid_size(const ModuleRI::SternheimerFDHamiltonian::Grid& grid) +{ + return grid.nx * grid.ny * grid.nz; +} + +int grid_index(const ModuleRI::SternheimerFDHamiltonian::Grid& grid, const int ix, const int iy, const int iz) +{ + return (ix * grid.ny + iy) * grid.nz + iz; +} + +void validate_grid(const ModuleRI::SternheimerFDHamiltonian::Grid& grid) +{ + if (grid.nx <= 0 || grid.ny <= 0 || grid.nz <= 0) + { + throw std::invalid_argument("Sternheimer projector sampler requires positive grid dimensions."); + } + if (grid.hx <= 0.0 || grid.hy <= 0.0 || grid.hz <= 0.0) + { + throw std::invalid_argument("Sternheimer projector sampler requires positive grid spacings."); + } + bool has_nonzero_kpoint = false; + for (const double coordinate : grid.kpoint) + { + if (!std::isfinite(coordinate)) + { + throw std::invalid_argument("Sternheimer projector sampler requires finite reduced k-point coordinates."); + } + has_nonzero_kpoint = has_nonzero_kpoint || coordinate != 0.0; + } + if (!grid.periodic && has_nonzero_kpoint) + { + throw std::invalid_argument("Sternheimer projector sampler nonperiodic grids cannot use a Bloch k-point."); + } +} + +void validate_radial_set(const ModuleRI::SternheimerFDRadialProjectorSet& radial_set) +{ + const int nr = static_cast(radial_set.radial_grid.size()); + const int nproj = static_cast(radial_set.beta_radials.size()); + if (nr < 2) + { + throw std::invalid_argument("Sternheimer projector sampler requires at least two radial grid points."); + } + if (nproj == 0) + { + throw std::invalid_argument("Sternheimer projector sampler requires at least one radial projector."); + } + if (static_cast(radial_set.angular_momenta.size()) != nproj) + { + throw std::invalid_argument("Sternheimer projector sampler angular-momentum count mismatch."); + } + if (static_cast(radial_set.d_radial.size()) != nproj) + { + throw std::invalid_argument("Sternheimer projector sampler D matrix row count mismatch."); + } + if (radial_set.radial_grid.front() < 0.0) + { + throw std::invalid_argument("Sternheimer projector sampler radial grid must be non-negative."); + } + for (int ir = 1; ir != nr; ++ir) + { + if (radial_set.radial_grid[ir] <= radial_set.radial_grid[ir - 1]) + { + throw std::invalid_argument("Sternheimer projector sampler radial grid must be strictly increasing."); + } + } + for (int ip = 0; ip != nproj; ++ip) + { + if (radial_set.angular_momenta[ip] < 0) + { + throw std::invalid_argument("Sternheimer projector sampler angular momenta must be non-negative."); + } + if (static_cast(radial_set.beta_radials[ip].size()) != nr) + { + throw std::invalid_argument("Sternheimer projector sampler radial beta size mismatch."); + } + if (static_cast(radial_set.d_radial[ip].size()) != nproj) + { + throw std::invalid_argument("Sternheimer projector sampler D matrix must be square."); + } + } +} + +std::vector build_channels(const std::vector& angular_momenta) +{ + std::vector channels; + for (int ip = 0; ip != static_cast(angular_momenta.size()); ++ip) + { + const int l = angular_momenta[ip]; + for (int m_index = 0; m_index != 2 * l + 1; ++m_index) + { + ProjectorChannel channel; + channel.radial_index = ip; + channel.angular_momentum = l; + channel.magnetic_index = m_index; + channel.ylm_index = l * l + m_index; + channels.push_back(channel); + } + } + return channels; +} + +struct PeriodicImageRange +{ + int first = 0; + int last = 0; +}; + +std::array periodic_image_ranges( + const std::array& displacement, + const ModuleRI::SternheimerFDHamiltonian::Grid& grid, + const double cutoff) +{ + std::array ranges{}; + if (!grid.periodic) + { + return ranges; + } + const ModuleRI::SternheimerFDLatticeVectors dual = ModuleRI::sternheimer_fd_grid_dual_vectors(grid); + for (int direction = 0; direction != 3; ++direction) + { + double reduced_displacement = 0.0; + double dual_norm_squared = 0.0; + for (int component = 0; component != 3; ++component) + { + reduced_displacement += dual[direction][component] * displacement[component]; + dual_norm_squared += dual[direction][component] * dual[direction][component]; + } + const double reduced_cutoff = std::sqrt(dual_norm_squared) * cutoff; + ranges[direction].first = static_cast(std::ceil(reduced_displacement - reduced_cutoff)); + ranges[direction].last = static_cast(std::floor(reduced_displacement + reduced_cutoff)); + } + return ranges; +} + +double interpolate_radial(const std::vector& radial_grid, const std::vector& values, const double radius) +{ + if (radius < radial_grid.front() || radius > radial_grid.back()) + { + return 0.0; + } + if (radius == radial_grid.front()) + { + return values.front(); + } + + const std::vector::const_iterator upper = std::upper_bound(radial_grid.begin(), radial_grid.end(), radius); + if (upper == radial_grid.end()) + { + return values.back(); + } + + const int hi = static_cast(upper - radial_grid.begin()); + const int lo = hi - 1; + const double width = radial_grid[hi] - radial_grid[lo]; + const double t = (radius - radial_grid[lo]) / width; + return (1.0 - t) * values[lo] + t * values[hi]; +} + +double abacus_betar_to_real_space_beta(const std::vector& radial_grid, + const ModuleBase::matrix& beta_radials, + const std::vector& angular_momenta, + const int ip, + const int ir) +{ + const double radius = radial_grid[ir]; + if (radius > 0.0) + { + return beta_radials(ip, ir) / radius; + } + + if (angular_momenta[ip] > 0) + { + return 0.0; + } + + for (int jr = ir + 1; jr != static_cast(radial_grid.size()); ++jr) + { + if (radial_grid[jr] > 0.0) + { + return beta_radials(ip, jr) / radial_grid[jr]; + } + } + return 0.0; +} + +void evaluate_real_spherical_harmonics(const int lmax, + const double x, + const double y, + const double z, + std::vector& ylm) +{ + ylm.assign((lmax + 1) * (lmax + 1), 0.0); + if (lmax == 0 || x * x + y * y + z * z > 1.0e-28) + { + ModuleBase::YlmReal::rlylm(lmax, x, y, z, ylm.data()); + } + else + { + ylm[0] = 0.28209479177387814347; + } +} + +} // namespace + +namespace ModuleRI +{ + +SternheimerFDRadialProjectorSet +make_sternheimer_fd_radial_projector_set_from_abacus_matrices(const std::vector& radial_grid, + const ModuleBase::matrix& beta_radials, + const std::vector& angular_momenta, + const ModuleBase::matrix& d_radial) +{ + const int nproj = static_cast(angular_momenta.size()); + const int nr = static_cast(radial_grid.size()); + if (nproj <= 0) + { + throw std::invalid_argument("Sternheimer ABACUS projector set requires at least one projector."); + } + if (beta_radials.nr != nproj || beta_radials.nc != nr) + { + throw std::invalid_argument("Sternheimer ABACUS projector set expects betar(nbeta, mesh)."); + } + if (d_radial.nr != nproj || d_radial.nc != nproj) + { + throw std::invalid_argument("Sternheimer ABACUS projector set expects dion(nbeta, nbeta)."); + } + + SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = radial_grid; + radial_set.angular_momenta = angular_momenta; + radial_set.beta_radials.assign(nproj, std::vector(nr, 0.0)); + radial_set.d_radial.assign(nproj, SternheimerFDNonlocalProjector::Vector(nproj, {0.0, 0.0})); + + for (int ip = 0; ip != nproj; ++ip) + { + for (int ir = 0; ir != nr; ++ir) + { + radial_set.beta_radials[ip][ir] + = abacus_betar_to_real_space_beta(radial_grid, beta_radials, angular_momenta, ip, ir); + } + for (int jp = 0; jp != nproj; ++jp) + { + radial_set.d_radial[ip][jp] = d_radial(ip, jp); + } + } + return radial_set; +} + +SternheimerFDNonlocalProjector::ProjectorBlock +sample_sternheimer_fd_projector_block(const SternheimerFDRadialProjectorSet& radial_set, + const SternheimerFDHamiltonian::Grid& grid, + const ModuleBase::Vector3& atom_position) +{ + validate_grid(grid); + validate_radial_set(radial_set); + + const std::vector channels = build_channels(radial_set.angular_momenta); + const int nchannel = static_cast(channels.size()); + const int size = grid_size(grid); + + SternheimerFDNonlocalProjector::ProjectorBlock block; + block.projectors.assign(nchannel, SternheimerFDNonlocalProjector::Vector(size, {0.0, 0.0})); + block.d_matrix.assign(nchannel, SternheimerFDNonlocalProjector::Vector(nchannel, {0.0, 0.0})); + + int lmax = 0; + for (const int l : radial_set.angular_momenta) + { + lmax = std::max(lmax, l); + } + + std::vector ylm; + const double cutoff = radial_set.radial_grid.back(); + for (int iz = 0; iz != grid.nz; ++iz) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int ix = 0; ix != grid.nx; ++ix) + { + const std::array position + = sternheimer_fd_grid_cartesian_position(grid, ix, iy, iz); + const std::array displacement{ + position[0] - atom_position.x, + position[1] - atom_position.y, + position[2] - atom_position.z}; + const std::array image_ranges + = periodic_image_ranges(displacement, grid, cutoff); + const int ir = grid_index(grid, ix, iy, iz); + + for (int rz = image_ranges[2].first; rz <= image_ranges[2].last; ++rz) + { + for (int ry = image_ranges[1].first; ry <= image_ranges[1].last; ++ry) + { + for (int rx = image_ranges[0].first; rx <= image_ranges[0].last; ++rx) + { + const std::array image{rx, ry, rz}; + const std::array translation + = sternheimer_fd_grid_lattice_translation(grid, image); + const double dx = displacement[0] - translation[0]; + const double dy = displacement[1] - translation[1]; + const double dz = displacement[2] - translation[2]; + const double radius = std::sqrt(dx * dx + dy * dy + dz * dz); + if (radius > cutoff) + { + continue; + } + + evaluate_real_spherical_harmonics(lmax, dx, dy, dz, ylm); + const std::complex phase = sternheimer_bloch_phase(grid.kpoint, image); + for (int ich = 0; ich != nchannel; ++ich) + { + const ProjectorChannel& channel = channels[ich]; + const double radial_value + = interpolate_radial(radial_set.radial_grid, + radial_set.beta_radials[channel.radial_index], + radius); + block.projectors[ich][ir] += phase * radial_value * ylm[channel.ylm_index]; + } + } + } + } + } + } + } + + for (int ich = 0; ich != nchannel; ++ich) + { + const ProjectorChannel& left = channels[ich]; + for (int jch = 0; jch != nchannel; ++jch) + { + const ProjectorChannel& right = channels[jch]; + if (left.angular_momentum == right.angular_momentum && left.magnetic_index == right.magnetic_index) + { + block.d_matrix[ich][jch] = radial_set.d_radial[left.radial_index][right.radial_index]; + } + } + } + + return block; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.h new file mode 100644 index 00000000000..8c80ccad0d6 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.h @@ -0,0 +1,39 @@ +#ifndef STERNHEIMER_FD_PROJECTOR_SAMPLER_H +#define STERNHEIMER_FD_PROJECTOR_SAMPLER_H + +#include "source_base/matrix.h" +#include "source_base/vector3.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h" + +#include +#include + +namespace ModuleRI +{ + +struct SternheimerFDRadialProjectorSet +{ + using Complex = std::complex; + using Matrix = SternheimerFDNonlocalProjector::Matrix; + + std::vector radial_grid; + std::vector> beta_radials; + std::vector angular_momenta; + Matrix d_radial; +}; + +SternheimerFDRadialProjectorSet +make_sternheimer_fd_radial_projector_set_from_abacus_matrices(const std::vector& radial_grid, + const ModuleBase::matrix& beta_radials, + const std::vector& angular_momenta, + const ModuleBase::matrix& d_radial); + +SternheimerFDNonlocalProjector::ProjectorBlock +sample_sternheimer_fd_projector_block(const SternheimerFDRadialProjectorSet& radial_set, + const SternheimerFDHamiltonian::Grid& grid, + const ModuleBase::Vector3& atom_position); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.cpp new file mode 100644 index 00000000000..ba71eec2bf6 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.cpp @@ -0,0 +1,527 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.h" + +#include "source_base/module_external/lapack_connector.h" + +#include +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace ModuleRI +{ +namespace +{ + +using Complex = SternheimerFDHamiltonian::Complex; +using Vector = SternheimerFDHamiltonian::Vector; + +void scale_vector(Vector& vector, const Complex factor) +{ +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != vector.size(); ++ir) + { + vector[ir] *= factor; + } +} + +void axpy(const Complex alpha, const Vector& x, Vector& y) +{ + if (x.size() != y.size()) + { + throw std::invalid_argument("Sternheimer FD vector axpy sizes do not match."); + } +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != x.size(); ++ir) + { + y[ir] += alpha * x[ir]; + } +} + +void normalize_vector(Vector& vector, const double volume_element) +{ + const double norm = sternheimer_fd_grid_norm(vector, volume_element); + if (norm <= 0.0) + { + throw std::runtime_error("Sternheimer FD Lanczos generated a zero vector."); + } + scale_vector(vector, Complex(1.0 / norm, 0.0)); +} + +std::vector diagonalize_real_symmetric(std::vector& matrix, const int size) +{ + char jobz = 'V'; + char uplo = 'U'; + const int lda = size; + int info = 0; + std::vector eigenvalues(size, 0.0); + double work_query = 0.0; + const int minus_one = -1; + + dsyev_(&jobz, &uplo, &size, matrix.data(), &lda, eigenvalues.data(), &work_query, &minus_one, &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer FD Lanczos Ritz diagonalization workspace query failed."); + } + + const int lwork = std::max(1, static_cast(std::ceil(work_query))); + std::vector work(lwork, 0.0); + dsyev_(&jobz, &uplo, &size, matrix.data(), &lda, eigenvalues.data(), work.data(), &lwork, &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer FD Lanczos Ritz diagonalization failed."); + } + return eigenvalues; +} + +SternheimerFDZeroOrderStates build_zero_order_states_from_wavefunctions( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& eigenvalues, + std::vector wavefunctions, + const double volume_element) +{ + SternheimerFDZeroOrderStates states; + states.eigenvalues = eigenvalues; + states.wavefunctions = std::move(wavefunctions); + states.residual_norms.assign(states.eigenvalues.size(), 0.0); + + for (std::size_t ib = 0; ib != states.wavefunctions.size(); ++ib) + { + normalize_vector(states.wavefunctions[ib], volume_element); + + Vector hpsi; + hamiltonian.apply(states.wavefunctions[ib], hpsi); + Vector residual(hpsi.size()); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != hpsi.size(); ++ir) + { + residual[ir] = hpsi[ir] - states.eigenvalues[ib] * states.wavefunctions[ib][ir]; + } + states.residual_norms[ib] = sternheimer_fd_grid_norm(residual, volume_element); + } + return states; +} + +} // namespace + +SternheimerFDHamiltonian::Complex sternheimer_fd_grid_dot(const SternheimerFDHamiltonian::Vector& lhs, + const SternheimerFDHamiltonian::Vector& rhs, + const double volume_element, + int* threads_used) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer FD grid dot requires a positive volume element."); + } + if (lhs.size() != rhs.size()) + { + throw std::invalid_argument("Sternheimer FD grid dot vector sizes do not match."); + } + + double real_part = 0.0; + double imag_part = 0.0; +#ifdef _OPENMP +#pragma omp parallel + { +#pragma omp single + { + if (threads_used != nullptr) + { + *threads_used = omp_get_num_threads(); + } + } + +#pragma omp for reduction(+ : real_part, imag_part) schedule(static) +#endif + for (std::size_t ir = 0; ir != lhs.size(); ++ir) + { + const double lhs_real = lhs[ir].real(); + const double lhs_imag = lhs[ir].imag(); + const double rhs_real = rhs[ir].real(); + const double rhs_imag = rhs[ir].imag(); + real_part += lhs_real * rhs_real + lhs_imag * rhs_imag; + imag_part += lhs_real * rhs_imag - lhs_imag * rhs_real; + } +#ifdef _OPENMP + } +#else + if (threads_used != nullptr) + { + *threads_used = 1; + } +#endif + return volume_element * SternheimerFDHamiltonian::Complex(real_part, imag_part); +} + +double sternheimer_fd_grid_norm(const SternheimerFDHamiltonian::Vector& wavefunction, const double volume_element) +{ + return std::sqrt(std::real(sternheimer_fd_grid_dot(wavefunction, wavefunction, volume_element))); +} + +double sternheimer_fd_linear_response_residual_norm( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const SternheimerFDHamiltonian::Vector& delta_wavefunction, + const double omega, + const double volume_element) +{ + const std::size_t grid_size = static_cast(hamiltonian.grid().size()); + if (rhs.size() != grid_size || delta_wavefunction.size() != grid_size) + { + throw std::invalid_argument("Sternheimer FD residual vectors do not match the grid."); + } + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer FD residual requires a positive volume element."); + } + for (const Vector& occupied: occupied_wavefunctions) + { + if (occupied.size() != grid_size) + { + throw std::invalid_argument("Sternheimer FD residual occupied state does not match the grid."); + } + } + + auto dot = [volume_element](const Vector& lhs, const Vector& rhs_vec) { + return sternheimer_fd_grid_dot(lhs, rhs_vec, volume_element); + }; + Vector projected_wavefunction = delta_wavefunction; + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, projected_wavefunction); + + Vector residual; + hamiltonian.apply(projected_wavefunction, residual); + const Complex shift(-reference_eigenvalue, omega); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != residual.size(); ++ir) + { + residual[ir] += shift * projected_wavefunction[ir]; + } + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, residual); + + Vector projected_rhs = rhs; + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, projected_rhs); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != residual.size(); ++ir) + { + residual[ir] -= projected_rhs[ir]; + } + return sternheimer_fd_grid_norm(residual, volume_element); +} + +SternheimerFDZeroOrderStates solve_sternheimer_fd_zero_order_dense(const SternheimerFDHamiltonian& hamiltonian, + const int num_states, + const double volume_element, + const int max_size) +{ + const int grid_size = hamiltonian.grid().size(); + if (num_states <= 0) + { + throw std::invalid_argument("Sternheimer FD zero-order solver requires a positive number of states."); + } + if (num_states > grid_size) + { + throw std::invalid_argument("Sternheimer FD zero-order solver requested more states than grid points."); + } + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer FD zero-order solver requires a positive volume element."); + } + + const auto eigenpairs = hamiltonian.diagonalize_dense(max_size); + const double inv_sqrt_volume = 1.0 / std::sqrt(volume_element); + + SternheimerFDZeroOrderStates states; + states.eigenvalues.assign(eigenpairs.eigenvalues.begin(), eigenpairs.eigenvalues.begin() + num_states); + states.wavefunctions.assign(num_states, SternheimerFDHamiltonian::Vector(grid_size)); + states.residual_norms.assign(num_states, 0.0); + + for (int ib = 0; ib != num_states; ++ib) + { +#pragma omp parallel for schedule(static) + for (int ir = 0; ir != grid_size; ++ir) + { + states.wavefunctions[ib][ir] = inv_sqrt_volume * eigenpairs.eigenvectors[ib][ir]; + } + + SternheimerFDHamiltonian::Vector hpsi; + hamiltonian.apply(states.wavefunctions[ib], hpsi); + SternheimerFDHamiltonian::Vector residual(grid_size); +#pragma omp parallel for schedule(static) + for (int ir = 0; ir != grid_size; ++ir) + { + residual[ir] = hpsi[ir] - states.eigenvalues[ib] * states.wavefunctions[ib][ir]; + } + states.residual_norms[ib] = sternheimer_fd_grid_norm(residual, volume_element); + } + + return states; +} + +SternheimerFDZeroOrderStates solve_sternheimer_fd_zero_order_lanczos( + const SternheimerFDHamiltonian& hamiltonian, + const int num_states, + const double volume_element, + const SternheimerFDLanczosOptions& options) +{ + const int grid_size = hamiltonian.grid().size(); + if (num_states <= 0) + { + throw std::invalid_argument("Sternheimer FD Lanczos zero-order solver requires a positive number of states."); + } + if (num_states > grid_size) + { + throw std::invalid_argument("Sternheimer FD Lanczos zero-order solver requested more states than grid points."); + } + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer FD Lanczos zero-order solver requires a positive volume element."); + } + if (options.max_subspace_size < num_states) + { + throw std::invalid_argument( + "Sternheimer FD Lanczos zero-order solver requires max_subspace_size >= num_states."); + } + if (options.residual_tolerance < 0.0) + { + throw std::invalid_argument( + "Sternheimer FD Lanczos zero-order solver requires a non-negative residual tolerance."); + } + + const int subspace_limit = std::min(options.max_subspace_size, grid_size); + std::mt19937 generator(options.initial_seed); + std::uniform_real_distribution distribution(-0.5, 0.5); + + Vector q(grid_size, Complex(0.0, 0.0)); + for (Complex& value: q) + { + value = Complex(distribution(generator), distribution(generator)); + } + normalize_vector(q, volume_element); + + std::vector basis; + basis.reserve(subspace_limit); + std::vector alpha; + alpha.reserve(subspace_limit); + std::vector beta; + beta.reserve(std::max(0, subspace_limit - 1)); + + Vector previous(grid_size, Complex(0.0, 0.0)); + double beta_previous = 0.0; + constexpr double breakdown_tolerance = 1.0e-14; + + for (int iter = 0; iter != subspace_limit; ++iter) + { + basis.push_back(q); + + Vector work; + hamiltonian.apply(q, work); + if (iter > 0) + { + axpy(Complex(-beta_previous, 0.0), previous, work); + } + + const double alpha_value = std::real(sternheimer_fd_grid_dot(q, work, volume_element)); + alpha.push_back(alpha_value); + axpy(Complex(-alpha_value, 0.0), q, work); + + for (int pass = 0; pass != 2; ++pass) + { + for (const Vector& basis_vector: basis) + { + const Complex overlap = sternheimer_fd_grid_dot(basis_vector, work, volume_element); + axpy(-overlap, basis_vector, work); + } + } + + const double beta_value = sternheimer_fd_grid_norm(work, volume_element); + if (iter + 1 == subspace_limit || beta_value < breakdown_tolerance) + { + break; + } + + beta.push_back(beta_value); + previous = q; + q = std::move(work); + scale_vector(q, Complex(1.0 / beta_value, 0.0)); + beta_previous = beta_value; + } + + const int subspace_size = static_cast(basis.size()); + if (subspace_size < num_states) + { + throw std::runtime_error("Sternheimer FD Lanczos subspace is smaller than the requested state count."); + } + + std::vector ritz_matrix(subspace_size * subspace_size, 0.0); + for (int i = 0; i != subspace_size; ++i) + { + ritz_matrix[i + subspace_size * i] = alpha[i]; + } + for (int i = 0; i + 1 != subspace_size; ++i) + { + ritz_matrix[i + (i + 1) * subspace_size] = beta[i]; + ritz_matrix[i + 1 + i * subspace_size] = beta[i]; + } + + const std::vector ritz_values = diagonalize_real_symmetric(ritz_matrix, subspace_size); + + std::vector eigenvalues(num_states, 0.0); + std::vector wavefunctions(num_states, Vector(grid_size, Complex(0.0, 0.0))); + for (int ib = 0; ib != num_states; ++ib) + { + eigenvalues[ib] = ritz_values[ib]; + for (int j = 0; j != subspace_size; ++j) + { + const double coefficient = ritz_matrix[j + ib * subspace_size]; + axpy(Complex(coefficient, 0.0), basis[j], wavefunctions[ib]); + } + } + + return build_zero_order_states_from_wavefunctions(hamiltonian, eigenvalues, std::move(wavefunctions), volume_element); +} + +SternheimerFDHamiltonian::Vector build_sternheimer_fd_complete_sos_response( + const SternheimerFDZeroOrderStates& complete_states, + const int occupied_state_count, + const int occupied_state_index, + const SternheimerFDHamiltonian::Vector& rhs, + const double omega, + const double volume_element) +{ + const std::size_t grid_size = rhs.size(); + if (grid_size == 0 || complete_states.wavefunctions.size() != grid_size + || complete_states.eigenvalues.size() != grid_size) + { + throw std::invalid_argument( + "Sternheimer complete SOS reference requires one eigenpair per finite-grid degree of freedom."); + } + if (occupied_state_count <= 0 || occupied_state_count >= static_cast(grid_size) + || occupied_state_index < 0 || occupied_state_index >= occupied_state_count) + { + throw std::invalid_argument("Sternheimer complete SOS reference has invalid occupied-state indices."); + } + if (omega < 0.0 || volume_element <= 0.0) + { + throw std::invalid_argument( + "Sternheimer complete SOS reference requires non-negative omega and a positive volume element."); + } + for (const auto& wavefunction: complete_states.wavefunctions) + { + if (wavefunction.size() != grid_size) + { + throw std::invalid_argument("Sternheimer complete SOS eigenvector size does not match the rhs."); + } + } + + SternheimerFDHamiltonian::Vector response(grid_size, SternheimerFDHamiltonian::Complex(0.0, 0.0)); + const double occupied_eigenvalue + = complete_states.eigenvalues[static_cast(occupied_state_index)]; + for (int state_index = occupied_state_count; state_index != static_cast(grid_size); ++state_index) + { + const auto& virtual_state = complete_states.wavefunctions[static_cast(state_index)]; + const SternheimerFDHamiltonian::Complex denominator( + complete_states.eigenvalues[static_cast(state_index)] - occupied_eigenvalue, + omega); + if (std::abs(denominator) < 1.0e-30) + { + throw std::runtime_error("Sternheimer complete SOS reference found a singular denominator."); + } + const SternheimerFDHamiltonian::Complex coefficient + = sternheimer_fd_grid_dot(virtual_state, rhs, volume_element) / denominator; + axpy(coefficient, virtual_state, response); + } + return response; +} + +SternheimerFDLinearResponse solve_sternheimer_fd_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options) +{ + const int grid_size = hamiltonian.grid().size(); + if (static_cast(rhs.size()) != grid_size) + { + throw std::invalid_argument("Sternheimer FD linear response rhs size does not match the grid."); + } + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer FD linear response requires a positive volume element."); + } + for (const auto& occupied: occupied_wavefunctions) + { + if (static_cast(occupied.size()) != grid_size) + { + throw std::invalid_argument("Sternheimer FD linear response occupied-state size does not match the grid."); + } + } + + auto dot = [volume_element](const SternheimerFDHamiltonian::Vector& lhs, + const SternheimerFDHamiltonian::Vector& rhs_vec) { + return sternheimer_fd_grid_dot(lhs, rhs_vec, volume_element); + }; + + SternheimerFDHamiltonian::Vector projected_rhs = rhs; + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, projected_rhs); + + SternheimerRPA::LinearProblem problem; + problem.dot = dot; + problem.apply = [&hamiltonian, &occupied_wavefunctions, reference_eigenvalue, omega, dot]( + const SternheimerFDHamiltonian::Vector& input, + SternheimerFDHamiltonian::Vector& output) { + SternheimerFDHamiltonian::Vector pc_input = input; + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, pc_input); + + hamiltonian.apply(pc_input, output); + const SternheimerFDHamiltonian::Complex shift(-reference_eigenvalue, omega); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != output.size(); ++ir) + { + output[ir] += shift * pc_input[ir]; + } + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, output); + }; + std::shared_ptr spectral_preconditioner; + if (options.use_fd_spectral_preconditioner) + { + spectral_preconditioner = get_thread_local_sternheimer_fd_spectral_preconditioner( + hamiltonian, + reference_eigenvalue, + omega, + options.fd_spectral_preconditioner_regularization); + problem.precondition = [spectral_preconditioner, &occupied_wavefunctions, dot]( + const SternheimerFDHamiltonian::Vector& input, + SternheimerFDHamiltonian::Vector& output) { + spectral_preconditioner->apply(input, output); + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, output); + }; + } + + SternheimerFDLinearResponse response; + response.delta_wavefunction.assign(grid_size, SternheimerFDHamiltonian::Complex(0.0, 0.0)); + response.solver = SternheimerRPA::solve_gmres(problem, projected_rhs, response.delta_wavefunction, options); + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, response.delta_wavefunction); + + SternheimerFDHamiltonian::Vector applied; + problem.apply(response.delta_wavefunction, applied); +#pragma omp parallel for schedule(static) + for (int ir = 0; ir != grid_size; ++ir) + { + applied[ir] -= projected_rhs[ir]; + } + response.residual_norm = sternheimer_fd_grid_norm(applied, volume_element); + return response; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h new file mode 100644 index 00000000000..e8aca6f6e9e --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h @@ -0,0 +1,83 @@ +#ifndef STERNHEIMER_FD_SOLVER_H +#define STERNHEIMER_FD_SOLVER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" + +#include +#include + +namespace ModuleRI +{ + +struct SternheimerFDZeroOrderStates +{ + std::vector eigenvalues; + std::vector wavefunctions; + std::vector residual_norms; +}; + +struct SternheimerFDLanczosOptions +{ + int max_subspace_size = 120; + double residual_tolerance = 1.0e-8; + unsigned int initial_seed = 1; +}; + +struct SternheimerFDLinearResponse +{ + SternheimerFDHamiltonian::Vector delta_wavefunction; + SternheimerRPA::SolverResult solver; + double residual_norm = 0.0; +}; + +SternheimerFDHamiltonian::Complex sternheimer_fd_grid_dot(const SternheimerFDHamiltonian::Vector& lhs, + const SternheimerFDHamiltonian::Vector& rhs, + double volume_element, + int* threads_used = nullptr); + +double sternheimer_fd_grid_norm(const SternheimerFDHamiltonian::Vector& wavefunction, double volume_element); + +double sternheimer_fd_linear_response_residual_norm( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const SternheimerFDHamiltonian::Vector& delta_wavefunction, + double omega, + double volume_element); + +SternheimerFDZeroOrderStates solve_sternheimer_fd_zero_order_dense(const SternheimerFDHamiltonian& hamiltonian, + int num_states, + double volume_element, + int max_size = 4096); + +SternheimerFDZeroOrderStates solve_sternheimer_fd_zero_order_lanczos( + const SternheimerFDHamiltonian& hamiltonian, + int num_states, + double volume_element, + const SternheimerFDLanczosOptions& options = SternheimerFDLanczosOptions()); + +// Build the exact positive-frequency response in a finite grid from its +// complete eigenspectrum. This is a small-grid validation reference, not a +// production replacement for the iterative Sternheimer solve. +SternheimerFDHamiltonian::Vector build_sternheimer_fd_complete_sos_response( + const SternheimerFDZeroOrderStates& complete_states, + int occupied_state_count, + int occupied_state_index, + const SternheimerFDHamiltonian::Vector& rhs, + double omega, + double volume_element); + +SternheimerFDLinearResponse solve_sternheimer_fd_linear_response( + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions()); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.cpp new file mode 100644 index 00000000000..20b5fd61b01 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.cpp @@ -0,0 +1,359 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.h" + +#include "source_base/module_external/blas_connector.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h" + +#include +#include +#include +#include +#include + +namespace +{ + +using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; +using Grid = Hamiltonian::Grid; +using Complex = Hamiltonian::Complex; +using Vector = Hamiltonian::Vector; +using Galerkin = ModuleRI::SternheimerGalerkinOperator; +using Transfer = ModuleRI::SternheimerGridTransfer; +constexpr std::size_t state_block_size = 8; +constexpr std::size_t grid_block_size = 8192; + +std::size_t checked_add(const std::size_t left, const std::size_t right) +{ + if (right > std::numeric_limits::max() - left) + { + throw std::length_error("Sternheimer Galerkin audit workspace size overflows size_t."); + } + return left + right; +} + +std::size_t checked_multiply(const std::size_t left, const std::size_t right) +{ + if (left != 0 && right > std::numeric_limits::max() / left) + { + throw std::length_error("Sternheimer Galerkin audit workspace size overflows size_t."); + } + return left * right; +} + +std::size_t complex_bytes(const std::size_t count) +{ + return checked_multiply(count, sizeof(Complex)); +} + +bool same_dimensions(const Grid& coarse, const Grid& fine) +{ + // Geometry and Bloch-sector equality are validated by Transfer first. + return coarse.nx == fine.nx && coarse.ny == fine.ny && coarse.nz == fine.nz; +} + +double volume_element(const Grid& grid) +{ + const auto a = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + const double volume = std::abs(a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) + + a[0][1] * (a[1][2] * a[2][0] - a[1][0] * a[2][2]) + + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])); + return volume / grid.size(); +} + +bool finite(const Complex value) +{ + return std::isfinite(value.real()) && std::isfinite(value.imag()); +} + +double norm_squared(const Vector& state, const double weight) +{ + double sum = 0.0; + for (const Complex value : state) + { + sum += std::norm(value); + } + const double norm = weight * sum; + if (!std::isfinite(norm)) + { + throw std::runtime_error("Sternheimer Galerkin audit encountered a nonfinite norm."); + } + return norm; +} + +// Grid tiles keep BLAS leading dimensions small; state columns stay in their +// original coordinates. This workspace is private to one synchronous audit. +class BlockedProducts +{ + public: + BlockedProducts(const std::size_t count, const std::size_t points, const std::size_t block) + : points_(points), + left_packed_(std::min(grid_block_size, points) * count), + right_packed_(std::min(grid_block_size, points) * block) + { + } + + void accumulate(const std::vector& left, + const std::vector& right, + const std::size_t right_count, + const double weight, + Complex* output) + { + for (std::size_t begin = 0; begin < points_; begin += grid_block_size) + { + const std::size_t count = std::min(grid_block_size, points_ - begin); + for (std::size_t column = 0; column != left.size(); ++column) + { + std::copy_n(left[column] + begin, count, left_packed_.data() + count * column); + } + for (std::size_t column = 0; column != right_count; ++column) + { + std::copy_n(right[column] + begin, count, right_packed_.data() + count * column); + } + BlasConnector::gemm_cm('C', 'N', static_cast(left.size()), static_cast(right_count), + static_cast(count), Complex(weight, 0.0), + left_packed_.data(), static_cast(count), + right_packed_.data(), static_cast(count), Complex(1.0, 0.0), + output, static_cast(left.size())); + } + } + + private: + std::size_t points_; + Vector left_packed_; + Vector right_packed_; +}; + +} // namespace + +namespace ModuleRI +{ + +std::size_t sternheimer_galerkin_audit_workspace_bytes(const Hamiltonian& fine, + const Grid& coarse, + const std::size_t dimension) +{ + if (dimension == 0 || dimension > static_cast(std::numeric_limits::max())) + { + throw std::invalid_argument("Sternheimer Galerkin audit needs a nonempty BLAS-sized state set."); + } + // Validate grids, Bloch sector and FD8 before size arithmetic or allocation. + const std::size_t operator_bytes = Galerkin::workspace_bytes_required(fine, coarse); + const std::size_t transfer_bytes = Transfer::workspace_bytes_required(coarse, fine.grid()); + const std::size_t nf = static_cast(fine.grid().size()); + const std::size_t nc = static_cast(coarse.size()); + const bool identity = same_dimensions(coarse, fine.grid()); + const std::size_t block = std::min(state_block_size, dimension); + const std::size_t matrix_size = checked_multiply(dimension, dimension); + std::size_t persistent = checked_add(complex_bytes(checked_multiply(5, matrix_size)), + checked_multiply(checked_multiply(3, dimension), sizeof(double))); + persistent = checked_add(persistent, checked_multiply(checked_multiply(2, dimension), sizeof(const Complex*))); + if (!identity) + { + persistent = checked_add(persistent, complex_bytes(checked_multiply(checked_add(nc, nf), dimension))); + } + + const std::size_t projection = identity ? 0 : checked_add(transfer_bytes, complex_bytes(checked_add(nc, nf))); + const std::size_t coarse_pack = checked_multiply(std::min(grid_block_size, nc), checked_add(dimension, block)); + std::size_t coarse_vectors = checked_add(checked_multiply(nc, block), coarse_pack); + coarse_vectors = checked_add(coarse_vectors, checked_multiply(identity ? 1 : 2, nc)); + const std::size_t pointers = checked_multiply(block, sizeof(const Complex*)); + const std::size_t coarse_stage = checked_add(checked_add(operator_bytes, complex_bytes(coarse_vectors)), pointers); + + // Reuse the frozen operator's bound for scalar H_f transient scratch only. + const std::size_t fine_scratch = operator_bytes - transfer_bytes - complex_bytes(checked_multiply(2, nf)); + const std::size_t fine_pack = checked_multiply(std::min(grid_block_size, nf), checked_add(dimension, block)); + std::size_t fine_vectors = checked_add(checked_multiply(nf, block), fine_pack); + fine_vectors = checked_add(fine_vectors, checked_multiply(identity ? 1 : 2, nf)); + fine_vectors = checked_add(fine_vectors, matrix_size); + const std::size_t fine_stage = checked_add(checked_add(fine_scratch, complex_bytes(fine_vectors)), pointers); + return checked_add(persistent, std::max(projection, std::max(coarse_stage, fine_stage))); +} + +SternheimerGalerkinAuditMatrices audit_sternheimer_galerkin_matrices( + std::shared_ptr fine, + const std::vector& fine_states, + Grid coarse, + const std::size_t max_workspace_bytes) +{ + if (!fine) + { + throw std::invalid_argument("Sternheimer Galerkin audit requires a fine Hamiltonian."); + } + const std::size_t n = fine_states.size(); + const std::size_t required = sternheimer_galerkin_audit_workspace_bytes(*fine, coarse, n); + if (required > max_workspace_bytes) + { + throw std::length_error("Sternheimer Galerkin audit numerical payload exceeds its budget."); + } + const std::size_t nf = static_cast(fine->grid().size()); + const std::size_t nc = static_cast(coarse.size()); + for (const Vector* state : fine_states) + { + if (state == nullptr || state->size() != nf) + { + throw std::invalid_argument("Sternheimer Galerkin audit requires nonnull full fine-grid state views."); + } + for (const Complex value : *state) + { + if (!finite(value)) + { + throw std::invalid_argument("Sternheimer Galerkin audit input states must be finite."); + } + } + } + + SternheimerGalerkinAuditMatrices result; + result.dimension = n; + result.workspace_bytes = required; + result.overlap.resize(n * n); + result.kinetic.resize(n * n); + result.local_potential.resize(n * n); + result.nonlocal.resize(n * n); + result.hamiltonian.resize(n * n); + result.fine_norm_squared.resize(n); + result.projected_norm_squared.resize(n); + result.reconstruction_relative_error.resize(n); + const bool identity = same_dimensions(coarse, fine->grid()); + const double dvf = volume_element(fine->grid()); + const double dvc = volume_element(coarse); + const std::size_t block = std::min(state_block_size, n); + Vector coarse_cache; + Vector filtered_cache; + std::vector coarse_columns(n); + std::vector filtered_columns(n); + for (std::size_t column = 0; column != n; ++column) + { + result.fine_norm_squared[column] = norm_squared(*fine_states[column], dvf); + } + if (identity) + { + for (std::size_t column = 0; column != n; ++column) + { + coarse_columns[column] = fine_states[column]->data(); + filtered_columns[column] = fine_states[column]->data(); + } + result.projected_norm_squared = result.fine_norm_squared; + } + else + { + coarse_cache.resize(nc * n); + filtered_cache.resize(nf * n); + Transfer transfer(coarse, fine->grid()); + Vector projected(nc); + Vector reconstructed(nf); + for (std::size_t column = 0; column != n; ++column) + { + transfer.restrict_adjoint(*fine_states[column], projected); + transfer.interpolate(projected, reconstructed); + std::copy(projected.begin(), projected.end(), coarse_cache.data() + nc * column); + std::copy(reconstructed.begin(), reconstructed.end(), filtered_cache.data() + nf * column); + coarse_columns[column] = coarse_cache.data() + nc * column; + filtered_columns[column] = filtered_cache.data() + nf * column; + result.projected_norm_squared[column] = norm_squared(projected, dvc); + double error_squared = 0.0; + for (std::size_t point = 0; point != nf; ++point) + { + error_squared += std::norm(reconstructed[point] - (*fine_states[column])[point]); + } + if (!std::isfinite(error_squared)) + { + throw std::runtime_error("Sternheimer Galerkin audit encountered a nonfinite reconstruction error."); + } + const double fine_norm = result.fine_norm_squared[column]; + result.reconstruction_relative_error[column] = fine_norm == 0.0 ? 0.0 : std::sqrt(dvf * error_squared / fine_norm); + } + } + + // Assemble C* H_c,term C through the actual Galerkin component API. Total H + // is applied separately, not manufactured by summing reported components. + { + Galerkin op(fine, coarse, Galerkin::workspace_bytes_required(*fine, coarse)); + BlockedProducts products(n, nc, block); + Vector input(identity ? 0 : nc); + Vector output(nc); + Vector images(nc * block); + std::vector right(block); + using Apply = void (Galerkin::*)(const Vector&, Vector&); + const std::array apply{{&Galerkin::apply_kinetic, &Galerkin::apply_local_potential, + &Galerkin::apply_nonlocal, &Galerkin::apply}}; + const std::array matrices{{&result.kinetic, &result.local_potential, + &result.nonlocal, &result.hamiltonian}}; + for (std::size_t begin = 0; begin < n; begin += block) + { + const std::size_t count = std::min(block, n - begin); + for (std::size_t column = 0; column != count; ++column) + { + right[column] = coarse_columns[begin + column]; + } + products.accumulate(coarse_columns, right, count, dvc, result.overlap.data() + n * begin); + for (std::size_t term = 0; term != apply.size(); ++term) + { + for (std::size_t column = 0; column != count; ++column) + { + const Vector* state = fine_states[begin + column]; + if (!identity) + { + std::copy_n(coarse_columns[begin + column], nc, input.data()); + state = &input; + } + (op.*apply[term])(*state, output); + std::copy(output.begin(), output.end(), images.data() + nc * column); + right[column] = images.data() + nc * column; + } + products.accumulate(coarse_columns, right, count, dvc, matrices[term]->data() + n * begin); + } + } + } + + // Independent FILTERED fine contraction, not a comparison to unfiltered KS. + // Cached J C columns are reused: this stage performs no FFTs and just one + // direct H_f application per state, with bounded blocks of operator images. + { + BlockedProducts products(n, nf, block); + Vector input(identity ? 0 : nf); + Vector output(nf); + Vector images(nf * block); + Vector reference(n * n); + std::vector right(block); + for (std::size_t begin = 0; begin < n; begin += block) + { + const std::size_t count = std::min(block, n - begin); + for (std::size_t column = 0; column != count; ++column) + { + const Vector* state = fine_states[begin + column]; + if (!identity) + { + std::copy_n(filtered_columns[begin + column], nf, input.data()); + state = &input; + } + fine->apply(*state, output); + std::copy(output.begin(), output.end(), images.data() + nf * column); + right[column] = images.data() + nf * column; + } + products.accumulate(filtered_columns, right, count, dvf, reference.data() + n * begin); + } + for (std::size_t i = 0; i != reference.size(); ++i) + { + const double error = std::abs(reference[i] - result.hamiltonian[i]); + if (!std::isfinite(error)) + { + throw std::runtime_error("Sternheimer Galerkin audit encountered a nonfinite filtered matrix check."); + } + result.filtered_matrix_identity_max_abs_error = std::max(result.filtered_matrix_identity_max_abs_error, error); + } + } + for (const Vector* matrix : {&result.overlap, &result.kinetic, &result.local_potential, + &result.nonlocal, &result.hamiltonian}) + { + for (const Complex value : *matrix) + { + if (!finite(value)) + { + throw std::runtime_error("Sternheimer Galerkin audit encountered a nonfinite matrix element."); + } + } + } + return result; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.h new file mode 100644 index 00000000000..2eda344718c --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.h @@ -0,0 +1,61 @@ +#ifndef STERNHEIMER_GALERKIN_AUDIT_H +#define STERNHEIMER_GALERKIN_AUDIT_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include +#include +#include + +namespace ModuleRI +{ + +struct SternheimerGalerkinAuditMatrices +{ + std::size_t dimension = 0; + // Column-major matrices in the ORIGINAL input-state coordinates, including + // zero/dependent states. C = J* Phi_f; every operator term is C* H_c,term C. + std::vector overlap; + std::vector kinetic; + std::vector local_potential; + std::vector nonlocal; + std::vector hamiltonian; + std::vector fine_norm_squared; + std::vector projected_norm_squared; + // ||J C_i - Phi_f,i||_f / ||Phi_f,i||_f; defined as zero for a zero input state. + std::vector reconstruction_relative_error; + // Maximum absolute H-matrix discrepancy against independently contracted + // (J C)* H_f (J C), using direct fine-H applications and fine-grid BLAS. + // This compares FILTERED fine states, not the unfiltered Phi_f* H_f Phi_f. + double filtered_matrix_identity_max_abs_error = 0.0; + std::size_t workspace_bytes = 0; +}; + +// Allocation-free peak numerical-payload bound, including returned matrices, +// norms, nonidentity caches for C and J C, and blocked/streamed workspace. +// Excludes the borrowed H_f and input states, FFTW/BLAS opaque internal storage, +// allocator/container and runtime overhead. Not a process-RSS limit. +std::size_t sternheimer_galerkin_audit_workspace_bytes( + const SternheimerFDHamiltonian& fine_hamiltonian, + const SternheimerFDHamiltonian::Grid& coarse_grid, + std::size_t dimension); + +// Synchronous spectrum-only audit. All views must be nonnull finite full fine-grid +// vectors in H_f's Bloch sector and remain alive/immutable throughout the call. +// Values alone cannot verify KS provenance or source/target-sector assignments. +// No normalization, state selection, energy shifts, diagonalization, or file I/O. +// With equal grids, borrows the fine views without making another full-state copy; +// that call yields the UNFILTERED fine reference. With different grids, caches C +// and J C once each; operator images are streamed in fixed-size state blocks. +// Insufficient budgets are rejected before numerical allocation, never truncated. +// Each call owns its mutable contexts; a shared H_f must remain immutable. +// Neither these matrices nor a positive selected subspace prove full-Pc positivity. +SternheimerGalerkinAuditMatrices audit_sternheimer_galerkin_matrices( + std::shared_ptr fine_hamiltonian, + const std::vector& fine_states, + SternheimerFDHamiltonian::Grid coarse_grid, + std::size_t max_workspace_bytes); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.cpp new file mode 100644 index 00000000000..ffbc371da4d --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.cpp @@ -0,0 +1,207 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.h" + +#include +#include +#include +#include + +namespace +{ + +using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; +using Galerkin = ModuleRI::SternheimerGalerkinOperator; + +std::size_t CheckedAdd(const std::size_t left, const std::size_t right) +{ + if (right > std::numeric_limits::max() - left) + { + throw std::length_error("Sternheimer Galerkin workspace size overflows size_t."); + } + return left + right; +} + +std::size_t CheckedMultiply(const std::size_t left, const std::size_t right) +{ + if (left != 0 && right > std::numeric_limits::max() / left) + { + throw std::length_error("Sternheimer Galerkin workspace size overflows size_t."); + } + return left * right; +} + +const Hamiltonian& RequireFine(const std::shared_ptr& fine) +{ + if (!fine) + { + throw std::invalid_argument("Sternheimer Galerkin requires a fine Hamiltonian."); + } + return *fine; +} + +std::size_t CheckedWorkspace(const Hamiltonian& fine, const Hamiltonian::Grid& coarse, const std::size_t budget) +{ + const std::size_t bytes = Galerkin::workspace_bytes_required(fine, coarse); + if (bytes > budget) + { + throw std::length_error("Sternheimer Galerkin numerical workspace exceeds its instance budget."); + } + return bytes; +} + +std::shared_ptr CopyFine(const Hamiltonian& fine, + const Hamiltonian::Grid& coarse, + const std::size_t budget) +{ + CheckedWorkspace(fine, coarse, budget); + return std::make_shared(fine); +} + +} // namespace + +namespace ModuleRI +{ + +SternheimerGalerkinOperator::SternheimerGalerkinOperator(std::shared_ptr fine_hamiltonian, + const Grid& coarse_grid, + const std::size_t max_workspace_bytes) + : fine_hamiltonian_(std::move(fine_hamiltonian)), + workspace_bytes_(CheckedWorkspace(RequireFine(fine_hamiltonian_), coarse_grid, max_workspace_bytes)), + transfer_(coarse_grid, fine_hamiltonian_->grid()), + fine_input_(static_cast(fine_hamiltonian_->grid().size())), + fine_output_(fine_input_.size()) +{ +} + +SternheimerGalerkinOperator::SternheimerGalerkinOperator(const Hamiltonian& fine_hamiltonian, + const Grid& coarse_grid, + const std::size_t max_workspace_bytes) + : SternheimerGalerkinOperator(CopyFine(fine_hamiltonian, coarse_grid, max_workspace_bytes), + coarse_grid, max_workspace_bytes) +{ +} + +const SternheimerGalerkinOperator::Grid& SternheimerGalerkinOperator::grid() const +{ + return transfer_.coarse_grid(); +} + +const SternheimerGalerkinOperator::Hamiltonian& SternheimerGalerkinOperator::fine_hamiltonian() const +{ + return *fine_hamiltonian_; +} + +std::size_t SternheimerGalerkinOperator::workspace_bytes_required(const Hamiltonian& fine, const Grid& coarse) +{ + if (fine.finite_difference_order() != 8) + { + throw std::invalid_argument("Sternheimer Galerkin reference prototype requires the fine FD8 operator."); + } + // Transfer validation checks both grid sizes, periodicity, lattice and k + // before grid.size() or any FFT/fine-vector allocation is used here. + const std::size_t transfer_bytes = SternheimerGridTransfer::workspace_bytes_required(coarse, fine.grid()); + const std::size_t fine_size = static_cast(fine.grid().size()); + const std::size_t fine_vector_bytes = CheckedMultiply(fine_size, sizeof(Complex)); + const std::size_t persistent_bytes = CheckedMultiply(2, fine_vector_bytes); + const auto& directions = fine.cached_first_derivative_directions(); + const auto direction_count = static_cast(std::count(directions.begin(), directions.end(), true)); + const std::size_t derivative_bytes = CheckedMultiply(direction_count, fine_vector_bytes); + std::size_t max_projectors = 0; + if (fine.nonlocal_projector() != nullptr) + { + for (const auto& block: fine.nonlocal_projector()->blocks()) + { + max_projectors = std::max(max_projectors, block.projectors.size()); + } + } + const std::size_t projector_bytes = CheckedMultiply(CheckedMultiply(2, max_projectors), sizeof(Complex)); + // Scalar FD derivative buffers are destroyed before the nonlocal apply; + // nonlocal blocks are processed serially, each with two coefficient vectors. + return CheckedAdd(CheckedAdd(transfer_bytes, persistent_bytes), std::max(derivative_bytes, projector_bytes)); +} + +std::size_t SternheimerGalerkinOperator::workspace_bytes() const +{ + return workspace_bytes_; +} + +void SternheimerGalerkinOperator::validate_input(const Vector& psi) const +{ + if (psi.size() != static_cast(grid().size())) + { + throw std::invalid_argument("Sternheimer Galerkin input size does not match the coarse grid."); + } +} + +void SternheimerGalerkinOperator::apply_component(const Vector& psi, Vector& output, const Component component) +{ + validate_input(psi); + transfer_.interpolate(psi, fine_input_); + switch (component) + { + case Component::Total: fine_hamiltonian_->apply(fine_input_, fine_output_); break; + case Component::Kinetic: fine_hamiltonian_->apply_kinetic(fine_input_, fine_output_); break; + case Component::Local: fine_hamiltonian_->apply_local_potential(fine_input_, fine_output_); break; + case Component::Nonlocal: fine_hamiltonian_->apply_nonlocal(fine_input_, fine_output_); break; + } + transfer_.restrict_adjoint(fine_output_, output); +} + +void SternheimerGalerkinOperator::apply_component_batch(const Matrix& psi, Matrix& output, const Component component) +{ + for (const auto& vector: psi) + { + validate_input(vector); + } + if (&psi != &output) + { + output.resize(psi.size()); + } + // Stream RHS vectors to bound fine-grid workspace independently of batch + // length. The fine Hamiltonian still controls its internal OpenMP execution. + for (std::size_t column = 0; column != psi.size(); ++column) + { + apply_component(psi[column], output[column], component); + } +} + +void SternheimerGalerkinOperator::apply(const Vector& psi, Vector& hpsi) +{ + apply_component(psi, hpsi, Component::Total); +} + +void SternheimerGalerkinOperator::apply_kinetic(const Vector& psi, Vector& kinetic_psi) +{ + apply_component(psi, kinetic_psi, Component::Kinetic); +} + +void SternheimerGalerkinOperator::apply_local_potential(const Vector& psi, Vector& local_psi) +{ + apply_component(psi, local_psi, Component::Local); +} + +void SternheimerGalerkinOperator::apply_nonlocal(const Vector& psi, Vector& nonlocal_psi) +{ + apply_component(psi, nonlocal_psi, Component::Nonlocal); +} + +void SternheimerGalerkinOperator::apply_batch(const Matrix& psi, Matrix& hpsi) +{ + apply_component_batch(psi, hpsi, Component::Total); +} + +void SternheimerGalerkinOperator::apply_kinetic_batch(const Matrix& psi, Matrix& kinetic_psi) +{ + apply_component_batch(psi, kinetic_psi, Component::Kinetic); +} + +void SternheimerGalerkinOperator::apply_local_potential_batch(const Matrix& psi, Matrix& local_psi) +{ + apply_component_batch(psi, local_psi, Component::Local); +} + +void SternheimerGalerkinOperator::apply_nonlocal_batch(const Matrix& psi, Matrix& nonlocal_psi) +{ + apply_component_batch(psi, nonlocal_psi, Component::Nonlocal); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.h new file mode 100644 index 00000000000..5b0f1f1ad04 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.h @@ -0,0 +1,79 @@ +#ifndef STERNHEIMER_GALERKIN_OPERATOR_H +#define STERNHEIMER_GALERKIN_OPERATOR_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h" + +#include +#include + +namespace ModuleRI +{ + +// Reference prototype: H_c = J* H_f J, where J* is the grid-weighted adjoint. +// The supplied H_f remains FD8, including its original local potential and +// nonlocal projectors/D matrices. No point-local coarse potential is defined. +// H_f is the FullGrid FD8 reference, not the old fine weak-A/hybrid response. +// Each concurrent worker must own a separate instance; apply uses mutable +// instance-owned scratch. A shared fine Hamiltonian must remain immutable. +// Apply has no cross-instance lock; FFTW plan management belongs to transfer. +class SternheimerGalerkinOperator +{ + public: + using Hamiltonian = SternheimerFDHamiltonian; + using Grid = Hamiltonian::Grid; + using Complex = Hamiltonian::Complex; + using Vector = Hamiltonian::Vector; + using Matrix = Hamiltonian::Matrix; + + SternheimerGalerkinOperator(std::shared_ptr fine_hamiltonian, + const Grid& coarse_grid, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024); + // Copies H_f; its already-immutable nonlocal projector can remain shared. + SternheimerGalerkinOperator(const Hamiltonian& fine_hamiltonian, + const Grid& coarse_grid, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024); + + SternheimerGalerkinOperator(const SternheimerGalerkinOperator&) = delete; + SternheimerGalerkinOperator& operator=(const SternheimerGalerkinOperator&) = delete; + + const Grid& grid() const; + const Hamiltonian& fine_hamiltonian() const; + + // Budget for numerical payload: transfer storage, two retained fine-grid + // vectors and peak scalar fine-H scratch. Excludes H_f storage (also when + // copied), caller input/output, FFTW opaque plans, allocator/container and + // OpenMP runtime overhead. This is not a process-RSS limit. Batch outputs + // are caller-owned; batches reuse the same single-vector workspace. + static std::size_t workspace_bytes_required(const Hamiltonian& fine_hamiltonian, const Grid& coarse_grid); + std::size_t workspace_bytes() const; + + // Scalar and batch input/output may alias. All input dimensions are checked + // before output modification. Matrix batches are vector-major, as in H_f. + void apply(const Vector& psi, Vector& hpsi); + void apply_kinetic(const Vector& psi, Vector& kinetic_psi); + void apply_local_potential(const Vector& psi, Vector& local_psi); + void apply_nonlocal(const Vector& psi, Vector& nonlocal_psi); + void apply_batch(const Matrix& psi, Matrix& hpsi); + void apply_kinetic_batch(const Matrix& psi, Matrix& kinetic_psi); + void apply_local_potential_batch(const Matrix& psi, Matrix& local_psi); + void apply_nonlocal_batch(const Matrix& psi, Matrix& nonlocal_psi); + + private: + enum class Component { Total, Kinetic, Local, Nonlocal }; + + void validate_input(const Vector& psi) const; + void apply_component(const Vector& psi, Vector& output, Component component); + void apply_component_batch(const Matrix& psi, Matrix& output, Component component); + + std::shared_ptr fine_hamiltonian_; + // Initialization order ensures budget checks precede transfer/FFT allocation. + std::size_t workspace_bytes_; + SternheimerGridTransfer transfer_; + Vector fine_input_; + Vector fine_output_; +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.cpp new file mode 100644 index 00000000000..79b2c62530f --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.cpp @@ -0,0 +1,537 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.h" + +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +SternheimerAOPotentials read_sternheimer_ao_potentials(const std::string& path, const int nao, const int naux) +{ + std::ifstream input(path); + std::string magic, units, extra; + int version = 0, file_nao = 0, file_naux = 0; + if (!(input >> magic >> version >> file_nao >> file_naux >> units) || magic != "ABACUS_STERNHEIMER_AO_POTENTIALS" + || version != 1 || units != "Gamma_Hartree" || nao <= 0 || naux <= 0 || file_nao != nao || file_naux != naux) + { + throw std::runtime_error("Analytic AO potential header or dimensions differ."); + } + SternheimerAOPotentials result; + result.nao = nao; + result.naux = naux; + const std::size_t block = static_cast(nao) * nao; + if (block > std::numeric_limits::max() / static_cast(naux)) + { + throw std::runtime_error("Analytic AO potential dimensions overflow."); + } + result.values.resize(block * naux); + for (auto& value: result.values) + { + double re = 0., im = 0.; + if (!(input >> re >> im) || !std::isfinite(re) || !std::isfinite(im)) + { + throw std::runtime_error("Incomplete or non-finite analytic AO potentials."); + } + value = {re, im}; + } + if (input >> extra) + { + throw std::runtime_error("Trailing data in analytic AO potentials."); + } + double error = 0., norm = 0.; + for (int mu = 0; mu < naux; ++mu) + { + for (int p = 0; p < nao; ++p) + { + for (int q = 0; q < nao; ++q) + { + const auto value = result.values[mu * block + p * nao + q]; + error += std::norm(value - std::conj(result.values[mu * block + q * nao + p])); + norm += std::norm(value); + } + } + } + if (norm == 0. || !std::isfinite(norm) || std::sqrt(error / norm) > 1.e-10) + { + throw std::runtime_error("Analytic AO potential matrices are not Hermitian."); + } + return result; +} + +SternheimerPerturbationTensor contract_sternheimer_ao_potentials( + const SternheimerAOPotentials& potentials, + const std::vector>>& occupied_coefficients, + const std::vector>>& virtual_coefficients) +{ + const int nao = potentials.nao; + const std::size_t block = static_cast(nao) * nao; + if (nao <= 0 || potentials.naux <= 0 || potentials.values.size() != block * potentials.naux) + { + throw std::invalid_argument("Invalid analytic AO potential dimensions."); + } + for (const auto* coefficients: {&occupied_coefficients, &virtual_coefficients}) + { + for (const auto& state: *coefficients) + { + if (state.size() != static_cast(nao)) + { + throw std::invalid_argument("Analytic AO coefficient dimensions differ."); + } + } + } + SternheimerPerturbationTensor tensor(static_cast(occupied_coefficients.size()), + static_cast(virtual_coefficients.size()), + potentials.naux); + for (int i = 0; i < tensor.occupied; ++i) + { + for (int mu = 0; mu < tensor.auxiliaries; ++mu) + { + std::vector> image(nao, 0.); + for (int p = 0; p < nao; ++p) + { + for (int q = 0; q < nao; ++q) + { + image[p] += potentials.values[mu * block + p * nao + q] * occupied_coefficients[i][q]; + } + } + for (int a = 0; a < tensor.virtuals; ++a) + { + for (int p = 0; p < nao; ++p) + { + tensor.at(i, a, mu) += std::conj(virtual_coefficients[a][p]) * image[p]; + } + } + } + } + return tensor; +} + +void accumulate_sternheimer_analytic_ao_column(const SternheimerPerturbationTensor& couplings_ha, + const int occupied_index, + const std::vector>& response_coefficients, + const double occupation, + const int channel, + std::vector>& branch) +{ + const int naux = couplings_ha.auxiliaries; + if (response_coefficients.size() != static_cast(couplings_ha.virtuals) + || branch.size() != static_cast(naux) * naux || channel < 0 || channel >= naux) + { + throw std::invalid_argument("Analytic AO response projection dimensions differ."); + } + for (int mu = 0; mu < naux; ++mu) + { + std::complex value = 0.; + for (int a = 0; a < couplings_ha.virtuals; ++a) + { + value += std::conj(couplings_ha.at(occupied_index, a, mu)) * response_coefficients[a]; + } + branch[static_cast(mu) * naux + channel] += occupation * value; + } +} + +namespace +{ + +using Complex = std::complex; + +std::size_t checked_product(const int first, const int second, const char* label) +{ + if (first < 0 || second < 0) + { + throw std::invalid_argument(std::string(label) + " dimensions must be non-negative."); + } + const std::size_t lhs = static_cast(first); + const std::size_t rhs = static_cast(second); + if (rhs != 0 && lhs > std::numeric_limits::max() / rhs) + { + throw std::overflow_error(std::string(label) + " dimensions overflow size_t."); + } + return lhs * rhs; +} + +std::size_t checked_product(const int first, const int second, const int third, const char* label) +{ + const std::size_t first_second = checked_product(first, second, label); + if (third < 0) + { + throw std::invalid_argument(std::string(label) + " dimensions must be non-negative."); + } + const std::size_t rhs = static_cast(third); + if (rhs != 0 && first_second > std::numeric_limits::max() / rhs) + { + throw std::overflow_error(std::string(label) + " dimensions overflow size_t."); + } + return first_second * rhs; +} + +void validate_metadata(const SternheimerGridDiagnosticMetadata& metadata) +{ + if (metadata.nx <= 0 || metadata.ny <= 0 || metadata.nz <= 0) + { + throw std::invalid_argument("Sternheimer grid diagnostic dimensions must be positive."); + } + if (metadata.spin < 0 || metadata.occupied < 0 || metadata.virtuals < 0 || metadata.auxiliaries < 0) + { + throw std::invalid_argument("Sternheimer grid diagnostic state counts must be non-negative."); + } + if (metadata.volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer grid diagnostic volume element must be positive."); + } +} + +void validate_operator_sizes(const SternheimerDeltaGridMatrices& matrices, const std::size_t expected_size) +{ + if (matrices.overlap.size() != expected_size || matrices.kinetic.size() != expected_size + || matrices.local_potential.size() != expected_size || matrices.nonlocal.size() != expected_size + || matrices.hamiltonian.size() != expected_size) + { + throw std::invalid_argument("Sternheimer grid diagnostic operator matrix dimensions do not match."); + } +} + +double relative_norm(const double difference_squared, const double reference_squared) +{ + const double difference = std::sqrt(difference_squared); + const double reference = std::sqrt(reference_squared); + return reference > 0.0 ? difference / reference : difference; +} + +void write_metadata(std::ofstream& output, const SternheimerGridDiagnosticMetadata& metadata, const char* kind) +{ + output << "ABACUS_STERNHEIMER_GRID_DIAGNOSTICS 1\n"; + output << "kind " << kind << '\n'; + output << "grid " << metadata.nx << ' ' << metadata.ny << ' ' << metadata.nz << '\n'; + output << "spin " << metadata.spin << '\n'; + output << "occupied " << metadata.occupied << '\n'; + output << "virtuals " << metadata.virtuals << '\n'; + output << "auxiliaries " << metadata.auxiliaries << '\n'; + output << "volume_element " << std::setprecision(17) << std::scientific << metadata.volume_element << '\n'; +} + +void write_column_major_matrix(std::ofstream& output, + const char* label, + const std::vector& matrix, + const int dimension) +{ + output << "matrix " << label << '\n'; + for (int row = 0; row != dimension; ++row) + { + for (int column = 0; column != dimension; ++column) + { + const Complex value = matrix[static_cast(row) + + static_cast(dimension) * static_cast(column)]; + output << row << ' ' << column << ' ' << value.real() << ' ' << value.imag() << '\n'; + } + } +} + +} // namespace + +SternheimerPerturbationTensor::SternheimerPerturbationTensor(const int occupied_count, + const int virtual_count, + const int auxiliary_count) + : occupied(occupied_count), virtuals(virtual_count), auxiliaries(auxiliary_count), + values(checked_product(occupied_count, virtual_count, auxiliary_count, "Sternheimer perturbation tensor"), + Complex(0.0, 0.0)) +{ +} + +SternheimerPerturbationTensor::Complex& SternheimerPerturbationTensor::at(const int occupied_index, + const int virtual_index, + const int auxiliary_index) +{ + return const_cast( + static_cast(*this).at(occupied_index, virtual_index, auxiliary_index)); +} + +const SternheimerPerturbationTensor::Complex& SternheimerPerturbationTensor::at(const int occupied_index, + const int virtual_index, + const int auxiliary_index) const +{ + if (occupied_index < 0 || occupied_index >= occupied || virtual_index < 0 || virtual_index >= virtuals + || auxiliary_index < 0 || auxiliary_index >= auxiliaries) + { + throw std::out_of_range("Sternheimer perturbation tensor index is out of range."); + } + const std::size_t index = (static_cast(occupied_index) * static_cast(virtuals) + + static_cast(virtual_index)) + * static_cast(auxiliaries) + + static_cast(auxiliary_index); + return values[index]; +} + +SternheimerLocalPerturbationTensor build_local_delta_perturbation_tensor( + const std::vector& virtual_functions, + const std::vector>& perturbation_potentials, + const std::vector& occupied_wavefunctions, + const double volume_element, + const int owner_rank, + const int owner_count) +{ + if (volume_element <= 0.0) + { + throw std::invalid_argument("Sternheimer perturbation tensor requires a positive grid volume element."); + } + if (owner_count <= 0 || owner_rank < 0 || owner_rank >= owner_count) + { + throw std::invalid_argument("Sternheimer perturbation tensor owner rank is invalid."); + } + if (virtual_functions.size() > static_cast(std::numeric_limits::max()) + || perturbation_potentials.size() > static_cast(std::numeric_limits::max()) + || occupied_wavefunctions.size() > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Sternheimer perturbation tensor dimensions exceed integer storage."); + } + + std::size_t grid_size = 0; + if (!occupied_wavefunctions.empty()) + { + grid_size = occupied_wavefunctions.front().size(); + } + else if (!virtual_functions.empty()) + { + grid_size = virtual_functions.front().values.size(); + } + else if (!perturbation_potentials.empty()) + { + grid_size = perturbation_potentials.front().size(); + } + for (const SternheimerFDHamiltonian::Vector& occupied: occupied_wavefunctions) + { + if (occupied.size() != grid_size) + { + throw std::invalid_argument("Sternheimer occupied grids have inconsistent dimensions."); + } + } + for (const SternheimerDeltaGridFunction& virtual_function: virtual_functions) + { + if (virtual_function.values.size() != grid_size) + { + throw std::invalid_argument("Sternheimer virtual grids have inconsistent dimensions."); + } + } + for (const std::vector& potential: perturbation_potentials) + { + if (potential.size() != grid_size) + { + throw std::invalid_argument("Sternheimer perturbation potential grids have inconsistent dimensions."); + } + } + + const int occupied_count = static_cast(occupied_wavefunctions.size()); + const int virtual_count = static_cast(virtual_functions.size()); + const int auxiliary_count = static_cast(perturbation_potentials.size()); + SternheimerLocalPerturbationTensor result; + result.tensor = SternheimerPerturbationTensor(occupied_count, virtual_count, auxiliary_count); + result.row_counts.assign(checked_product(occupied_count, auxiliary_count, "Sternheimer perturbation rows"), 0); + for (int occupied = 0; occupied != occupied_count; ++occupied) + { + for (int auxiliary = 0; auxiliary != auxiliary_count; ++auxiliary) + { + const std::size_t row = static_cast(occupied) * static_cast(auxiliary_count) + + static_cast(auxiliary); + if (static_cast(row % static_cast(owner_count)) != owner_rank) + { + continue; + } + result.row_counts[row] = 1; + for (int virtual_index = 0; virtual_index != virtual_count; ++virtual_index) + { + Complex value(0.0, 0.0); + for (std::size_t grid_index = 0; grid_index != grid_size; ++grid_index) + { + value += volume_element + * std::conj(virtual_functions[static_cast(virtual_index)].values[grid_index]) + * perturbation_potentials[static_cast(auxiliary)][grid_index] + * occupied_wavefunctions[static_cast(occupied)][grid_index]; + } + result.tensor.at(occupied, virtual_index, auxiliary) = value; + } + } + } + return result; +} + +double relative_component_reconstruction_error(const std::vector& total, + const std::vector& sos, + const std::vector& pulay, + const std::vector& qspace) +{ + if (total.size() != sos.size() || total.size() != pulay.size() || total.size() != qspace.size()) + { + throw std::invalid_argument("Sternheimer response component dimensions do not match."); + } + double difference_squared = 0.0; + double reference_squared = 0.0; + for (std::size_t index = 0; index != total.size(); ++index) + { + difference_squared += std::norm(total[index] - sos[index] - pulay[index] - qspace[index]); + reference_squared += std::norm(total[index]); + } + return relative_norm(difference_squared, reference_squared); +} + +double relative_operator_reconstruction_error(const SternheimerDeltaGridMatrices& matrices) +{ + const std::size_t size = matrices.hamiltonian.size(); + validate_operator_sizes(matrices, size); + double difference_squared = 0.0; + double reference_squared = 0.0; + for (std::size_t index = 0; index != size; ++index) + { + difference_squared += std::norm(matrices.hamiltonian[index] - matrices.kinetic[index] + - matrices.local_potential[index] - matrices.nonlocal[index]); + reference_squared += std::norm(matrices.hamiltonian[index]); + } + return relative_norm(difference_squared, reference_squared); +} + +void validate_grid_diagnostic_request(const bool diagnostics_enabled, + const bool write_librpa, + const bool use_delta_sternheimer, + const bool use_lcao_zero_order, + const bool use_ks_virtual_source) +{ + if (!diagnostics_enabled) + { + return; + } + if (!write_librpa) + { + throw std::invalid_argument("sternheimer_grid_diagnostics requires out_sternheimer_librpa=true."); + } + if (!use_delta_sternheimer) + { + throw std::invalid_argument("sternheimer_grid_diagnostics requires sternheimer_delta=true."); + } + if (!use_lcao_zero_order) + { + throw std::invalid_argument("sternheimer_grid_diagnostics requires the LCAO zero-order route."); + } + if (!use_ks_virtual_source) + { + throw std::invalid_argument("sternheimer_grid_diagnostics requires sternheimer_virt_src=ks_bands."); + } +} + +std::string sternheimer_component_v1_filename(const std::string& component, + const int iq, + const int ifrequency, + const int rank) +{ + if (component != "sos" && component != "pulay" && component != "qspace") + { + throw std::invalid_argument("Unknown Sternheimer response component: " + component); + } + if (iq <= 0 || ifrequency <= 0 || rank < 0) + { + throw std::invalid_argument("Sternheimer response component file indices are invalid."); + } + std::ostringstream filename; + filename << "v1_sternheimer_component_" << component << "_iq_" << iq << "_ifreq_" << ifrequency << "_rank" << rank + << ".dat"; + return filename.str(); +} + +void write_delta_grid_matrices(const std::string& path, + const SternheimerGridDiagnosticMetadata& metadata, + const SternheimerDeltaGridMatrices& matrices, + const std::vector& occupied_virtual_overlap, + const double reconstruction_tolerance) +{ + validate_metadata(metadata); + if (reconstruction_tolerance < 0.0) + { + throw std::invalid_argument("Sternheimer operator reconstruction tolerance must be non-negative."); + } + const std::size_t matrix_size + = checked_product(metadata.virtuals, metadata.virtuals, "Sternheimer grid diagnostic matrix"); + validate_operator_sizes(matrices, matrix_size); + if (occupied_virtual_overlap.size() + != checked_product(metadata.occupied, metadata.virtuals, "Sternheimer occupied-virtual overlap")) + { + throw std::invalid_argument("Sternheimer occupied-virtual overlap dimensions do not match."); + } + if (relative_operator_reconstruction_error(matrices) > reconstruction_tolerance) + { + throw std::runtime_error("Sternheimer grid operator components do not reconstruct the Hamiltonian."); + } + + std::ofstream output(path.c_str(), std::ios::out | std::ios::trunc); + if (!output) + { + throw std::runtime_error("Failed to open Sternheimer grid diagnostic output: " + path); + } + write_metadata(output, metadata, "delta_grid_matrices"); + output << "energy_unit Rydberg\n"; + output << "storage column_major\n"; + write_column_major_matrix(output, "overlap", matrices.overlap, metadata.virtuals); + write_column_major_matrix(output, "kinetic", matrices.kinetic, metadata.virtuals); + write_column_major_matrix(output, "local_potential", matrices.local_potential, metadata.virtuals); + write_column_major_matrix(output, "nonlocal", matrices.nonlocal, metadata.virtuals); + write_column_major_matrix(output, "hamiltonian", matrices.hamiltonian, metadata.virtuals); + output << "matrix occupied_virtual_overlap\n"; + for (int occupied = 0; occupied != metadata.occupied; ++occupied) + { + for (int virtual_index = 0; virtual_index != metadata.virtuals; ++virtual_index) + { + const Complex value + = occupied_virtual_overlap[static_cast(occupied) * metadata.virtuals + virtual_index]; + output << occupied << ' ' << virtual_index << ' ' << value.real() << ' ' << value.imag() << '\n'; + } + } + if (!output) + { + throw std::runtime_error("Failed to write Sternheimer grid diagnostic output: " + path); + } +} + +void write_delta_perturbation_tensor(const std::string& path, + const SternheimerGridDiagnosticMetadata& metadata, + const SternheimerPerturbationTensor& tensor) +{ + validate_metadata(metadata); + if (tensor.occupied != metadata.occupied || tensor.virtuals != metadata.virtuals + || tensor.auxiliaries != metadata.auxiliaries + || tensor.values.size() + != checked_product(tensor.occupied, + tensor.virtuals, + tensor.auxiliaries, + "Sternheimer perturbation tensor")) + { + throw std::invalid_argument("Sternheimer perturbation tensor dimensions do not match metadata."); + } + + std::ofstream output(path.c_str(), std::ios::out | std::ios::trunc); + if (!output) + { + throw std::runtime_error("Failed to open Sternheimer perturbation diagnostic output: " + path); + } + write_metadata(output, metadata, "delta_perturbation_tensor"); + output << "potential_unit Rydberg\n"; + output << "storage row_major\n"; + output << "tensor perturbation occupied virtual auxiliary\n"; + for (int occupied = 0; occupied != tensor.occupied; ++occupied) + { + for (int virtual_index = 0; virtual_index != tensor.virtuals; ++virtual_index) + { + for (int auxiliary = 0; auxiliary != tensor.auxiliaries; ++auxiliary) + { + const Complex value = tensor.at(occupied, virtual_index, auxiliary); + output << occupied << ' ' << virtual_index << ' ' << auxiliary << ' ' << value.real() << ' ' + << value.imag() << '\n'; + } + } + } + if (!output) + { + throw std::runtime_error("Failed to write Sternheimer perturbation diagnostic output: " + path); + } +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.h new file mode 100644 index 00000000000..7179c93cc21 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.h @@ -0,0 +1,104 @@ +#ifndef STERNHEIMER_GRID_DIAGNOSTICS_H +#define STERNHEIMER_GRID_DIAGNOSTICS_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_delta.h" + +#include +#include +#include + +namespace ModuleRI +{ + +struct SternheimerGridDiagnosticMetadata +{ + int nx = 0; + int ny = 0; + int nz = 0; + int spin = 0; + int occupied = 0; + int virtuals = 0; + int auxiliaries = 0; + double volume_element = 0.0; +}; + +struct SternheimerPerturbationTensor +{ + using Complex = std::complex; + + SternheimerPerturbationTensor() = default; + SternheimerPerturbationTensor(int occupied_count, int virtual_count, int auxiliary_count); + + Complex& at(int occupied_index, int virtual_index, int auxiliary_index); + const Complex& at(int occupied_index, int virtual_index, int auxiliary_index) const; + + int occupied = 0; + int virtuals = 0; + int auxiliaries = 0; + std::vector values; +}; + +struct SternheimerLocalPerturbationTensor +{ + SternheimerPerturbationTensor tensor; + std::vector row_counts; +}; + +struct SternheimerAOPotentials +{ + int nao = 0; + int naux = 0; + // Channel-major, row-major AO matrices in Hartree. + std::vector> values; +}; + +SternheimerAOPotentials read_sternheimer_ao_potentials(const std::string& path, int nao, int naux); + +SternheimerPerturbationTensor contract_sternheimer_ao_potentials( + const SternheimerAOPotentials& potentials, + const std::vector>>& occupied_coefficients, + const std::vector>>& virtual_coefficients); + +void accumulate_sternheimer_analytic_ao_column(const SternheimerPerturbationTensor& couplings_ha, + int occupied_index, + const std::vector>& response_coefficients, + double occupation, + int channel, + std::vector>& branch); + +SternheimerLocalPerturbationTensor build_local_delta_perturbation_tensor( + const std::vector& virtual_functions, + const std::vector>& perturbation_potentials, + const std::vector& occupied_wavefunctions, + double volume_element, + int owner_rank, + int owner_count); + +double relative_component_reconstruction_error(const std::vector>& total, + const std::vector>& sos, + const std::vector>& pulay, + const std::vector>& qspace); + +double relative_operator_reconstruction_error(const SternheimerDeltaGridMatrices& matrices); + +void validate_grid_diagnostic_request(bool diagnostics_enabled, + bool write_librpa, + bool use_delta_sternheimer, + bool use_lcao_zero_order, + bool use_ks_virtual_source); + +std::string sternheimer_component_v1_filename(const std::string& component, int iq, int ifrequency, int rank); + +void write_delta_grid_matrices(const std::string& path, + const SternheimerGridDiagnosticMetadata& metadata, + const SternheimerDeltaGridMatrices& matrices, + const std::vector>& occupied_virtual_overlap, + double reconstruction_tolerance); + +void write_delta_perturbation_tensor(const std::string& path, + const SternheimerGridDiagnosticMetadata& metadata, + const SternheimerPerturbationTensor& tensor); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.cpp new file mode 100644 index 00000000000..669609b4ac1 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.cpp @@ -0,0 +1,595 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Grid = ModuleRI::SternheimerGridTransfer::Grid; +using Vector = ModuleRI::SternheimerGridTransfer::Vector; +using Complex = ModuleRI::SternheimerFDHamiltonian::Complex; +using Lattice = ModuleRI::SternheimerFDLatticeVectors; + +struct GridInfo +{ + std::size_t count; + Lattice lattice; +}; + +GridInfo validate_grid(const Grid& grid) +{ + if (!grid.periodic) + { + throw std::invalid_argument("Sternheimer grid transfer requires periodic grids."); + } + std::size_t count = 1; + for (const int dimension : {grid.nx, grid.ny, grid.nz}) + { + if (dimension <= 0 + || count > static_cast(std::numeric_limits::max()) + / static_cast(dimension)) + { + throw std::invalid_argument("Sternheimer grid transfer has invalid or overflowing grid dimensions."); + } + count *= static_cast(dimension); + } + for (const double spacing : {grid.hx, grid.hy, grid.hz}) + { + if (!std::isfinite(spacing) || spacing <= 0.0) + { + throw std::invalid_argument("Sternheimer grid transfer requires finite positive grid spacings."); + } + } + for (const double k : grid.kpoint) + { + if (!std::isfinite(k)) + { + throw std::invalid_argument("Sternheimer grid transfer requires a finite Bloch k point."); + } + } + const Lattice cell = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + for (const auto& row : cell) + { + for (const double entry : row) + { + if (!std::isfinite(entry)) + { + throw std::invalid_argument("Sternheimer grid transfer requires a finite lattice."); + } + } + } + const double determinant + = cell[0][0] * (cell[1][1] * cell[2][2] - cell[1][2] * cell[2][1]) + - cell[0][1] * (cell[1][0] * cell[2][2] - cell[1][2] * cell[2][0]) + + cell[0][2] * (cell[1][0] * cell[2][1] - cell[1][1] * cell[2][0]); + if (!std::isfinite(determinant) || determinant == 0.0) + { + throw std::invalid_argument("Sternheimer grid transfer requires a nonsingular finite-volume lattice."); + } + return {count, cell}; +} + +struct PairInfo +{ + std::size_t coarse_count; + std::size_t fine_count; + bool identity; +}; + +PairInfo validate_pair(const Grid& coarse, const Grid& fine) +{ + const GridInfo coarse_info = validate_grid(coarse); + const GridInfo fine_info = validate_grid(fine); + if (fine.nx < coarse.nx || fine.ny < coarse.ny || fine.nz < coarse.nz) + { + throw std::invalid_argument("Sternheimer grid transfer fine dimensions must not be smaller than coarse."); + } + constexpr double tolerance = 1.0e-12; + for (int i = 0; i != 3; ++i) + { + if (std::abs(coarse.kpoint[i] - fine.kpoint[i]) > tolerance) + { + throw std::invalid_argument("Sternheimer grid transfer requires the same explicit Bloch k point."); + } + for (int j = 0; j != 3; ++j) + { + const double left = coarse_info.lattice[i][j]; + const double right = fine_info.lattice[i][j]; + if (std::abs(left - right) > tolerance * std::max({1.0, std::abs(left), std::abs(right)})) + { + throw std::invalid_argument("Sternheimer grid transfer requires the same physical lattice."); + } + } + } + return {coarse_info.count, fine_info.count, + coarse.nx == fine.nx && coarse.ny == fine.ny && coarse.nz == fine.nz}; +} + +std::size_t numeric_workspace_bytes(const PairInfo& grids, const bool spectral = false) +{ + if (grids.identity && !spectral) + { + return 0; + } + std::size_t total = 0; + const auto append = [&total](const std::size_t count, const std::size_t bytes_per_item) { + if (count > (std::numeric_limits::max() - total) / bytes_per_item) + { + throw std::invalid_argument("Sternheimer grid transfer workspace size overflows."); + } + total += count * bytes_per_item; + }; + append(grids.coarse_count, sizeof(fftw_complex) + sizeof(Complex)); + append(grids.fine_count, sizeof(fftw_complex) + sizeof(Complex)); + append(grids.coarse_count, sizeof(std::size_t)); + return total; +} + +std::size_t index(const Grid& grid, const int ix, const int iy, const int iz) +{ + return (static_cast(ix) * grid.ny + iy) * grid.nz + iz; +} + +int embedded_index(const int coarse_index, const int coarse_dimension, const int fine_dimension) +{ + const int frequency = coarse_index <= coarse_dimension / 2 ? coarse_index : coarse_index - coarse_dimension; + return frequency < 0 ? frequency + fine_dimension : frequency; +} + +Lattice spectral_reciprocal_vectors(const Grid& grid) +{ + Lattice reciprocal = ModuleRI::sternheimer_fd_grid_dual_vectors(grid); + const double two_pi = 2.0 * std::acos(-1.0); + for (auto& row : reciprocal) + { + for (double& value : row) + { + value *= two_pi; + } + } + const std::array dimensions{grid.nx, grid.ny, grid.nz}; + double maximum_squared = 0.0; + for (int axis = 0; axis != 3; ++axis) + { + double maximum = 0.0; + for (int direction = 0; direction != 3; ++direction) + { + maximum += std::abs(reciprocal[direction][axis]) + * (std::abs(grid.kpoint[direction]) + dimensions[direction] / 2); + } + maximum_squared += maximum * maximum; + } + if (!std::isfinite(maximum_squared)) + { + throw std::invalid_argument("Sternheimer grid transfer requires finite spectral derivative symbols."); + } + return reciprocal; +} + +std::array spectral_wavevector(const Grid& grid, const Lattice& reciprocal, const std::size_t i) +{ + const std::array dimensions{grid.nx, grid.ny, grid.nz}; + const std::array indices{static_cast(i / (static_cast(grid.ny) * grid.nz)), + static_cast((i / grid.nz) % grid.ny), + static_cast(i % grid.nz)}; + std::array result{}; + for (int direction = 0; direction != 3; ++direction) + { + const int frequency = indices[direction] <= dimensions[direction] / 2 + ? indices[direction] : indices[direction] - dimensions[direction]; + // Keep the explicit k label: reducing k modulo a grid dimension would + // preserve nodal phases but give the wrong analytic derivative. + const double shifted = frequency + grid.kpoint[direction]; + for (int axis = 0; axis != 3; ++axis) + { + result[axis] += shifted * reciprocal[direction][axis]; + } + } + return result; +} + +void validate_spectral_input(const Vector& input, const std::size_t count) +{ + if (input.size() != count) + { + throw std::invalid_argument("Sternheimer spectral transfer input size does not match its grid."); + } + for (const Complex value : input) + { + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + { + throw std::invalid_argument("Sternheimer spectral transfer requires finite input fields."); + } + } +} + +Vector bloch_phases(const Grid& grid, const std::size_t count) +{ + Vector phases(count); + const double two_pi = 2.0 * std::acos(-1.0); + // Integer multiples of a dimension do not change phases at that grid's nodes. + const std::array k{std::remainder(grid.kpoint[0], static_cast(grid.nx)), + std::remainder(grid.kpoint[1], static_cast(grid.ny)), + std::remainder(grid.kpoint[2], static_cast(grid.nz))}; + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const double phase = two_pi * (k[0] * (static_cast(ix) / grid.nx) + + k[1] * (static_cast(iy) / grid.ny) + + k[2] * (static_cast(iz) / grid.nz)); + phases[index(grid, ix, iy, iz)] = std::exp(Complex(0.0, phase)); + } + } + } + return phases; +} + +struct FFTGrid +{ + fftw_complex* data = nullptr; + fftw_plan forward = nullptr; + fftw_plan backward = nullptr; + + FFTGrid(const Grid& grid, const std::size_t count) + { + data = fftw_alloc_complex(count); + if (data == nullptr) + { + throw std::bad_alloc(); + } + for (std::size_t i = 0; i != count; ++i) + { + data[i][0] = 0.0; + data[i][1] = 0.0; + } +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + forward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, data, data, FFTW_FORWARD, FFTW_ESTIMATE); + backward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, data, data, FFTW_BACKWARD, FFTW_ESTIMATE); + } + if (forward == nullptr || backward == nullptr) + { + release(); + throw std::runtime_error("Failed to create Sternheimer grid transfer FFTW plans."); + } + } + + ~FFTGrid() + { + release(); + } + + FFTGrid(const FFTGrid&) = delete; + FFTGrid& operator=(const FFTGrid&) = delete; + + void release() + { +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + if (forward != nullptr) + { + fftw_destroy_plan(forward); + } + if (backward != nullptr) + { + fftw_destroy_plan(backward); + } + } + fftw_free(data); + data = nullptr; + forward = nullptr; + backward = nullptr; + } +}; + +} // namespace + +namespace ModuleRI +{ + +struct SternheimerGridTransfer::Impl +{ + Grid coarse_grid; + Grid fine_grid; + PairInfo grids; + Vector coarse_phases; + Vector fine_phases; + std::vector coarse_to_fine; + std::unique_ptr coarse_fft; + std::unique_ptr fine_fft; + + Impl(const Grid& coarse, const Grid& fine) + : coarse_grid(coarse), fine_grid(fine), grids(validate_pair(coarse, fine)) + { + numeric_workspace_bytes(grids); + if (grids.identity) + { + return; + } + initialize_fft(); + } + + void initialize_fft() + { + if (coarse_fft != nullptr) + { + return; + } + // Build once, including lazy same-grid spectral use. Locals ensure a + // failed FFT allocation leaves no partial workspace to duplicate on retry. + const Grid& coarse = coarse_grid; + const Grid& fine = fine_grid; + Vector new_coarse_phases = bloch_phases(coarse, grids.coarse_count); + Vector new_fine_phases = bloch_phases(fine, grids.fine_count); + std::vector new_map(grids.coarse_count); + for (int ix = 0; ix != coarse.nx; ++ix) + { + for (int iy = 0; iy != coarse.ny; ++iy) + { + for (int iz = 0; iz != coarse.nz; ++iz) + { + new_map[index(coarse, ix, iy, iz)] + = index(fine, embedded_index(ix, coarse.nx, fine.nx), + embedded_index(iy, coarse.ny, fine.ny), + embedded_index(iz, coarse.nz, fine.nz)); + } + } + } + auto new_coarse_fft = std::make_unique(coarse, grids.coarse_count); + auto new_fine_fft = std::make_unique(fine, grids.fine_count); + coarse_phases = std::move(new_coarse_phases); + fine_phases = std::move(new_fine_phases); + coarse_to_fine = std::move(new_map); + coarse_fft = std::move(new_coarse_fft); + fine_fft = std::move(new_fine_fft); + } + + Lattice prepare_spectral(const std::size_t budget) + { + const Lattice reciprocal = spectral_reciprocal_vectors(coarse_grid); + if (numeric_workspace_bytes(grids, true) > budget) + { + throw std::length_error("Sternheimer spectral transfer numeric workspace exceeds its budget."); + } + initialize_fft(); + return reciprocal; + } + + static void forward_field(const Vector& input, const Vector& phases, FFTGrid& fft) + { + for (std::size_t i = 0; i != input.size(); ++i) + { + const Complex periodic = std::conj(phases[i]) * input[i]; + fft.data[i][0] = periodic.real(); + fft.data[i][1] = periodic.imag(); + } + fftw_execute(fft.forward); + } + + static void backward_field(FFTGrid& fft, const Vector& phases, Vector& output) + { + fftw_execute(fft.backward); + output.resize(phases.size()); + for (std::size_t i = 0; i != output.size(); ++i) + { + output[i] = phases[i] * Complex(fft.data[i][0], fft.data[i][1]); + if (!std::isfinite(output[i].real()) || !std::isfinite(output[i].imag())) + { + throw std::overflow_error("Sternheimer spectral transfer produced a nonfinite result."); + } + } + } + + void interpolate_with_gradients(const Vector& input, Vector& values, + std::array& gradients, const std::size_t budget) + { + for (const auto& field : gradients) + { + if (&values == &field) + { + throw std::invalid_argument("Sternheimer fine values and gradient outputs must be distinct vectors."); + } + } + validate_spectral_input(input, grids.coarse_count); + const Lattice reciprocal = prepare_spectral(budget); + // Preserve the input spectrum before writing any possibly aliased output. + forward_field(input, coarse_phases, *coarse_fft); + const double normalization = 1.0 / static_cast(grids.coarse_count); + for (int component = -1; component != 3; ++component) + { + if (component == -1 && grids.identity) + { + values = input; + continue; + } + for (std::size_t i = 0; i != grids.fine_count; ++i) + { + fine_fft->data[i][0] = 0.0; + fine_fft->data[i][1] = 0.0; + } + for (std::size_t i = 0; i != grids.coarse_count; ++i) + { + const Complex coefficient(coarse_fft->data[i][0], coarse_fft->data[i][1]); + const Complex factor = component == -1 ? Complex(normalization, 0.0) + : Complex(0.0, normalization * spectral_wavevector(coarse_grid, reciprocal, i)[component]); + const Complex transformed = factor * coefficient; + const std::size_t target = coarse_to_fine[i]; + fine_fft->data[target][0] = transformed.real(); + fine_fft->data[target][1] = transformed.imag(); + } + backward_field(*fine_fft, fine_phases, component == -1 ? values : gradients[component]); + } + } + + void restrict_gradient_adjoint(const std::array& fields, Vector& output, const std::size_t budget) + { + for (const auto& field : fields) + { + validate_spectral_input(field, grids.fine_count); + } + const Lattice reciprocal = prepare_spectral(budget); + for (std::size_t i = 0; i != grids.coarse_count; ++i) + { + coarse_fft->data[i][0] = 0.0; + coarse_fft->data[i][1] = 0.0; + } + const double normalization = 1.0 / static_cast(grids.fine_count); + for (int component = 0; component != 3; ++component) + { + forward_field(fields[component], fine_phases, *fine_fft); + for (std::size_t i = 0; i != grids.coarse_count; ++i) + { + const std::size_t source = coarse_to_fine[i]; + const Complex coefficient(fine_fft->data[source][0], fine_fft->data[source][1]); + const Complex factor(0.0, -normalization * spectral_wavevector(coarse_grid, reciprocal, i)[component]); + const Complex transformed = factor * coefficient; + coarse_fft->data[i][0] += transformed.real(); + coarse_fft->data[i][1] += transformed.imag(); + } + } + // Consume all three input fields before an aliased output can overwrite one. + backward_field(*coarse_fft, coarse_phases, output); + } + + void apply_negative_laplacian(const Vector& input, Vector& output, const std::size_t budget) + { + validate_spectral_input(input, grids.coarse_count); + const Lattice reciprocal = prepare_spectral(budget); + forward_field(input, coarse_phases, *coarse_fft); + const double normalization = 1.0 / static_cast(grids.coarse_count); + for (std::size_t i = 0; i != grids.coarse_count; ++i) + { + const auto q = spectral_wavevector(coarse_grid, reciprocal, i); + const double factor = normalization * (q[0] * q[0] + q[1] * q[1] + q[2] * q[2]); + coarse_fft->data[i][0] *= factor; + coarse_fft->data[i][1] *= factor; + } + backward_field(*coarse_fft, coarse_phases, output); + } + + void apply(const Vector& input, Vector& output, const bool interpolate) + { + const std::size_t input_size = interpolate ? grids.coarse_count : grids.fine_count; + const std::size_t output_size = interpolate ? grids.fine_count : grids.coarse_count; + if (input.size() != input_size) + { + throw std::invalid_argument("Sternheimer grid transfer input vector size does not match its grid."); + } + if (grids.identity) + { + output = input; + return; + } + FFTGrid& source = interpolate ? *coarse_fft : *fine_fft; + FFTGrid& target = interpolate ? *fine_fft : *coarse_fft; + const Vector& source_phases = interpolate ? coarse_phases : fine_phases; + const Vector& target_phases = interpolate ? fine_phases : coarse_phases; + for (std::size_t i = 0; i != input_size; ++i) + { + const Complex periodic = std::conj(source_phases[i]) * input[i]; + source.data[i][0] = periodic.real(); + source.data[i][1] = periodic.imag(); + } + fftw_execute(source.forward); + for (std::size_t i = 0; i != output_size; ++i) + { + target.data[i][0] = 0.0; + target.data[i][1] = 0.0; + } + // FFTW is unnormalized: 1/Nc gives J, while 1/Nf gives its dV-weighted adjoint. + const double normalization = 1.0 / static_cast(input_size); + for (std::size_t i = 0; i != grids.coarse_count; ++i) + { + const std::size_t from = interpolate ? i : coarse_to_fine[i]; + const std::size_t to = interpolate ? coarse_to_fine[i] : i; + target.data[to][0] = normalization * source.data[from][0]; + target.data[to][1] = normalization * source.data[from][1]; + } + fftw_execute(target.backward); + output.resize(output_size); + for (std::size_t i = 0; i != output_size; ++i) + { + output[i] = target_phases[i] * Complex(target.data[i][0], target.data[i][1]); + } + } +}; + +SternheimerGridTransfer::SternheimerGridTransfer(const Grid& coarse_grid, const Grid& fine_grid) + : impl_(std::make_unique(coarse_grid, fine_grid)) +{ +} + +SternheimerGridTransfer::~SternheimerGridTransfer() = default; +SternheimerGridTransfer::SternheimerGridTransfer(SternheimerGridTransfer&&) noexcept = default; +SternheimerGridTransfer& SternheimerGridTransfer::operator=(SternheimerGridTransfer&&) noexcept = default; + +const SternheimerGridTransfer::Grid& SternheimerGridTransfer::coarse_grid() const +{ + return impl_->coarse_grid; +} + +const SternheimerGridTransfer::Grid& SternheimerGridTransfer::fine_grid() const +{ + return impl_->fine_grid; +} + +void SternheimerGridTransfer::interpolate(const Vector& coarse, Vector& fine) +{ + impl_->apply(coarse, fine, true); +} + +void SternheimerGridTransfer::restrict_adjoint(const Vector& fine, Vector& coarse) +{ + impl_->apply(fine, coarse, false); +} + +void SternheimerGridTransfer::interpolate_with_gradients(const Vector& coarse, Vector& fine_values, + std::array& fine_gradients, + const std::size_t max_workspace_bytes) +{ + impl_->interpolate_with_gradients(coarse, fine_values, fine_gradients, max_workspace_bytes); +} + +void SternheimerGridTransfer::restrict_gradient_adjoint(const std::array& fine_fields, Vector& coarse, + const std::size_t max_workspace_bytes) +{ + impl_->restrict_gradient_adjoint(fine_fields, coarse, max_workspace_bytes); +} + +void SternheimerGridTransfer::apply_negative_laplacian(const Vector& coarse, Vector& result, + const std::size_t max_workspace_bytes) +{ + impl_->apply_negative_laplacian(coarse, result, max_workspace_bytes); +} + +std::size_t SternheimerGridTransfer::workspace_bytes_required(const Grid& coarse_grid, const Grid& fine_grid) +{ + return numeric_workspace_bytes(validate_pair(coarse_grid, fine_grid)); +} + +std::size_t SternheimerGridTransfer::gradient_workspace_bytes_required(const Grid& coarse_grid, const Grid& fine_grid) +{ + const PairInfo grids = validate_pair(coarse_grid, fine_grid); + spectral_reciprocal_vectors(coarse_grid); + return numeric_workspace_bytes(grids, true); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h new file mode 100644 index 00000000000..793f1658908 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h @@ -0,0 +1,97 @@ +#ifndef STERNHEIMER_GRID_TRANSFER_H +#define STERNHEIMER_GRID_TRANSFER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include +#include +#include + +namespace ModuleRI +{ + +// Transfers full complex Bloch fields on periodic, z-fast grids of the same cell and k. +// Modes use i <= N/2 ? i : i-N: the unique even-grid Nyquist mode is positive. +// No conjugate pairing, Nyquist merging, or wrapping of k to another sector is performed. +// J is isometric for dV = |det(cell)|/N; restrict_adjoint is its weighted adjoint J*. +// This interpolation does not recover modes outside the chosen coarse Fourier space. +// Each worker must own its instance: execution is non-const and is NOT reentrant, +// even with distinct output vectors. A shared Hamiltonian must use worker-owned +// transfer contexts, not one shared mutable transfer. FFT execution takes no lock. +// Const grid accessors and the allocation-free workspace estimate do not mutate state. +// Plan management uses the shared OpenMP FFTW critical region. Without OpenMP, +// callers must serialize construction/destruction across native threads. +class SternheimerGridTransfer +{ + public: + using Grid = SternheimerFDHamiltonian::Grid; + using Vector = SternheimerFDHamiltonian::Vector; + + // Fine dimensions must be >= coarse dimensions in every direction. Effective + // lattice entries and k must agree to relative/absolute 1e-12, respectively. + // Invalid geometry, nonperiodic grids and point counts exceeding INT_MAX throw. + SternheimerGridTransfer(const Grid& coarse_grid, const Grid& fine_grid); + ~SternheimerGridTransfer(); + SternheimerGridTransfer(SternheimerGridTransfer&&) noexcept; + SternheimerGridTransfer& operator=(SternheimerGridTransfer&&) noexcept; + SternheimerGridTransfer(const SternheimerGridTransfer&) = delete; + SternheimerGridTransfer& operator=(const SternheimerGridTransfer&) = delete; + + const Grid& coarse_grid() const; + const Grid& fine_grid() const; + + // Output is resized; input and output may refer to the same vector. + // Equal-size grids take an exact copy path, without FFTs or phase roundoff. + void interpolate(const Vector& coarse, Vector& fine); + void restrict_adjoint(const Vector& fine, Vector& coarse); + + // D_a = partial_a J uses i(G+k)_a in Cartesian coordinates, with reciprocal + // lattice vectors including 2*pi. Gradients differentiate the FULL Bloch + // field, not just its periodic part. Same-grid values are copied exactly, + // but same-grid derivatives still use Fourier transforms. The positive + // Nyquist choice makes no q-star/time-reversal covariance claim. + // Input may alias fine_values or one gradient. fine_values must not alias + // any fine_gradients entry. All outputs are resized. New spectral operations + // reject wrong-sized/nonfinite inputs and insufficient budgets before writing + // outputs. The budget bounds internal numeric payload, excluding outputs. + // Arithmetic overflow throws overflow_error; outputs may be partial then. + void interpolate_with_gradients(const Vector& coarse, + Vector& fine_values, + std::array& fine_gradients, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024); + + // Sum_a D_a* f_a with the dV-weighted adjoint: multiplier -i(G+k)_a + // and fine-grid Fourier normalization 1/Nf. Output may alias any input. + void restrict_gradient_adjoint(const std::array& fine_fields, + Vector& coarse, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024); + + // Analytic coarse-space -Laplacian, with symbol |G+k|^2 and no kinetic + // prefactor. Equals sum_a D_a* D_a, NOT the FD kinetic operator. Uses only + // coarse FFT execution, no fine gradient arrays. Input/output may alias. + void apply_negative_laplacian(const Vector& coarse, + Vector& result, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024); + + // Allocation-free validation and numeric payload estimate: two FFT buffers, + // two Bloch-phase arrays, and one coarse-to-fine index map. Identity needs zero. + // Excludes output vectors, opaque FFTW plans, allocator and small object overhead; + // this is not a process-memory bound or a constructor memory-budget guarantee. + static std::size_t workspace_bytes_required(const Grid& coarse_grid, const Grid& fine_grid); + + // Total internal numeric payload after enabling ANY spectral operation above. + // Nonidentity reuses the value-transfer workspace with no extra arrays. + // Identity lazily allocates the same two FFT buffers, phase arrays and index + // map; those stay owned by the instance. Use this estimate instead of the + // value-only estimate thereafter. Exclusions are the same as above; four + // fine output fields are caller-owned and are NOT part of this estimate. + static std::size_t gradient_workspace_bytes_required(const Grid& coarse_grid, const Grid& fine_grid); + + private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_kq.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_kq.cpp new file mode 100644 index 00000000000..9d3c1856c27 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_kq.cpp @@ -0,0 +1,140 @@ +#include "sternheimer_kq.h" + +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +void validate_finite_kpoint(const SternheimerReducedKPoint& kpoint, const char* name) +{ + for (const double coordinate: kpoint) + { + if (!std::isfinite(coordinate)) + { + throw std::invalid_argument(std::string("Sternheimer ") + name + " coordinates must be finite."); + } + } +} + +} // namespace + +SternheimerFoldedKPoint fold_sternheimer_kpoint(const SternheimerReducedKPoint& kpoint) +{ + validate_finite_kpoint(kpoint, "k-point"); + + SternheimerFoldedKPoint result{}; + for (std::size_t direction = 0; direction != kpoint.size(); ++direction) + { + const double shift = std::floor(kpoint[direction] + 0.5); + if (shift < static_cast(std::numeric_limits::min()) + || shift > static_cast(std::numeric_limits::max())) + { + throw std::invalid_argument("Sternheimer reciprocal-lattice shift exceeds integer range."); + } + result.kpoint[direction] = kpoint[direction] - shift; + result.reciprocal_shift[direction] = static_cast(shift); + } + return result; +} + +double periodic_sternheimer_kpoint_distance(const SternheimerReducedKPoint& lhs, const SternheimerReducedKPoint& rhs) +{ + validate_finite_kpoint(lhs, "left k-point"); + validate_finite_kpoint(rhs, "right k-point"); + + double distance = 0.0; + for (std::size_t direction = 0; direction != lhs.size(); ++direction) + { + const double difference = lhs[direction] - rhs[direction]; + distance = std::max(distance, std::abs(difference - std::round(difference))); + } + return distance; +} + +std::complex sternheimer_bloch_phase(const SternheimerReducedKPoint& kpoint, + const std::array& lattice_translation) +{ + validate_finite_kpoint(kpoint, "Bloch k-point"); + + double reduced_phase = 0.0; + for (std::size_t direction = 0; direction != kpoint.size(); ++direction) + { + reduced_phase += kpoint[direction] * lattice_translation[direction]; + } + const double argument = 2.0 * std::acos(-1.0) * reduced_phase; + return {std::cos(argument), std::sin(argument)}; +} + +std::vector build_sternheimer_kq_map(const std::vector& kpoints, + const SternheimerReducedKPoint& qpoint, + const double tolerance) +{ + if (kpoints.empty()) + { + throw std::invalid_argument("Sternheimer k+q mapping requires a nonempty k-point mesh."); + } + if (!std::isfinite(tolerance) || tolerance <= 0.0) + { + throw std::invalid_argument("Sternheimer k+q mapping tolerance must be positive and finite."); + } + validate_finite_kpoint(qpoint, "q-point"); + for (const auto& kpoint: kpoints) + { + validate_finite_kpoint(kpoint, "mesh k-point"); + } + + std::vector mapping; + mapping.reserve(kpoints.size()); + for (std::size_t source_index = 0; source_index != kpoints.size(); ++source_index) + { + SternheimerReducedKPoint k_plus_q{}; + for (std::size_t direction = 0; direction != k_plus_q.size(); ++direction) + { + k_plus_q[direction] = kpoints[source_index][direction] + qpoint[direction]; + } + const SternheimerFoldedKPoint folded = fold_sternheimer_kpoint(k_plus_q); + + int target_index = -1; + for (std::size_t candidate_index = 0; candidate_index != kpoints.size(); ++candidate_index) + { + if (periodic_sternheimer_kpoint_distance(folded.kpoint, kpoints[candidate_index]) <= tolerance) + { + if (target_index >= 0) + { + throw std::invalid_argument("Sternheimer k+q mapping found duplicate periodic target k-points."); + } + target_index = static_cast(candidate_index); + } + } + if (target_index < 0) + { + throw std::invalid_argument("Sternheimer q-point is not commensurate with the supplied k-point mesh."); + } + + const SternheimerReducedKPoint& selected_target = kpoints[static_cast(target_index)]; + std::array reciprocal_shift{}; + for (std::size_t direction = 0; direction != k_plus_q.size(); ++direction) + { + const double shift = k_plus_q[direction] - selected_target[direction]; + const double rounded_shift = std::round(shift); + if (std::abs(shift - rounded_shift) > tolerance + || rounded_shift < static_cast(std::numeric_limits::min()) + || rounded_shift > static_cast(std::numeric_limits::max())) + { + throw std::runtime_error( + "Sternheimer k+q mapping could not express the selected target by a reciprocal-lattice shift."); + } + reciprocal_shift[direction] = static_cast(rounded_shift); + } + mapping.push_back({static_cast(source_index), target_index, selected_target, reciprocal_shift}); + } + return mapping; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_kq.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_kq.h new file mode 100644 index 00000000000..499d358e4d6 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_kq.h @@ -0,0 +1,41 @@ +#ifndef MODULE_RI_STERNHEIMER_KQ_H +#define MODULE_RI_STERNHEIMER_KQ_H + +#include +#include +#include + +namespace ModuleRI +{ + +using SternheimerReducedKPoint = std::array; + +struct SternheimerFoldedKPoint +{ + SternheimerReducedKPoint kpoint; + std::array reciprocal_shift; +}; + +struct SternheimerKQPair +{ + int source_index; + int target_index; + SternheimerReducedKPoint folded_k_plus_q; + std::array reciprocal_shift; +}; + +SternheimerFoldedKPoint fold_sternheimer_kpoint(const SternheimerReducedKPoint& kpoint); + +double periodic_sternheimer_kpoint_distance(const SternheimerReducedKPoint& lhs, const SternheimerReducedKPoint& rhs); + +// ABACUS folds real-space matrices with exp(+i 2 pi k.R). +std::complex sternheimer_bloch_phase(const SternheimerReducedKPoint& kpoint, + const std::array& lattice_translation); + +std::vector build_sternheimer_kq_map(const std::vector& kpoints, + const SternheimerReducedKPoint& qpoint, + double tolerance = 1.0e-10); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_lapack_gesv.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_lapack_gesv.h new file mode 100644 index 00000000000..ba025bf00b3 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_lapack_gesv.h @@ -0,0 +1,28 @@ +#ifndef MODULE_DELTA_ST_STERNHEIMER_LAPACK_GESV_H +#define MODULE_DELTA_ST_STERNHEIMER_LAPACK_GESV_H + +// The Delta-Sternheimer solver needs the complex general linear solver +// ZGESV, which master_ghj declares in +// source_base/module_external/lapack_connector.h. The base branch of this +// worktree does not declare it there yet, so the declaration is kept local +// to module_delta_st to avoid depending on an unrelated core header change. +// +// When lapack_connector.h gains the upstream ZGESV declaration, this header +// (and the two includes of it) can be dropped without changing behaviour: +// the declaration below matches the upstream one exactly. + +#include + +extern "C" +{ +void zgesv_(const int* n, + const int* nrhs, + std::complex* a, + const int* lda, + int* ipiv, + std::complex* b, + const int* ldb, + int* info); +} + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.cpp new file mode 100644 index 00000000000..a6f38a5f708 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.cpp @@ -0,0 +1,307 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +namespace +{ +std::vector fixed_subspace_views( + const std::vector& occupied, + const std::vector& virtual_states) +{ + std::vector views; + views.reserve(occupied.size() + virtual_states.size()); + // Match the existing fixed-subspace projection order. + for (const auto& state: occupied) + { + views.push_back(&state); + } + for (const auto& state: virtual_states) + { + views.push_back(&state.orbital); + } + return views; +} +} // namespace + +double require_positive_periodic_delta_excitations( + const std::vector& target_states, + const std::vector& source_occupied_eigenvalues_ry, + const std::string& diagnostic_context) +{ + std::ostringstream diagnostic; + diagnostic.imbue(std::locale::classic()); + diagnostic << std::setprecision(std::numeric_limits::max_digits10) + << "STERNHEIMER_EXCITATION_ERROR context={" << diagnostic_context + << "} units=Ry index_base=1 source_count=" << source_occupied_eigenvalues_ry.size() + << " target_count=" << target_states.size(); + if (source_occupied_eigenvalues_ry.empty() || target_states.empty()) + { + diagnostic << " reason=empty_spectrum"; + throw SternheimerExcitationError(diagnostic.str()); + } + + std::size_t source_max_index = 0; + for (std::size_t index = 0; index != source_occupied_eigenvalues_ry.size(); ++index) + { + const double value = source_occupied_eigenvalues_ry[index]; + if (!std::isfinite(value)) + { + diagnostic << " reason=nonfinite_source source_index=" << index + 1 + << " source_eigenvalue_Ry=" << value; + throw SternheimerExcitationError(diagnostic.str()); + } + if (value > source_occupied_eigenvalues_ry[source_max_index]) + { + source_max_index = index; + } + } + std::size_t target_min_index = 0; + for (std::size_t index = 0; index != target_states.size(); ++index) + { + const double value = target_states[index].eigenvalue; + if (!std::isfinite(value)) + { + diagnostic << " reason=nonfinite_target target_index=" << index + 1 + << " target_eigenvalue_Ry=" << value; + throw SternheimerExcitationError(diagnostic.str()); + } + if (value < target_states[target_min_index].eigenvalue) + { + target_min_index = index; + } + } + + const double source_max = source_occupied_eigenvalues_ry[source_max_index]; + const double target_min = target_states[target_min_index].eigenvalue; + const double minimum_excitation = target_min - source_max; + diagnostic << " source_max_Ry=" << source_max << " source_max_index=" << source_max_index + 1 + << " target_min_Ry=" << target_min << " target_min_index=" << target_min_index + 1 + << " min_excitation_Ry=" << minimum_excitation; + if (!std::isfinite(minimum_excitation)) + { + diagnostic << " reason=nonfinite_gap"; + throw SternheimerExcitationError(diagnostic.str()); + } + if (minimum_excitation <= 0.0) + { + diagnostic << " reason=nonpositive_gap"; + throw SternheimerExcitationError(diagnostic.str()); + } + return minimum_excitation; +} + +SternheimerPeriodicResponseProjectors::SternheimerPeriodicResponseProjectors( + const std::vector& occupied_wavefunctions, + const std::vector& virtual_states, + const double volume_element, + const bool pack_batch) + : occupied(occupied_wavefunctions, volume_element, pack_batch), + fixed(fixed_subspace_views(occupied_wavefunctions, virtual_states), volume_element, pack_batch) +{ +} + +std::size_t SternheimerPeriodicResponseProjectors::estimated_storage_bytes( + const std::size_t grid_size, const std::size_t occupied_count, const std::size_t virtual_count, + const bool pack_batch) +{ + const auto maximum = std::numeric_limits::max(); + const auto value_bytes = sizeof(SternheimerFDHamiltonian::Complex); + const auto metadata_bytes = value_bytes + sizeof(const SternheimerFDHamiltonian::Vector*); + const auto packed_grid_size = pack_batch ? grid_size : 0; + if (occupied_count > (maximum - virtual_count) / 2 + || packed_grid_size > (maximum - metadata_bytes) / value_bytes) + { + throw std::overflow_error("Periodic Sternheimer shared-projector storage size overflow."); + } + const auto count = 2 * occupied_count + virtual_count; + const auto bytes_per_state = packed_grid_size * value_bytes + metadata_bytes; + if (count > maximum / bytes_per_state) + { + throw std::overflow_error("Periodic Sternheimer shared-projector storage size overflow."); + } + return count * bytes_per_state; +} + +SternheimerPeriodicLinearResponse solve_sternheimer_periodic_linear_response( + const bool use_delta_sternheimer, + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options, + const SternheimerPeriodicResponseProjectors* shared_projectors) +{ + SternheimerPeriodicLinearResponse result; + result.projected_rhs = rhs; + const auto dot = [volume_element](const SternheimerFDHamiltonian::Vector& lhs, + const SternheimerFDHamiltonian::Vector& rhs_vector) { + return sternheimer_fd_grid_dot(lhs, rhs_vector, volume_element); + }; + SternheimerRPA::project_out_subspace(occupied_wavefunctions, dot, result.projected_rhs); + if (use_delta_sternheimer) + { + std::unique_ptr local_projectors; + if (shared_projectors == nullptr) + { + local_projectors = std::make_unique( + occupied_wavefunctions, virtual_states, volume_element, false); + shared_projectors = local_projectors.get(); + } + SternheimerDeltaLinearResponse response + = solve_delta_sternheimer_linear_response(hamiltonian, + shared_projectors->fixed, + reference_eigenvalue, + rhs, + virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + options); + result.wavefunction = response.response.reconstructed_wavefunction; + result.solver = response.solver; + result.residual_norm = response.residual_norm; + result.full_grid_equation_residual_norm + = sternheimer_fd_linear_response_residual_norm(hamiltonian, + occupied_wavefunctions, + reference_eigenvalue, + rhs, + result.wavefunction, + omega, + volume_element); + result.has_delta_components = true; + result.delta_components = std::move(response.response); + return result; + } + + const SternheimerFDLinearResponse response + = solve_sternheimer_fd_linear_response(hamiltonian, + occupied_wavefunctions, + reference_eigenvalue, + rhs, + omega, + volume_element, + options); + result.wavefunction = response.delta_wavefunction; + result.solver = response.solver; + result.residual_norm = response.residual_norm; + result.full_grid_equation_residual_norm = response.residual_norm; + return result; +} + +std::vector solve_sternheimer_periodic_linear_response_batch( + const bool use_delta_sternheimer, + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + const double reference_eigenvalue, + const SternheimerFDHamiltonian::Matrix& rhs, + const std::vector& virtual_states, + const std::vector>& perturbation_matrix_elements, + const double omega, + const double volume_element, + const SternheimerRPA::SolverOptions& options, + const SternheimerPeriodicResponseProjectors* shared_projectors) +{ + if (rhs.size() != perturbation_matrix_elements.size()) + { + throw std::invalid_argument( + "Periodic Sternheimer batch response requires one perturbation-element vector per rhs."); + } + std::vector results(rhs.size()); + if (!use_delta_sternheimer) + { + for (std::size_t column = 0; column != rhs.size(); ++column) + { + results[column] = solve_sternheimer_periodic_linear_response(false, + hamiltonian, + occupied_wavefunctions, + reference_eigenvalue, + rhs[column], + virtual_states, + perturbation_matrix_elements[column], + omega, + volume_element, + options); + } + return results; + } + + std::unique_ptr local_projectors; + if (shared_projectors == nullptr) + { + local_projectors = std::make_unique( + occupied_wavefunctions, virtual_states, volume_element); + shared_projectors = local_projectors.get(); + } + const auto& occupied_projector = shared_projectors->occupied; + SternheimerFDHamiltonian::Matrix projected_rhs = rhs; + occupied_projector.project_batch(projected_rhs); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + results[column].projected_rhs = projected_rhs[column]; + } + + std::vector responses + = solve_delta_sternheimer_linear_response_batch(hamiltonian, + shared_projectors->fixed, + reference_eigenvalue, + rhs, + virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + options); + SternheimerFDHamiltonian::Matrix wavefunctions(rhs.size()); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + results[column].wavefunction = responses[column].response.reconstructed_wavefunction; + wavefunctions[column] = results[column].wavefunction; + results[column].solver = responses[column].solver; + results[column].residual_norm = responses[column].residual_norm; + results[column].has_delta_components = true; + results[column].delta_components = std::move(responses[column].response); + } + + SternheimerFDHamiltonian::Matrix projected_wavefunctions = wavefunctions; + occupied_projector.project_batch(projected_wavefunctions); + SternheimerFDHamiltonian::Matrix residuals; + hamiltonian.apply_batch(projected_wavefunctions, residuals); + const SternheimerFDHamiltonian::Complex shift(-reference_eigenvalue, omega); +#pragma omp parallel for schedule(static) + for (std::size_t column = 0; column != residuals.size(); ++column) + { + for (std::size_t ir = 0; ir != residuals[column].size(); ++ir) + { + residuals[column][ir] += shift * projected_wavefunctions[column][ir]; + } + } + occupied_projector.project_batch(residuals); +#pragma omp parallel for schedule(static) + for (std::size_t column = 0; column != residuals.size(); ++column) + { + double norm_squared = 0.0; + for (std::size_t ir = 0; ir != residuals[column].size(); ++ir) + { + residuals[column][ir] -= projected_rhs[column][ir]; + norm_squared += std::norm(residuals[column][ir]); + } + results[column].full_grid_equation_residual_norm + = std::sqrt(volume_element * norm_squared); + } + return results; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.h new file mode 100644 index 00000000000..f99b22180b7 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.h @@ -0,0 +1,86 @@ +#ifndef STERNHEIMER_PERIODIC_SOLVER_H +#define STERNHEIMER_PERIODIC_SOLVER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_delta.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h" + +#include +#include + +namespace ModuleRI +{ + +class SternheimerExcitationError : public std::runtime_error +{ + public: + using std::runtime_error::runtime_error; +}; + +// Validate one periodic k/k+q pair using all source occupied energies in Ry. +// Return the positive minimum excitation, or throw without modifying inputs. +// The caller supplies grid/k-pair context and handles fatal process termination; +// workers without an owned pair must not call this with empty placeholder data. +double require_positive_periodic_delta_excitations( + const std::vector& target_states, + const std::vector& source_occupied_eigenvalues_ry, + const std::string& diagnostic_context); + +struct SternheimerPeriodicLinearResponse +{ + SternheimerFDHamiltonian::Vector projected_rhs; + SternheimerFDHamiltonian::Vector wavefunction; + SternheimerRPA::SolverResult solver; + double residual_norm = 0.0; + double full_grid_equation_residual_norm = 0.0; + bool has_delta_components = false; + SternheimerDeltaPostprocessResult delta_components; +}; + +// Build once per k/k+q pair, before worker admission. Basis vectors must stay +// unchanged and alive until every channel worker has finished. +struct SternheimerPeriodicResponseProjectors +{ + SternheimerPeriodicResponseProjectors( + const std::vector& occupied_wavefunctions, + const std::vector& virtual_states, + double volume_element, + bool pack_batch = true); + + static std::size_t estimated_storage_bytes(std::size_t grid_size, + std::size_t occupied_count, + std::size_t virtual_count, + bool pack_batch = true); + + SternheimerSubspaceProjector occupied; + SternheimerSubspaceProjector fixed; +}; + +SternheimerPeriodicLinearResponse solve_sternheimer_periodic_linear_response( + bool use_delta_sternheimer, + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Vector& rhs, + const std::vector& virtual_states, + const std::vector& perturbation_matrix_elements, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions(), + const SternheimerPeriodicResponseProjectors* shared_projectors = nullptr); + +std::vector solve_sternheimer_periodic_linear_response_batch( + bool use_delta_sternheimer, + const SternheimerFDHamiltonian& hamiltonian, + const std::vector& occupied_wavefunctions, + double reference_eigenvalue, + const SternheimerFDHamiltonian::Matrix& rhs, + const std::vector& virtual_states, + const std::vector>& perturbation_matrix_elements, + double omega, + double volume_element, + const SternheimerRPA::SolverOptions& options = SternheimerRPA::SolverOptions(), + const SternheimerPeriodicResponseProjectors* shared_projectors = nullptr); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.cpp new file mode 100644 index 00000000000..5aa548cfefb --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.cpp @@ -0,0 +1,439 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.h" + +#include "source_basis/module_pw/pw_basis.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __MPI +#include +#endif + +namespace ModuleRI +{ +namespace +{ + +using ReciprocalIndex = std::array; + +class FFTWBuffer +{ + public: + FFTWBuffer(const int nx, const int ny, const int nz, const int direction) + : size_(static_cast(nx) * static_cast(ny) * static_cast(nz)) + { + data_ = fftw_alloc_complex(size_); + if (data_ == nullptr) + { + throw std::bad_alloc(); + } + for (std::size_t index = 0; index != size_; ++index) + { + data_[index][0] = 0.0; + data_[index][1] = 0.0; + } +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + plan_ = fftw_plan_dft_3d(nx, ny, nz, data_, data_, direction, FFTW_ESTIMATE); + } + if (plan_ == nullptr) + { + fftw_free(data_); + data_ = nullptr; + throw std::runtime_error("Failed to initialize a Sternheimer response-grid FFT."); + } + } + + ~FFTWBuffer() + { +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + if (plan_ != nullptr) + { + fftw_destroy_plan(plan_); + } + } + fftw_free(data_); + } + + FFTWBuffer(const FFTWBuffer&) = delete; + FFTWBuffer& operator=(const FFTWBuffer&) = delete; + + fftw_complex* data() + { + return data_; + } + + const fftw_complex* data() const + { + return data_; + } + + void execute() const + { + fftw_execute(plan_); + } + + private: + std::size_t size_ = 0; + fftw_complex* data_ = nullptr; + fftw_plan plan_ = nullptr; +}; + +std::size_t fft_index(const int ix, const int iy, const int iz, const int ny, const int nz) +{ + return static_cast((ix * ny + iy) * nz + iz); +} + +int signed_frequency(const int index, const int dimension) +{ + return index <= dimension / 2 ? index : index - dimension; +} + +int frequency_index(const int frequency, const int dimension) +{ + return frequency >= 0 ? frequency : frequency + dimension; +} + +struct FrequencySelection +{ + std::array values = {0, 0}; + int size = 1; +}; + +FrequencySelection fine_frequencies_for_coarse_index(const int coarse_index, + const int coarse_dimension, + const int fine_dimension) +{ + const int frequency = signed_frequency(coarse_index, coarse_dimension); + if (coarse_dimension % 2 == 0 && coarse_index == coarse_dimension / 2 + && fine_dimension > coarse_dimension) + { + return {{frequency, -frequency}, 2}; + } + return {{frequency, 0}, 1}; +} + +std::unique_ptr make_serial_basis_with_dimensions( + const ModulePW::PW_Basis& reference, + const double ecutwfc, + const std::array& dimensions, + const int fft_mode) +{ + auto basis = std::make_unique("cpu", "double"); +#ifdef __MPI + basis->initmpi(1, 0, MPI_COMM_SELF); +#endif + basis->initgrids(reference.lat0, reference.latvec, dimensions[0], dimensions[1], dimensions[2]); + basis->initparameters(false, 4.0 * ecutwfc); + basis->fft_bundle.initfftmode(fft_mode); + basis->setuptransform(); + basis->collect_local_pw(); + if (basis->nrxx != basis->nxyz) + { + throw std::runtime_error("Serial Sternheimer fine FFT basis does not own the complete real-space grid."); + } + return basis; +} + +bool explicit_dimensions_requested(const std::array& dimensions) +{ + const int positive = static_cast(std::count_if(dimensions.begin(), dimensions.end(), [](const int value) { + return value > 0; + })); + if (std::any_of(dimensions.begin(), dimensions.end(), [](const int value) { return value < 0; })) + { + throw std::invalid_argument("Explicit Sternheimer response-grid dimensions must be zero or positive."); + } + if (positive != 0 && positive != 3) + { + throw std::invalid_argument("Explicit Sternheimer response-grid dimensions must be all zero or all positive."); + } + return positive == 3; +} + +void validate_explicit_dimensions(const ModulePW::PW_Basis& pbe_basis, + const std::array& dimensions, + const int fd_order) +{ + if (fd_order != 2 && fd_order != 4 && fd_order != 6 && fd_order != 8) + { + throw std::invalid_argument("Sternheimer finite-difference order must be 2, 4, 6, or 8."); + } + const std::array pbe_dimensions = {pbe_basis.nx, pbe_basis.ny, pbe_basis.nz}; + const int minimum_dimension = fd_order + 1; + for (std::size_t direction = 0; direction != dimensions.size(); ++direction) + { + if (dimensions[direction] < minimum_dimension) + { + throw std::invalid_argument("Each explicit Sternheimer response-grid dimension must exceed twice the " + "finite-difference stencil radius."); + } + if (dimensions[direction] > pbe_dimensions[direction]) + { + throw std::invalid_argument("Explicit Sternheimer response-grid dimensions must not exceed the PBE grid."); + } + } +} + +std::unique_ptr make_serial_basis_with_cutoff(const ModulePW::PW_Basis& reference, + const double ecutwfc, + const int fft_mode) +{ + auto basis = std::make_unique("cpu", "double"); +#ifdef __MPI + basis->initmpi(1, 0, MPI_COMM_SELF); +#endif + basis->initgrids(reference.lat0, reference.latvec, 4.0 * ecutwfc); + basis->initparameters(false, 4.0 * ecutwfc); + basis->fft_bundle.initfftmode(fft_mode); + basis->setuptransform(); + basis->collect_local_pw(); + if (basis->nrxx != basis->nxyz) + { + throw std::runtime_error("Serial Sternheimer response FFT basis does not own the complete real-space grid."); + } + return basis; +} + +ReciprocalIndex reciprocal_index(const ModuleBase::Vector3& vector) +{ + ReciprocalIndex index = {0, 0, 0}; + const std::array components = {vector.x, vector.y, vector.z}; + for (std::size_t direction = 0; direction != components.size(); ++direction) + { + const double rounded = std::round(components[direction]); + if (std::abs(components[direction] - rounded) > 1.0e-10) + { + throw std::runtime_error("Sternheimer FFT basis returned a noninteger reciprocal-grid index."); + } + index[direction] = static_cast(rounded); + } + return index; +} + +} // namespace + +SternheimerResponseGrid::SternheimerResponseGrid() = default; +SternheimerResponseGrid::~SternheimerResponseGrid() = default; +SternheimerResponseGrid::SternheimerResponseGrid(SternheimerResponseGrid&&) noexcept = default; +SternheimerResponseGrid& SternheimerResponseGrid::operator=(SternheimerResponseGrid&&) noexcept = default; + +bool sternheimer_uses_independent_response_grid(const double response_ecutwfc, const double pbe_ecutwfc) +{ + if (pbe_ecutwfc <= 0.0) + { + throw std::invalid_argument("PBE ecutwfc must be positive before selecting a Sternheimer response grid."); + } + if (response_ecutwfc < 0.0) + { + throw std::invalid_argument("Sternheimer response ecutwfc must be zero or positive."); + } + const double tolerance = 1.0e-12 * std::max(1.0, pbe_ecutwfc); + if (response_ecutwfc > pbe_ecutwfc + tolerance) + { + throw std::invalid_argument("Sternheimer response ecutwfc must not exceed the PBE ecutwfc."); + } + return response_ecutwfc > 0.0; +} + +const char* sternheimer_response_grid_source_name(const SternheimerResponseGridSource source) +{ + switch (source) + { + case SternheimerResponseGridSource::Pbe: + return "pbe"; + case SternheimerResponseGridSource::Cutoff: + return "cutoff"; + case SternheimerResponseGridSource::Explicit: + return "explicit"; + } + throw std::invalid_argument("Unknown Sternheimer response-grid source."); +} + +SternheimerResponseGrid make_sternheimer_response_grid(const ModulePW::PW_Basis& pbe_basis, + const double pbe_ecutwfc, + const double response_ecutwfc, + const int fft_mode, + const std::array response_dimensions, + const int fd_order) +{ + SternheimerResponseGrid result; + result.basis = &pbe_basis; + result.requested_ecutwfc = response_ecutwfc; + result.requested_dimensions = response_dimensions; + const bool uses_cutoff = sternheimer_uses_independent_response_grid(response_ecutwfc, pbe_ecutwfc); + const bool uses_explicit_dimensions = explicit_dimensions_requested(response_dimensions); + if (uses_cutoff && uses_explicit_dimensions) + { + throw std::invalid_argument( + "sternheimer_response_ecutwfc and explicit response-grid dimensions are mutually exclusive."); + } + if (!uses_cutoff && !uses_explicit_dimensions) + { + return result; + } + + std::unique_ptr response_basis; + if (uses_explicit_dimensions) + { + validate_explicit_dimensions(pbe_basis, response_dimensions, fd_order); + response_basis + = make_serial_basis_with_dimensions(pbe_basis, pbe_ecutwfc, response_dimensions, fft_mode); + result.source = SternheimerResponseGridSource::Explicit; + } + else + { + response_basis = make_serial_basis_with_cutoff(pbe_basis, response_ecutwfc, fft_mode); + result.source = SternheimerResponseGridSource::Cutoff; + } + if (response_basis->nx == pbe_basis.nx && response_basis->ny == pbe_basis.ny && response_basis->nz == pbe_basis.nz) + { + result.source = SternheimerResponseGridSource::Pbe; + return result; + } + + result.serial_fine_basis = make_serial_basis_with_dimensions( + pbe_basis, pbe_ecutwfc, {pbe_basis.nx, pbe_basis.ny, pbe_basis.nz}, fft_mode); + result.serial_response_basis = std::move(response_basis); + result.basis = result.serial_response_basis.get(); + result.independent = true; + return result; +} + +std::vector restrict_sternheimer_real_field(const ModulePW::PW_Basis& fine_basis, + const ModulePW::PW_Basis& coarse_basis, + const std::vector& fine_values) +{ + if (&fine_basis == &coarse_basis) + { + if (fine_values.size() != static_cast(fine_basis.nxyz)) + { + throw std::invalid_argument("Sternheimer real field size does not match the identity FFT grid."); + } + return fine_values; + } + if (fine_basis.nrxx != fine_basis.nxyz || coarse_basis.nrxx != coarse_basis.nxyz) + { + throw std::invalid_argument("Sternheimer spectral restriction requires serial complete FFT grids."); + } + if (fine_values.size() != static_cast(fine_basis.nxyz)) + { + throw std::invalid_argument("Sternheimer fine real field size does not match its FFT grid."); + } + + std::vector> fine_coefficients(static_cast(fine_basis.npw)); + fine_basis.real2recip(fine_values.data(), fine_coefficients.data()); + + std::map> coefficients_by_index; + for (int ig = 0; ig != fine_basis.npw; ++ig) + { + coefficients_by_index.emplace(reciprocal_index(fine_basis.gdirect[ig]), fine_coefficients[ig]); + } + + std::vector> coarse_coefficients(static_cast(coarse_basis.npw), + std::complex(0.0, 0.0)); + for (int ig = 0; ig != coarse_basis.npw; ++ig) + { + const auto found = coefficients_by_index.find(reciprocal_index(coarse_basis.gdirect[ig])); + if (found != coefficients_by_index.end()) + { + coarse_coefficients[ig] = found->second; + } + } + + std::vector coarse_values(static_cast(coarse_basis.nxyz), 0.0); + coarse_basis.recip2real(coarse_coefficients.data(), coarse_values.data()); + return coarse_values; +} + +std::vector restrict_sternheimer_real_field_rectangular(const ModulePW::PW_Basis& fine_basis, + const ModulePW::PW_Basis& coarse_basis, + const std::vector& fine_values) +{ + if (fine_basis.nrxx != fine_basis.nxyz || coarse_basis.nrxx != coarse_basis.nxyz) + { + throw std::invalid_argument("Rectangular Sternheimer restriction requires serial complete FFT grids."); + } + if (fine_values.size() != static_cast(fine_basis.nxyz)) + { + throw std::invalid_argument("Sternheimer fine real field size does not match its rectangular FFT grid."); + } + if (coarse_basis.nx > fine_basis.nx || coarse_basis.ny > fine_basis.ny || coarse_basis.nz > fine_basis.nz) + { + throw std::invalid_argument("Rectangular Sternheimer restriction does not support grid upsampling."); + } + if (fine_basis.nx == coarse_basis.nx && fine_basis.ny == coarse_basis.ny && fine_basis.nz == coarse_basis.nz) + { + return fine_values; + } + + FFTWBuffer fine_fft(fine_basis.nx, fine_basis.ny, fine_basis.nz, FFTW_FORWARD); + for (std::size_t index = 0; index != fine_values.size(); ++index) + { + fine_fft.data()[index][0] = fine_values[index]; + fine_fft.data()[index][1] = 0.0; + } + fine_fft.execute(); + + FFTWBuffer coarse_fft(coarse_basis.nx, coarse_basis.ny, coarse_basis.nz, FFTW_BACKWARD); + const double inverse_fine_size = 1.0 / static_cast(fine_basis.nxyz); + for (int ix = 0; ix != coarse_basis.nx; ++ix) + { + const auto fine_x = fine_frequencies_for_coarse_index(ix, coarse_basis.nx, fine_basis.nx); + for (int iy = 0; iy != coarse_basis.ny; ++iy) + { + const auto fine_y = fine_frequencies_for_coarse_index(iy, coarse_basis.ny, fine_basis.ny); + for (int iz = 0; iz != coarse_basis.nz; ++iz) + { + const auto fine_z = fine_frequencies_for_coarse_index(iz, coarse_basis.nz, fine_basis.nz); + std::complex coefficient(0.0, 0.0); + for (int x_index = 0; x_index != fine_x.size; ++x_index) + { + const int kx = fine_x.values[static_cast(x_index)]; + for (int y_index = 0; y_index != fine_y.size; ++y_index) + { + const int ky = fine_y.values[static_cast(y_index)]; + for (int z_index = 0; z_index != fine_z.size; ++z_index) + { + const int kz = fine_z.values[static_cast(z_index)]; + const std::size_t fine_index + = fft_index(frequency_index(kx, fine_basis.nx), + frequency_index(ky, fine_basis.ny), + frequency_index(kz, fine_basis.nz), + fine_basis.ny, + fine_basis.nz); + coefficient += std::complex(fine_fft.data()[fine_index][0], + fine_fft.data()[fine_index][1]); + } + } + } + coefficient *= inverse_fine_size; + const std::size_t coarse_index = fft_index(ix, iy, iz, coarse_basis.ny, coarse_basis.nz); + coarse_fft.data()[coarse_index][0] = coefficient.real(); + coarse_fft.data()[coarse_index][1] = coefficient.imag(); + } + } + } + coarse_fft.execute(); + + std::vector coarse_values(static_cast(coarse_basis.nxyz), 0.0); + for (std::size_t index = 0; index != coarse_values.size(); ++index) + { + coarse_values[index] = coarse_fft.data()[index][0]; + } + return coarse_values; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.h new file mode 100644 index 00000000000..d5c0715ea3c --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.h @@ -0,0 +1,62 @@ +#ifndef STERNHEIMER_RESPONSE_GRID_H +#define STERNHEIMER_RESPONSE_GRID_H + +#include +#include +#include + +namespace ModulePW +{ +class PW_Basis; +} + +namespace ModuleRI +{ + +enum class SternheimerResponseGridSource +{ + Pbe, + Cutoff, + Explicit +}; + +struct SternheimerResponseGrid +{ + SternheimerResponseGrid(); + ~SternheimerResponseGrid(); + SternheimerResponseGrid(SternheimerResponseGrid&&) noexcept; + SternheimerResponseGrid& operator=(SternheimerResponseGrid&&) noexcept; + SternheimerResponseGrid(const SternheimerResponseGrid&) = delete; + SternheimerResponseGrid& operator=(const SternheimerResponseGrid&) = delete; + + std::unique_ptr serial_fine_basis; + std::unique_ptr serial_response_basis; + const ModulePW::PW_Basis* basis = nullptr; + bool independent = false; + double requested_ecutwfc = 0.0; + std::array requested_dimensions = {0, 0, 0}; + SternheimerResponseGridSource source = SternheimerResponseGridSource::Pbe; +}; + +bool sternheimer_uses_independent_response_grid(double response_ecutwfc, double pbe_ecutwfc); + +const char* sternheimer_response_grid_source_name(SternheimerResponseGridSource source); + +SternheimerResponseGrid make_sternheimer_response_grid(const ModulePW::PW_Basis& pbe_basis, + double pbe_ecutwfc, + double response_ecutwfc, + int fft_mode, + std::array response_dimensions = {0, 0, 0}, + int fd_order = 2); + +std::vector restrict_sternheimer_real_field(const ModulePW::PW_Basis& fine_basis, + const ModulePW::PW_Basis& coarse_basis, + const std::vector& fine_values); + +std::vector restrict_sternheimer_real_field_rectangular(const ModulePW::PW_Basis& fine_basis, + const ModulePW::PW_Basis& coarse_basis, + const std::vector& fine_values); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_rpa.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_rpa.cpp new file mode 100644 index 00000000000..9663eeaef91 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_rpa.cpp @@ -0,0 +1,2063 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" +#include "source_lcao/module_rpa/module_delta_st/greenx_minimax_adapter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +namespace +{ + +constexpr std::int32_t kChi0V1Marker = -41073291; +constexpr std::int32_t kChi0V1ComplexFlag = 1; +constexpr std::int32_t kCoulombV1Marker = -20129433; + +void assert_same_size(const SternheimerRPA::Vector& lhs, const SternheimerRPA::Vector& rhs, const char* context) +{ + if (lhs.size() != rhs.size()) + { + throw std::invalid_argument(std::string(context) + " vector sizes differ."); + } +} + +std::vector make_subspace_views(const std::vector& subspace) +{ + std::vector views; + views.reserve(subspace.size()); + for (const SternheimerRPA::Vector& basis: subspace) + { + views.push_back(&basis); + } + return views; +} + +double vector_norm(const SternheimerRPA::LinearProblem& problem, const SternheimerRPA::Vector& vec) +{ + const auto norm2 = problem.dot(vec, vec); + return std::sqrt(std::max(0.0, norm2.real())); +} + +void apply_preconditioner(const SternheimerRPA::LinearProblem& problem, + const SternheimerRPA::Vector& input, + SternheimerRPA::Vector& output) +{ + if (problem.precondition) + { + problem.precondition(input, output); + } + else + { + output = input; + } +} + +void axpy(const SternheimerRPA::Complex alpha, const SternheimerRPA::Vector& x, SternheimerRPA::Vector& y) +{ + assert_same_size(x, y, "SternheimerRPA::axpy"); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != y.size(); ++i) + { + y[i] += alpha * x[i]; + } +} + +void scale(const SternheimerRPA::Complex alpha, SternheimerRPA::Vector& x) +{ +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != x.size(); ++i) + { + x[i] *= alpha; + } +} + +std::vector solve_small_dense_system( + std::vector> matrix, + std::vector rhs) +{ + const int size = static_cast(rhs.size()); + for (int pivot = 0; pivot != size; ++pivot) + { + int pivot_row = pivot; + double pivot_abs = std::abs(matrix[pivot][pivot]); + for (int row = pivot + 1; row != size; ++row) + { + const double candidate_abs = std::abs(matrix[row][pivot]); + if (candidate_abs > pivot_abs) + { + pivot_abs = candidate_abs; + pivot_row = row; + } + } + if (pivot_abs < 1.0e-28) + { + throw std::runtime_error("SternheimerRPA::solve_gmres found a singular least-squares system."); + } + if (pivot_row != pivot) + { + std::swap(matrix[pivot], matrix[pivot_row]); + std::swap(rhs[pivot], rhs[pivot_row]); + } + + const SternheimerRPA::Complex diagonal = matrix[pivot][pivot]; + for (int col = pivot; col != size; ++col) + { + matrix[pivot][col] /= diagonal; + } + rhs[pivot] /= diagonal; + + for (int row = 0; row != size; ++row) + { + if (row == pivot) + { + continue; + } + const SternheimerRPA::Complex factor = matrix[row][pivot]; + if (std::abs(factor) == 0.0) + { + continue; + } + for (int col = pivot; col != size; ++col) + { + matrix[row][col] -= factor * matrix[pivot][col]; + } + rhs[row] -= factor * rhs[pivot]; + } + } + return rhs; +} + +struct LeastSquaresSolution +{ + std::vector coefficients; + double residual_norm = 0.0; +}; + +LeastSquaresSolution solve_gmres_least_squares( + const std::vector>& hessenberg, + const int rows, + const int cols, + const double beta) +{ + std::vector> normal_matrix( + cols, std::vector(cols, SternheimerRPA::Complex(0.0, 0.0))); + std::vector normal_rhs(cols, SternheimerRPA::Complex(0.0, 0.0)); + + for (int col = 0; col != cols; ++col) + { + normal_rhs[col] = std::conj(hessenberg[0][col]) * beta; + for (int other = 0; other != cols; ++other) + { + for (int row = 0; row != rows; ++row) + { + normal_matrix[col][other] += std::conj(hessenberg[row][col]) * hessenberg[row][other]; + } + } + } + + LeastSquaresSolution result; + result.coefficients = solve_small_dense_system(std::move(normal_matrix), std::move(normal_rhs)); + + double residual2 = 0.0; + for (int row = 0; row != rows; ++row) + { + SternheimerRPA::Complex value = row == 0 ? SternheimerRPA::Complex(beta, 0.0) + : SternheimerRPA::Complex(0.0, 0.0); + for (int col = 0; col != cols; ++col) + { + value -= hessenberg[row][col] * result.coefficients[col]; + } + residual2 += std::norm(value); + } + result.residual_norm = std::sqrt(residual2); + return result; +} + +void add_krylov_update(const std::vector& preconditioned_basis, + const std::vector& coefficients, + SternheimerRPA::Vector& solution) +{ + for (std::size_t basis_index = 0; basis_index != coefficients.size(); ++basis_index) + { + axpy(coefficients[basis_index], preconditioned_basis[basis_index], solution); + } +} + +void checked_write(std::ofstream& ofs, const void* data, const std::size_t bytes, const std::string& filename) +{ + const char* ptr = reinterpret_cast(data); + std::size_t bytes_left = bytes; + const std::size_t max_chunk = static_cast(std::numeric_limits::max()); + while (bytes_left > 0) + { + const std::size_t chunk = std::min(bytes_left, max_chunk); + ofs.write(ptr, static_cast(chunk)); + if (!ofs.good()) + { + throw std::runtime_error("Failed to write " + filename); + } + ptr += chunk; + bytes_left -= chunk; + } +} + +template +void write_scalar(std::ofstream& ofs, const Value& value, const std::string& filename) +{ + checked_write(ofs, &value, sizeof(Value), filename); +} + +void checked_read(std::ifstream& in, void* data, const std::size_t bytes, const std::string& filename) +{ + in.read(reinterpret_cast(data), static_cast(bytes)); + if (!in.good()) + { + throw std::runtime_error("Failed to read " + filename); + } +} + +template +Value read_scalar(std::ifstream& in, const std::string& filename) +{ + Value value{}; + checked_read(in, &value, sizeof(Value), filename); + return value; +} + +std::int32_t checked_i32_from_size(const std::size_t value, const std::string& context) +{ + if (value > static_cast(std::numeric_limits::max())) + { + throw std::runtime_error(context + " exceeds int32_t range."); + } + return static_cast(value); +} + +std::int64_t checked_i64_from_size(const std::size_t value, const std::string& context) +{ + if (value > static_cast(std::numeric_limits::max())) + { + throw std::runtime_error(context + " exceeds int64_t range."); + } + return static_cast(value); +} + +std::size_t checked_mul_size(const std::size_t lhs, const std::size_t rhs, const std::string& context) +{ + if (lhs != 0 && rhs > std::numeric_limits::max() / lhs) + { + throw std::runtime_error(context + " exceeds size_t range."); + } + return lhs * rhs; +} + +int sum_positive_sizes(const std::vector& values, const std::string& context) +{ + int sum = 0; + for (const int value: values) + { + if (value <= 0) + { + throw std::invalid_argument(context + " contains a non-positive atom block size."); + } + if (value > std::numeric_limits::max() - sum) + { + throw std::runtime_error(context + " exceeds int range."); + } + sum += value; + } + return sum; +} + +std::size_t upper_triangular_pair_index(const std::size_t iatom, const std::size_t jatom, const std::size_t natom) +{ + if (iatom > jatom) + { + throw std::invalid_argument("Sternheimer chi0 v1 writer expects upper-triangular atom pairs."); + } + return iatom * natom - iatom * (iatom - 1) / 2 + (jatom - iatom); +} + +std::pair upper_triangular_pair_from_index(const int pair_index, const int natom) +{ + int index = 0; + for (int iatom = 0; iatom != natom; ++iatom) + { + for (int jatom = iatom; jatom != natom; ++jatom) + { + if (index == pair_index) + { + return {iatom, jatom}; + } + ++index; + } + } + throw std::runtime_error("Coulomb v1 atom-pair index is out of range."); +} + +std::vector> map_atom_local_to_global( + const std::vector& channels, + const std::vector& atom_naux) +{ + std::vector> global_index(atom_naux.size()); + for (std::size_t iatom = 0; iatom != atom_naux.size(); ++iatom) + { + global_index[iatom].assign(static_cast(atom_naux[iatom]), -1); + } + + for (std::size_t iglobal = 0; iglobal != channels.size(); ++iglobal) + { + const SternheimerRPA::AuxiliaryChannel& channel = channels[iglobal]; + if (channel.channel_index != static_cast(iglobal)) + { + throw std::invalid_argument("Sternheimer chi0 v1 channel indices must match matrix ordering."); + } + if (channel.atom_index < 0 || channel.atom_index >= static_cast(atom_naux.size())) + { + throw std::invalid_argument("Sternheimer chi0 v1 channel atom index is out of range."); + } + const int naux = atom_naux[static_cast(channel.atom_index)]; + if (channel.atom_local_index < 0 || channel.atom_local_index >= naux) + { + throw std::invalid_argument("Sternheimer chi0 v1 channel local auxiliary index is out of range."); + } + int& mapped_index + = global_index[static_cast(channel.atom_index)][static_cast( + channel.atom_local_index)]; + if (mapped_index != -1) + { + throw std::invalid_argument("Sternheimer chi0 v1 channel mapping contains a duplicate local index."); + } + mapped_index = static_cast(iglobal); + } + + for (const std::vector& atom_map: global_index) + { + for (const int mapped_index: atom_map) + { + if (mapped_index < 0) + { + throw std::invalid_argument("Sternheimer chi0 v1 channel mapping is incomplete."); + } + } + } + return global_index; +} + +struct Chi0Block +{ + int pair_index = 0; + std::int64_t offset = 0; + std::vector payload; +}; + +struct CoulombFileBlock +{ + int pair_index = 0; + std::int64_t offset = 0; +}; + +} // namespace + +SternheimerRPA::CoulombV1Matrix SternheimerRPA::read_coulomb_v1_files( + const std::vector& filenames) +{ + if (filenames.empty()) + { + throw std::invalid_argument("Coulomb v1 reader requires at least one rank file."); + } + + CoulombV1Matrix result; + int value_flag_reference = -1; + std::vector pair_seen; + for (const std::string& filename: filenames) + { + std::ifstream in(filename.c_str(), std::ios::binary); + if (!in.good()) + { + throw std::runtime_error("Failed to open " + filename); + } + const std::int32_t marker = read_scalar(in, filename); + const std::int32_t iq = read_scalar(in, filename); + const std::int32_t naux = read_scalar(in, filename); + const std::int32_t value_flag = read_scalar(in, filename); + const std::int32_t natom = read_scalar(in, filename); + const std::int32_t nblocks = read_scalar(in, filename); + if (marker != kCoulombV1Marker || iq <= 0 || naux <= 0 || natom <= 0 || nblocks < 0) + { + throw std::runtime_error("Invalid Coulomb v1 header in " + filename); + } + std::vector atom_naux(static_cast(natom)); + for (int& count: atom_naux) + { + count = read_scalar(in, filename); + } + if (sum_positive_sizes(atom_naux, "Coulomb v1 atom sizes") != naux) + { + throw std::runtime_error("Coulomb v1 atom sizes do not sum to naux."); + } + if (result.iq == 0) + { + result.iq = iq; + result.atom_naux = atom_naux; + result.values.assign(static_cast(naux) * static_cast(naux), Complex(0.0, 0.0)); + value_flag_reference = value_flag; + pair_seen.assign(static_cast(natom * (natom + 1) / 2), false); + } + else if (result.iq != iq || result.atom_naux != atom_naux || value_flag_reference != value_flag) + { + throw std::runtime_error("Coulomb v1 rank-file metadata are inconsistent."); + } + + std::vector blocks(static_cast(nblocks)); + for (CoulombFileBlock& block: blocks) + { + block.pair_index = read_scalar(in, filename); + block.offset = read_scalar(in, filename); + } + std::vector starts(static_cast(natom), 0); + for (int iatom = 1; iatom != natom; ++iatom) + { + starts[static_cast(iatom)] + = starts[static_cast(iatom - 1)] + atom_naux[static_cast(iatom - 1)]; + } + for (const CoulombFileBlock& block: blocks) + { + if (block.pair_index < 0 || block.pair_index >= static_cast(pair_seen.size()) + || pair_seen[static_cast(block.pair_index)]) + { + throw std::runtime_error("Coulomb v1 atom-pair blocks are invalid or duplicated."); + } + pair_seen[static_cast(block.pair_index)] = true; + const auto pair = upper_triangular_pair_from_index(block.pair_index, natom); + const int nrow = atom_naux[static_cast(pair.first)]; + const int ncol = atom_naux[static_cast(pair.second)]; + in.seekg(block.offset, std::ios::beg); + if (!in.good()) + { + throw std::runtime_error("Invalid Coulomb v1 block offset in " + filename); + } + for (int irow = 0; irow != nrow; ++irow) + { + for (int icol = 0; icol != ncol; ++icol) + { + Complex value; + if (value_flag == 1) + { + value = read_scalar(in, filename); + } + else if (value_flag == 0) + { + value = Complex(read_scalar(in, filename), 0.0); + } + else + { + throw std::runtime_error("Unsupported Coulomb v1 value flag."); + } + const int row = starts[static_cast(pair.first)] + irow; + const int col = starts[static_cast(pair.second)] + icol; + result.values[static_cast(row) * static_cast(naux) + + static_cast(col)] = value; + if (pair.first != pair.second) + { + result.values[static_cast(col) * static_cast(naux) + + static_cast(row)] = std::conj(value); + } + } + } + } + } + if (std::find(pair_seen.begin(), pair_seen.end(), false) != pair_seen.end()) + { + throw std::runtime_error("Coulomb v1 rank files do not contain every atom-pair block."); + } + return result; +} + +SternheimerRPA::SolverResult SternheimerRPA::solve_bicgstab(const LinearProblem& problem, + const Vector& rhs, + Vector& solution) +{ + return solve_bicgstab(problem, rhs, solution, SolverOptions{}); +} + +SternheimerRPA::SolverResult SternheimerRPA::solve_bicgstab(const LinearProblem& problem, + const Vector& rhs, + Vector& solution, + const SolverOptions& options) +{ + if (!problem.apply) + { + throw std::invalid_argument("SternheimerRPA::solve_bicgstab requires an apply callback."); + } + if (!problem.dot) + { + throw std::invalid_argument("SternheimerRPA::solve_bicgstab requires a dot callback."); + } + if (solution.empty()) + { + solution.assign(rhs.size(), Complex(0.0, 0.0)); + } + assert_same_size(rhs, solution, "SternheimerRPA::solve_bicgstab"); + + Vector residual(rhs.size()); + Vector ax(rhs.size()); + problem.apply(solution, ax); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != rhs.size(); ++i) + { + residual[i] = rhs[i] - ax[i]; + } + + const double rhs_norm = vector_norm(problem, rhs); + const double norm_floor = rhs_norm > 0.0 ? rhs_norm : 1.0; + + SolverResult result; + result.absolute_residual = vector_norm(problem, residual); + result.relative_residual = result.absolute_residual / norm_floor; + if (result.relative_residual <= options.residual_tol) + { + result.converged = true; + return result; + } + + Vector residual_shadow = residual; + Vector p(rhs.size(), Complex(0.0, 0.0)); + Vector v(rhs.size(), Complex(0.0, 0.0)); + Vector s(rhs.size()); + Vector t(rhs.size()); + Vector p_hat(rhs.size()); + Vector s_hat(rhs.size()); + + Complex rho_old(1.0, 0.0); + Complex alpha(1.0, 0.0); + Complex omega_old(1.0, 0.0); + + for (int iter = 1; iter <= options.max_iter; ++iter) + { + const Complex rho_new = problem.dot(residual_shadow, residual); + if (std::abs(rho_new) < options.breakdown_tol) + { + result.iterations = iter - 1; + return result; + } + + if (iter == 1) + { + p = residual; + } + else + { + const Complex beta = (rho_new / rho_old) * (alpha / omega_old); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != p.size(); ++i) + { + p[i] = residual[i] + beta * (p[i] - omega_old * v[i]); + } + } + + apply_preconditioner(problem, p, p_hat); + problem.apply(p_hat, v); + const Complex rv = problem.dot(residual_shadow, v); + if (std::abs(rv) < options.breakdown_tol) + { + result.iterations = iter - 1; + return result; + } + alpha = rho_new / rv; + +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != s.size(); ++i) + { + s[i] = residual[i] - alpha * v[i]; + } + const double s_norm = vector_norm(problem, s); + if (s_norm / norm_floor <= options.residual_tol) + { + axpy(alpha, p_hat, solution); + result.converged = true; + result.iterations = iter; + result.absolute_residual = s_norm; + result.relative_residual = s_norm / norm_floor; + return result; + } + + apply_preconditioner(problem, s, s_hat); + problem.apply(s_hat, t); + const Complex tt = problem.dot(t, t); + if (std::abs(tt) < options.breakdown_tol) + { + result.iterations = iter - 1; + return result; + } + const Complex omega_new = problem.dot(t, s) / tt; + if (std::abs(omega_new) < options.breakdown_tol) + { + result.iterations = iter - 1; + return result; + } + + axpy(alpha, p_hat, solution); + axpy(omega_new, s_hat, solution); + +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != residual.size(); ++i) + { + residual[i] = s[i] - omega_new * t[i]; + } + + result.iterations = iter; + result.absolute_residual = vector_norm(problem, residual); + result.relative_residual = result.absolute_residual / norm_floor; + if (result.relative_residual <= options.residual_tol) + { + result.converged = true; + return result; + } + + rho_old = rho_new; + omega_old = omega_new; + } + + return result; +} + +SternheimerRPA::SolverResult SternheimerRPA::solve_gmres(const LinearProblem& problem, + const Vector& rhs, + Vector& solution, + const SolverOptions& options, + const int restart_dimension) +{ + if (!problem.apply) + { + throw std::invalid_argument("SternheimerRPA::solve_gmres requires an apply callback."); + } + if (!problem.dot) + { + throw std::invalid_argument("SternheimerRPA::solve_gmres requires a dot callback."); + } + if (restart_dimension <= 0) + { + throw std::invalid_argument("SternheimerRPA::solve_gmres requires a positive restart dimension."); + } + if (solution.empty()) + { + solution.assign(rhs.size(), Complex(0.0, 0.0)); + } + assert_same_size(rhs, solution, "SternheimerRPA::solve_gmres"); + + SolverResult result; + Vector residual(rhs.size()); + Vector ax(rhs.size()); + problem.apply(solution, ax); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != rhs.size(); ++i) + { + residual[i] = rhs[i] - ax[i]; + } + + const double rhs_norm = vector_norm(problem, rhs); + const double norm_floor = rhs_norm > 0.0 ? rhs_norm : 1.0; + double beta = vector_norm(problem, residual); + result.absolute_residual = beta; + result.relative_residual = beta / norm_floor; + if (result.relative_residual <= options.residual_tol) + { + result.converged = true; + return result; + } + + while (result.iterations < options.max_iter) + { + const int max_inner = std::min(restart_dimension, options.max_iter - result.iterations); + std::vector basis(max_inner + 1, Vector(rhs.size(), Complex(0.0, 0.0))); + std::vector preconditioned_basis(max_inner, Vector(rhs.size(), Complex(0.0, 0.0))); + std::vector> hessenberg(max_inner + 1, + std::vector(max_inner, Complex(0.0, 0.0))); + + basis[0] = residual; + scale(Complex(1.0 / beta, 0.0), basis[0]); + + LeastSquaresSolution least_squares; + int used_inner = 0; + for (int inner = 0; inner != max_inner; ++inner) + { + apply_preconditioner(problem, basis[inner], preconditioned_basis[inner]); + + Vector work; + problem.apply(preconditioned_basis[inner], work); + for (int basis_index = 0; basis_index <= inner; ++basis_index) + { + hessenberg[basis_index][inner] = problem.dot(basis[basis_index], work); + axpy(-hessenberg[basis_index][inner], basis[basis_index], work); + } + + const double next_norm = vector_norm(problem, work); + hessenberg[inner + 1][inner] = Complex(next_norm, 0.0); + if (next_norm > options.breakdown_tol) + { + basis[inner + 1] = work; + scale(Complex(1.0 / next_norm, 0.0), basis[inner + 1]); + } + + used_inner = inner + 1; + least_squares = solve_gmres_least_squares(hessenberg, used_inner + 1, used_inner, beta); + result.absolute_residual = least_squares.residual_norm; + result.relative_residual = result.absolute_residual / norm_floor; + result.iterations += 1; + + if (result.relative_residual <= options.residual_tol || next_norm <= options.breakdown_tol) + { + add_krylov_update(preconditioned_basis, least_squares.coefficients, solution); + result.converged = result.relative_residual <= options.residual_tol; + return result; + } + } + + add_krylov_update(preconditioned_basis, least_squares.coefficients, solution); + problem.apply(solution, ax); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != rhs.size(); ++i) + { + residual[i] = rhs[i] - ax[i]; + } + beta = vector_norm(problem, residual); + result.absolute_residual = beta; + result.relative_residual = result.absolute_residual / norm_floor; + if (result.relative_residual <= options.residual_tol) + { + result.converged = true; + return result; + } + } + + return result; +} + +std::vector SternheimerRPA::solve_gmres_batch( + const BatchLinearProblem& problem, + const Matrix& rhs, + Matrix& solution, + const SolverOptions& options, + const int restart_dimension) +{ + if (!problem.apply) + { + throw std::invalid_argument("SternheimerRPA::solve_gmres_batch requires an apply callback."); + } + if (!problem.dot) + { + throw std::invalid_argument("SternheimerRPA::solve_gmres_batch requires a dot callback."); + } + if (restart_dimension <= 0) + { + throw std::invalid_argument( + "SternheimerRPA::solve_gmres_batch requires a positive restart dimension."); + } + if (rhs.empty()) + { + solution.clear(); + return {}; + } + + const std::size_t vector_size = rhs.front().size(); + for (const Vector& column: rhs) + { + if (column.size() != vector_size) + { + throw std::invalid_argument( + "SternheimerRPA::solve_gmres_batch right-hand-side vector sizes differ."); + } + } + if (solution.empty()) + { + solution.assign(rhs.size(), Vector(vector_size, Complex(0.0, 0.0))); + } + if (solution.size() != rhs.size()) + { + throw std::invalid_argument( + "SternheimerRPA::solve_gmres_batch solution column count does not match the right-hand side."); + } + for (std::size_t column = 0; column != rhs.size(); ++column) + { + if (solution[column].empty()) + { + solution[column].assign(vector_size, Complex(0.0, 0.0)); + } + assert_same_size(rhs[column], solution[column], "SternheimerRPA::solve_gmres_batch"); + } + + const auto validate_batch_output = [](const Matrix& input, const Matrix& output, const char* context) { + if (output.size() != input.size()) + { + throw std::invalid_argument(std::string(context) + " output column count differs."); + } + for (std::size_t column = 0; column != input.size(); ++column) + { + assert_same_size(input[column], output[column], context); + } + }; + const auto norm = [&problem](const Vector& vector) { + const Complex norm2 = problem.dot(vector, vector); + return std::sqrt(std::max(0.0, norm2.real())); + }; + + struct ColumnState + { + SolverResult result; + Vector residual; + double norm_floor = 1.0; + double beta = 0.0; + bool active = true; + int max_inner = 0; + std::vector basis; + std::vector preconditioned_basis; + std::vector> hessenberg; + LeastSquaresSolution least_squares; + }; + + std::vector states(rhs.size()); + Matrix ax; + problem.apply(solution, ax); + validate_batch_output(solution, ax, "SternheimerRPA::solve_gmres_batch apply"); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + ColumnState& state = states[column]; + state.residual.resize(vector_size); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != vector_size; ++i) + { + state.residual[i] = rhs[column][i] - ax[column][i]; + } + const double rhs_norm = norm(rhs[column]); + state.norm_floor = rhs_norm > 0.0 ? rhs_norm : 1.0; + state.beta = norm(state.residual); + state.result.absolute_residual = state.beta; + state.result.relative_residual = state.beta / state.norm_floor; + if (state.result.relative_residual <= options.residual_tol) + { + state.result.converged = true; + state.active = false; + } + } + + while (true) + { + std::vector active_columns; + for (std::size_t column = 0; column != states.size(); ++column) + { + ColumnState& state = states[column]; + if (!state.active || state.result.iterations >= options.max_iter) + { + state.active = false; + continue; + } + active_columns.push_back(column); + state.max_inner = std::min(restart_dimension, options.max_iter - state.result.iterations); + state.basis.assign(static_cast(state.max_inner + 1), + Vector(vector_size, Complex(0.0, 0.0))); + state.preconditioned_basis.assign(static_cast(state.max_inner), + Vector(vector_size, Complex(0.0, 0.0))); + state.hessenberg.assign(static_cast(state.max_inner + 1), + std::vector(static_cast(state.max_inner), + Complex(0.0, 0.0))); + state.basis[0] = state.residual; + scale(Complex(1.0 / state.beta, 0.0), state.basis[0]); + } + if (active_columns.empty()) + { + break; + } + + for (int inner = 0; inner != restart_dimension; ++inner) + { + std::vector inner_columns; + Matrix precondition_input; + for (const std::size_t column: active_columns) + { + ColumnState& state = states[column]; + if (state.active && inner < state.max_inner) + { + inner_columns.push_back(column); + precondition_input.push_back(state.basis[static_cast(inner)]); + } + } + if (inner_columns.empty()) + { + break; + } + + Matrix preconditioned; + if (problem.precondition) + { + problem.precondition(precondition_input, preconditioned); + validate_batch_output(precondition_input, + preconditioned, + "SternheimerRPA::solve_gmres_batch precondition"); + } + else + { + preconditioned = precondition_input; + } + for (std::size_t active_index = 0; active_index != inner_columns.size(); ++active_index) + { + states[inner_columns[active_index]].preconditioned_basis[static_cast(inner)] + = preconditioned[active_index]; + } + + Matrix work; + problem.apply(preconditioned, work); + validate_batch_output(preconditioned, work, "SternheimerRPA::solve_gmres_batch apply"); + for (std::size_t active_index = 0; active_index != inner_columns.size(); ++active_index) + { + const std::size_t column = inner_columns[active_index]; + ColumnState& state = states[column]; + Vector& column_work = work[active_index]; + for (int basis_index = 0; basis_index <= inner; ++basis_index) + { + state.hessenberg[static_cast(basis_index)][static_cast(inner)] + = problem.dot(state.basis[static_cast(basis_index)], column_work); + axpy(-state.hessenberg[static_cast(basis_index)][static_cast(inner)], + state.basis[static_cast(basis_index)], + column_work); + } + + const double next_norm = norm(column_work); + state.hessenberg[static_cast(inner + 1)][static_cast(inner)] + = Complex(next_norm, 0.0); + if (next_norm > options.breakdown_tol) + { + state.basis[static_cast(inner + 1)] = column_work; + scale(Complex(1.0 / next_norm, 0.0), state.basis[static_cast(inner + 1)]); + } + + const int used_inner = inner + 1; + state.least_squares + = solve_gmres_least_squares(state.hessenberg, used_inner + 1, used_inner, state.beta); + state.result.absolute_residual = state.least_squares.residual_norm; + state.result.relative_residual = state.result.absolute_residual / state.norm_floor; + state.result.iterations += 1; + if (state.result.relative_residual <= options.residual_tol + || next_norm <= options.breakdown_tol) + { + add_krylov_update(state.preconditioned_basis, + state.least_squares.coefficients, + solution[column]); + state.result.converged = state.result.relative_residual <= options.residual_tol; + state.active = false; + } + } + } + + std::vector restart_columns; + Matrix restart_solutions; + for (const std::size_t column: active_columns) + { + ColumnState& state = states[column]; + if (!state.active) + { + continue; + } + add_krylov_update(state.preconditioned_basis, state.least_squares.coefficients, solution[column]); + restart_columns.push_back(column); + restart_solutions.push_back(solution[column]); + } + if (restart_columns.empty()) + { + continue; + } + + Matrix restart_ax; + problem.apply(restart_solutions, restart_ax); + validate_batch_output(restart_solutions, + restart_ax, + "SternheimerRPA::solve_gmres_batch restart apply"); + for (std::size_t active_index = 0; active_index != restart_columns.size(); ++active_index) + { + ColumnState& state = states[restart_columns[active_index]]; +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != vector_size; ++i) + { + state.residual[i] = rhs[restart_columns[active_index]][i] - restart_ax[active_index][i]; + } + state.beta = norm(state.residual); + state.result.absolute_residual = state.beta; + state.result.relative_residual = state.beta / state.norm_floor; + if (state.result.relative_residual <= options.residual_tol) + { + state.result.converged = true; + state.active = false; + } + } + } + + std::vector results; + results.reserve(states.size()); + for (const ColumnState& state: states) + { + results.push_back(state.result); + } + return results; +} + +void SternheimerRPA::build_rhs_from_hartree_perturbation(const std::vector& hartree_potential_r, + const Vector& psi_r, + Vector& rhs_r) +{ + if (hartree_potential_r.size() != psi_r.size()) + { + throw std::invalid_argument("SternheimerRPA::build_rhs_from_hartree_perturbation size mismatch."); + } + rhs_r.resize(psi_r.size()); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != psi_r.size(); ++ir) + { + rhs_r[ir] = -hartree_potential_r[ir] * psi_r[ir]; + } +} + +void SternheimerRPA::build_rhs_from_hartree_perturbation(const Vector& hartree_potential_r, + const Vector& psi_r, + Vector& rhs_r) +{ + if (hartree_potential_r.size() != psi_r.size()) + { + throw std::invalid_argument("SternheimerRPA::build_rhs_from_hartree_perturbation size mismatch."); + } + rhs_r.resize(psi_r.size()); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != psi_r.size(); ++ir) + { + rhs_r[ir] = -hartree_potential_r[ir] * psi_r[ir]; + } +} + +SternheimerRPA::Complex SternheimerRPA::accumulate_polarizability_grid_element( + const std::vector& hartree_potential_r, + const Vector& psi_r, + const Vector& delta_psi_r, + const double grid_weight) +{ + if (hartree_potential_r.size() != psi_r.size() || psi_r.size() != delta_psi_r.size()) + { + throw std::invalid_argument("SternheimerRPA::accumulate_polarizability_grid_element size mismatch."); + } + double real_part = 0.0; + double imag_part = 0.0; +#pragma omp parallel for reduction(+ : real_part, imag_part) schedule(static) + for (std::size_t ir = 0; ir != psi_r.size(); ++ir) + { + const Complex term = std::conj(psi_r[ir]) * hartree_potential_r[ir] * delta_psi_r[ir]; + real_part += term.real(); + imag_part += term.imag(); + } + return grid_weight * Complex(real_part, imag_part); +} + +SternheimerRPA::Complex SternheimerRPA::accumulate_polarizability_grid_element( + const Vector& hartree_potential_r, + const Vector& psi_r, + const Vector& delta_psi_r, + const double grid_weight) +{ + if (hartree_potential_r.size() != psi_r.size() || psi_r.size() != delta_psi_r.size()) + { + throw std::invalid_argument("SternheimerRPA::accumulate_polarizability_grid_element size mismatch."); + } + double real_part = 0.0; + double imag_part = 0.0; +#pragma omp parallel for reduction(+ : real_part, imag_part) schedule(static) + for (std::size_t ir = 0; ir != psi_r.size(); ++ir) + { + const Complex term = std::conj(psi_r[ir]) * std::conj(hartree_potential_r[ir]) * delta_psi_r[ir]; + real_part += term.real(); + imag_part += term.imag(); + } + return grid_weight * Complex(real_part, imag_part); +} + +void SternheimerRPA::accumulate_chi0_branch_column(const std::vector>& hartree_potentials_r, + const Vector& psi_r, + const Vector& delta_psi_r, + const double grid_weight, + const double occupation, + const int column_index, + std::vector& branch_matrix) +{ + const int num_channels = static_cast(hartree_potentials_r.size()); + if (num_channels <= 0) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column requires at least one channel."); + } + if (column_index < 0 || column_index >= num_channels) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column column index is out of range."); + } + if (branch_matrix.size() + != checked_mul_size(static_cast(num_channels), + static_cast(num_channels), + "Sternheimer chi0 branch matrix size")) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column matrix size mismatch."); + } + + for (int row = 0; row != num_channels; ++row) + { + branch_matrix[static_cast(row) * static_cast(num_channels) + + static_cast(column_index)] + += occupation * accumulate_polarizability_grid_element(hartree_potentials_r[static_cast(row)], + psi_r, + delta_psi_r, + grid_weight); + } +} + +void SternheimerRPA::accumulate_chi0_branch_column(const std::vector& hartree_potentials_r, + const Vector& psi_r, + const Vector& delta_psi_r, + const double grid_weight, + const double occupation, + const int column_index, + std::vector& branch_matrix) +{ + const int num_channels = static_cast(hartree_potentials_r.size()); + if (num_channels <= 0) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column requires at least one channel."); + } + if (column_index < 0 || column_index >= num_channels) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column column index is out of range."); + } + if (branch_matrix.size() + != checked_mul_size(static_cast(num_channels), + static_cast(num_channels), + "Sternheimer chi0 branch matrix size")) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column matrix size mismatch."); + } + + for (int row = 0; row != num_channels; ++row) + { + branch_matrix[static_cast(row) * static_cast(num_channels) + + static_cast(column_index)] + += occupation * accumulate_polarizability_grid_element(hartree_potentials_r[static_cast(row)], + psi_r, + delta_psi_r, + grid_weight); + } +} + +void SternheimerRPA::accumulate_chi0_branch_column(const std::vector& channels, + const Vector& psi_r, + const Vector& delta_psi_r, + const double grid_weight, + const double occupation, + const int column_index, + std::vector& branch_matrix) +{ + const int num_channels = static_cast(channels.size()); + if (num_channels <= 0) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column requires at least one channel."); + } + if (column_index < 0 || column_index >= num_channels) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column column index is out of range."); + } + if (branch_matrix.size() + != checked_mul_size(static_cast(num_channels), + static_cast(num_channels), + "Sternheimer chi0 branch matrix size")) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_column matrix size mismatch."); + } + for (int row = 0; row != num_channels; ++row) + { + branch_matrix[static_cast(row) * static_cast(num_channels) + + static_cast(column_index)] + += occupation + * accumulate_polarizability_grid_element(channels[static_cast(row)].potential_r, + psi_r, + delta_psi_r, + grid_weight); + } +} + +void SternheimerRPA::accumulate_chi0_branch_columns(const std::vector& channels, + const Vector& psi_r, + const std::vector& delta_psi_batch, + const double grid_weight, + const double occupation, + const int column_begin, + std::vector& branch_matrix) +{ + const int num_channels = static_cast(channels.size()); + const int batch_size = static_cast(delta_psi_batch.size()); + if (num_channels <= 0 || batch_size <= 0) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_columns requires channels and responses."); + } + if (column_begin < 0 || column_begin + batch_size > num_channels) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_columns column range is out of bounds."); + } + if (branch_matrix.size() + != checked_mul_size(static_cast(num_channels), + static_cast(num_channels), + "Sternheimer chi0 branch matrix size")) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_columns matrix size mismatch."); + } + for (const Vector* delta_psi_r: delta_psi_batch) + { + if (delta_psi_r == nullptr) + { + throw std::invalid_argument("SternheimerRPA::accumulate_chi0_branch_columns received a null response."); + } + assert_same_size(psi_r, *delta_psi_r, "SternheimerRPA::accumulate_chi0_branch_columns"); + } + for (const auto& channel: channels) + { + assert_same_size(channel.potential_r, psi_r, "SternheimerRPA::accumulate_chi0_branch_columns"); + } + +#pragma omp parallel + { + std::vector values(static_cast(batch_size)); +#pragma omp for schedule(static) + for (int row = 0; row != num_channels; ++row) + { + std::fill(values.begin(), values.end(), Complex(0.0, 0.0)); + const Vector& potential_r = channels[static_cast(row)].potential_r; + if (batch_size == 2) + { + Complex first(0.0, 0.0); + Complex second(0.0, 0.0); + const Vector& first_response = *delta_psi_batch[0]; + const Vector& second_response = *delta_psi_batch[1]; + for (std::size_t ir = 0; ir != psi_r.size(); ++ir) + { + const Complex probe = std::conj(psi_r[ir]) * std::conj(potential_r[ir]); + first += probe * first_response[ir]; + second += probe * second_response[ir]; + } + values[0] = first; + values[1] = second; + } + else + { + for (std::size_t ir = 0; ir != psi_r.size(); ++ir) + { + const Complex probe = std::conj(psi_r[ir]) * std::conj(potential_r[ir]); + for (int offset = 0; offset != batch_size; ++offset) + { + values[static_cast(offset)] + += probe * (*delta_psi_batch[static_cast(offset)])[ir]; + } + } + } + for (int offset = 0; offset != batch_size; ++offset) + { + branch_matrix[static_cast(row) * static_cast(num_channels) + + static_cast(column_begin + offset)] + += occupation * (grid_weight * values[static_cast(offset)]); + } + } + } +} + +std::vector SternheimerRPA::symmetrize_chi0_imaginary_frequency( + const std::vector& branch_matrix, + const int num_channels) +{ + if (num_channels <= 0) + { + throw std::invalid_argument("SternheimerRPA::symmetrize_chi0_imaginary_frequency requires channels."); + } + if (branch_matrix.size() + != checked_mul_size(static_cast(num_channels), + static_cast(num_channels), + "Sternheimer chi0 branch matrix size")) + { + throw std::invalid_argument("SternheimerRPA::symmetrize_chi0_imaginary_frequency matrix size mismatch."); + } + + std::vector chi0(branch_matrix.size(), Complex(0.0, 0.0)); + for (int row = 0; row != num_channels; ++row) + { + for (int col = 0; col != num_channels; ++col) + { + const std::size_t index + = static_cast(row) * static_cast(num_channels) + + static_cast(col); + const std::size_t transpose + = static_cast(col) * static_cast(num_channels) + + static_cast(row); + chi0[index] = branch_matrix[index] + std::conj(branch_matrix[transpose]); + } + } + return chi0; +} + +std::int32_t SternheimerRPA::chi0_v1_marker() +{ + return kChi0V1Marker; +} + +int SternheimerRPA::frequency_owner_rank(const int ifrequency_zero_based, + const int mpi_ranks, + const int rank_shift) +{ + if (ifrequency_zero_based < 0) + { + throw std::invalid_argument("SternheimerRPA::frequency_owner_rank requires a non-negative frequency index."); + } + if (mpi_ranks <= 0) + { + throw std::invalid_argument("SternheimerRPA::frequency_owner_rank requires a positive MPI rank count."); + } + if (mpi_ranks == 1) + { + return 0; + } + + int normalized_shift = rank_shift % mpi_ranks; + if (normalized_shift < 0) + { + normalized_shift += mpi_ranks; + } + return (ifrequency_zero_based + normalized_shift) % mpi_ranks; +} + +SternheimerRPA::FrequencyMPIAssignment SternheimerRPA::frequency_mpi_assignment( + const int ifrequency_zero_based, + const int frequency_count, + const int mpi_ranks, + const int mpi_rank, + const int rank_shift, + const bool use_channel_mpi) +{ + if (frequency_count <= 0 || ifrequency_zero_based < 0 || ifrequency_zero_based >= frequency_count) + { + throw std::invalid_argument( + "SternheimerRPA::frequency_mpi_assignment requires a valid frequency index and count."); + } + if (mpi_ranks <= 0 || mpi_rank < 0 || mpi_rank >= mpi_ranks) + { + throw std::invalid_argument( + "SternheimerRPA::frequency_mpi_assignment requires a valid MPI rank and rank count."); + } + + FrequencyMPIAssignment assignment; + if (!use_channel_mpi) + { + assignment.frequency_leader_rank = frequency_owner_rank(ifrequency_zero_based, mpi_ranks, rank_shift); + assignment.frequency_group_size = 1; + assignment.owns_frequency = mpi_rank == assignment.frequency_leader_rank; + assignment.frequency_group_local_rank = assignment.owns_frequency ? 0 : -1; + return assignment; + } + + if (mpi_ranks < frequency_count || mpi_ranks % frequency_count != 0) + { + throw std::invalid_argument( + "Sternheimer channel MPI requires MPI ranks to be an integer multiple of the frequency count."); + } + + assignment.frequency_group_size = mpi_ranks / frequency_count; + const int frequency_slot = frequency_owner_rank(ifrequency_zero_based, frequency_count, rank_shift); + assignment.frequency_leader_rank = frequency_slot * assignment.frequency_group_size; + const int frequency_group_end = assignment.frequency_leader_rank + assignment.frequency_group_size; + assignment.owns_frequency + = mpi_rank >= assignment.frequency_leader_rank && mpi_rank < frequency_group_end; + assignment.frequency_group_local_rank + = assignment.owns_frequency ? mpi_rank - assignment.frequency_leader_rank : -1; + return assignment; +} + +int SternheimerRPA::channel_group_owner(const int occupied_state, + const int auxiliary_channel, + const int auxiliary_channel_count, + const int frequency_group_size) +{ + if (occupied_state < 0 || auxiliary_channel < 0 || auxiliary_channel_count <= 0 + || auxiliary_channel >= auxiliary_channel_count || frequency_group_size <= 0) + { + throw std::invalid_argument("SternheimerRPA::channel_group_owner received invalid dimensions or indices."); + } + const std::int64_t equation_index = static_cast(occupied_state) * auxiliary_channel_count + + auxiliary_channel; + return static_cast(equation_index % frequency_group_size); +} + +int SternheimerRPA::global_equation_owner(const int occupied_state, + const int frequency_index, + const int auxiliary_channel, + const int frequency_count, + const int auxiliary_channel_count, + const int mpi_ranks, + const int rank_shift) +{ + if (occupied_state < 0 || frequency_count <= 0 || frequency_index < 0 + || frequency_index >= frequency_count || auxiliary_channel_count <= 0 || auxiliary_channel < 0 + || auxiliary_channel >= auxiliary_channel_count || mpi_ranks <= 0) + { + throw std::invalid_argument("SternheimerRPA::global_equation_owner received invalid dimensions or indices."); + } + + constexpr std::int64_t max_task_index = std::numeric_limits::max(); + if (static_cast(occupied_state) + > (max_task_index - frequency_index) / frequency_count) + { + throw std::overflow_error("SternheimerRPA::global_equation_owner task index overflow."); + } + const std::int64_t occupied_frequency + = static_cast(occupied_state) * frequency_count + frequency_index; + if (occupied_frequency > (max_task_index - auxiliary_channel) / auxiliary_channel_count) + { + throw std::overflow_error("SternheimerRPA::global_equation_owner task index overflow."); + } + const std::int64_t task_index = occupied_frequency * auxiliary_channel_count + auxiliary_channel; + + int normalized_shift = rank_shift % mpi_ranks; + if (normalized_shift < 0) + { + normalized_shift += mpi_ranks; + } + return static_cast((task_index % mpi_ranks + normalized_shift) % mpi_ranks); +} + +void SternheimerRPA::validate_mpi_layout(const std::string& layout, + const bool use_frequency_mpi, + const bool use_channel_mpi, + const bool write_siab, + const bool write_librpa, + const int frequency_count, + const int mpi_ranks) +{ + if (layout != "frequency_grouped" && layout != "global_equation") + { + throw std::invalid_argument("Sternheimer MPI layout must be frequency_grouped or global_equation."); + } + if (frequency_count <= 0 || mpi_ranks <= 0) + { + throw std::invalid_argument("Sternheimer MPI layout requires positive frequency and MPI rank counts."); + } + if (use_channel_mpi && !use_frequency_mpi) + { + throw std::invalid_argument("sternheimer_channel_mpi requires sternheimer_frequency_mpi=true."); + } + if (!write_siab && !write_librpa) + { + throw std::invalid_argument("Sternheimer MPI layout requires an enabled output target."); + } + + if (layout == "global_equation") + { + if (!use_frequency_mpi || !use_channel_mpi) + { + throw std::invalid_argument( + "sternheimer_mpi_layout=global_equation requires frequency MPI and channel MPI."); + } + return; + } + + if (use_channel_mpi && (mpi_ranks < frequency_count || mpi_ranks % frequency_count != 0)) + { + throw std::invalid_argument( + "Sternheimer channel MPI requires MPI ranks to be an integer multiple of the frequency count."); + } +} + +SternheimerRPA::TransitionEnergyWindow SternheimerRPA::transition_energy_window_from_eigenvalues_ry( + const std::vector& eigenvalues_ry, + const std::vector& occupations, + const double occupation_tolerance) +{ + TransitionEnergyWindow window; + if (!try_transition_energy_window_from_eigenvalues_ry(eigenvalues_ry, + occupations, + window, + occupation_tolerance)) + { + throw std::runtime_error("Sternheimer minimax window found no positive occupied-to-empty transition."); + } + return window; +} + +bool SternheimerRPA::try_transition_energy_window_from_eigenvalues_ry( + const std::vector& eigenvalues_ry, + const std::vector& occupations, + TransitionEnergyWindow& window, + const double occupation_tolerance) +{ + if (eigenvalues_ry.size() != occupations.size()) + { + throw std::invalid_argument("Sternheimer minimax window requires matching eigenvalue and occupation sizes."); + } + if (eigenvalues_ry.empty()) + { + throw std::invalid_argument("Sternheimer minimax window requires at least one eigenvalue."); + } + if (occupation_tolerance < 0.0) + { + throw std::invalid_argument("Sternheimer minimax occupation tolerance must be non-negative."); + } + + double emin_ry = std::numeric_limits::max(); + double emax_ry = 0.0; + bool found_transition = false; + for (std::size_t iv = 0; iv != eigenvalues_ry.size(); ++iv) + { + if (occupations[iv] <= occupation_tolerance) + { + continue; + } + for (std::size_t ic = 0; ic != eigenvalues_ry.size(); ++ic) + { + if (occupations[ic] > occupation_tolerance) + { + continue; + } + const double transition_ry = eigenvalues_ry[ic] - eigenvalues_ry[iv]; + if (transition_ry <= 0.0) + { + continue; + } + emin_ry = std::min(emin_ry, transition_ry); + emax_ry = std::max(emax_ry, transition_ry); + found_transition = true; + } + } + + if (!found_transition) + { + return false; + } + + window.emin_ha = 0.5 * emin_ry; + window.emax_ha = 0.5 * emax_ry; + return true; +} + +SternheimerRPA::TransitionEnergyWindow SternheimerRPA::merge_transition_energy_windows( + const std::vector& windows) +{ + if (windows.empty()) + { + throw std::invalid_argument("Sternheimer minimax window merge requires at least one spin channel."); + } + + TransitionEnergyWindow merged = windows.front(); + for (const TransitionEnergyWindow& window: windows) + { + if (window.emin_ha <= 0.0 || window.emax_ha < window.emin_ha) + { + throw std::invalid_argument("Sternheimer minimax spin window is invalid."); + } + merged.emin_ha = std::min(merged.emin_ha, window.emin_ha); + merged.emax_ha = std::max(merged.emax_ha, window.emax_ha); + } + return merged; +} + +SternheimerRPA::FrequencyGrid SternheimerRPA::generate_greenx_minimax_frequency_grid(const int nfreq, + const double emin_ha, + const double emax_ha) +{ + if (nfreq <= 0) + { + throw std::invalid_argument("Sternheimer minimax grid requires nfreq > 0."); + } + if (emin_ha <= 0.0 || emax_ha <= 0.0 || emax_ha < emin_ha) + { + throw std::invalid_argument("Sternheimer minimax grid requires 0 < emin <= emax in Hartree."); + } + + FrequencyGrid grid; + grid.omega_ha.assign(static_cast(nfreq), 0.0); + grid.weights_ha.assign(static_cast(nfreq), 0.0); + +#ifdef __GREENX_MINIMAX + int nfreq_c = nfreq; + double emin_c = emin_ha; + double emax_c = emax_ha; + int ierr = -1; + gx_minimax_grid_frequency_wrp(&nfreq_c, + &emin_c, + &emax_c, + grid.omega_ha.data(), + grid.weights_ha.data(), + &ierr); + if (ierr != 0) + { + throw std::runtime_error("GreenX minimax frequency-grid generation failed with ierr=" + + std::to_string(ierr) + "."); + } +#else + throw std::runtime_error("ABACUS was built without ENABLE_GREENX_MINIMAX; Sternheimer minimax is unavailable."); +#endif + + return grid; +} + +SternheimerRPA::FrequencyGrid SternheimerRPA::read_frequency_grid_file(const std::string& filename, + const int expected_size) +{ + if (expected_size <= 0) + { + throw std::invalid_argument("Sternheimer frequency grid file requires expected_size > 0."); + } + + std::ifstream in(filename.c_str()); + if (!in.good()) + { + throw std::runtime_error("Failed to open Sternheimer frequency grid file " + filename + "."); + } + + FrequencyGrid grid; + std::string line; + int line_number = 0; + while (std::getline(in, line)) + { + ++line_number; + const std::size_t comment = line.find('#'); + if (comment != std::string::npos) + { + line.resize(comment); + } + + std::istringstream iss(line); + std::vector columns; + double value = 0.0; + while (iss >> value) + { + columns.push_back(value); + } + if (!iss.eof()) + { + throw std::invalid_argument("Invalid token in Sternheimer frequency grid file " + filename + + " at line " + std::to_string(line_number) + "."); + } + if (columns.empty()) + { + continue; + } + if (columns.size() != 2 && columns.size() != 3) + { + throw std::invalid_argument("Sternheimer frequency grid file " + filename + + " expects rows with 2 columns (omega_Ha weight_Ha) or 3 columns " + "(index omega_Ha weight_Ha)."); + } + + double omega_ha = columns[0]; + double weight_ha = columns[1]; + if (columns.size() == 3) + { + const int expected_index = static_cast(grid.omega_ha.size()) + 1; + if (std::abs(columns[0] - static_cast(expected_index)) > 1.0e-10) + { + throw std::invalid_argument("Sternheimer frequency grid file " + filename + + " has an unexpected one-based index at line " + + std::to_string(line_number) + "."); + } + omega_ha = columns[1]; + weight_ha = columns[2]; + } + if (omega_ha <= 0.0 || weight_ha <= 0.0) + { + throw std::invalid_argument("Sternheimer frequency grid file " + filename + + " requires positive omega_Ha and weight_Ha."); + } + + grid.omega_ha.push_back(omega_ha); + grid.weights_ha.push_back(weight_ha); + } + + if (static_cast(grid.omega_ha.size()) != expected_size) + { + throw std::runtime_error("Sternheimer frequency grid file " + filename + " contains " + + std::to_string(grid.omega_ha.size()) + " rows but sternheimer_nfreq is " + + std::to_string(expected_size) + "."); + } + return grid; +} + +void SternheimerRPA::write_chi0_v1_file(const std::string& filename, + const Chi0V1Metadata& metadata, + const std::vector& channels, + const std::vector& chi0_matrix) +{ + if (metadata.iq <= 0 || metadata.ifrequency <= 0) + { + throw std::invalid_argument("Sternheimer chi0 v1 metadata requires positive iq and frequency indices."); + } + if (metadata.atom_naux.empty()) + { + throw std::invalid_argument("Sternheimer chi0 v1 metadata requires atom auxiliary sizes."); + } + const int naux = sum_positive_sizes(metadata.atom_naux, "Sternheimer chi0 v1 atom_naux"); + if (channels.size() != static_cast(naux)) + { + throw std::invalid_argument("Sternheimer chi0 v1 channel count does not match atom_naux."); + } + const std::size_t matrix_size = checked_mul_size(static_cast(naux), + static_cast(naux), + "Sternheimer chi0 v1 matrix size"); + if (chi0_matrix.size() != matrix_size) + { + throw std::invalid_argument("Sternheimer chi0 v1 matrix size mismatch."); + } + + const std::vector> atom_local_to_global + = map_atom_local_to_global(channels, metadata.atom_naux); + const std::size_t natom = metadata.atom_naux.size(); + std::vector blocks; + blocks.reserve(natom * (natom + 1) / 2); + for (std::size_t iatom = 0; iatom != natom; ++iatom) + { + for (std::size_t jatom = iatom; jatom != natom; ++jatom) + { + Chi0Block block; + block.pair_index = checked_i32_from_size(upper_triangular_pair_index(iatom, jatom, natom), + "Sternheimer chi0 v1 atom-pair index"); + const int inaux = metadata.atom_naux[iatom]; + const int jnaux = metadata.atom_naux[jatom]; + block.payload.reserve(static_cast(inaux) * static_cast(jnaux)); + for (int imu = 0; imu != inaux; ++imu) + { + const int iglobal = atom_local_to_global[iatom][static_cast(imu)]; + for (int jmu = 0; jmu != jnaux; ++jmu) + { + const int jglobal = atom_local_to_global[jatom][static_cast(jmu)]; + block.payload.push_back(chi0_matrix[static_cast(iglobal) + * static_cast(naux) + + static_cast(jglobal)]); + } + } + blocks.push_back(std::move(block)); + } + } + + const std::int32_t nblocks = checked_i32_from_size(blocks.size(), "Sternheimer chi0 v1 block count"); + std::int64_t offset = 6 * static_cast(sizeof(std::int32_t)) + + 2 * static_cast(sizeof(double)) + + static_cast(sizeof(std::int32_t)) + + static_cast(metadata.atom_naux.size() * sizeof(std::int32_t)) + + static_cast(blocks.size()) + * static_cast(sizeof(std::int32_t) + sizeof(std::int64_t)); + for (Chi0Block& block: blocks) + { + block.offset = offset; + const std::size_t bytes = checked_mul_size(block.payload.size(), + sizeof(Complex), + "Sternheimer chi0 v1 block payload"); + offset += checked_i64_from_size(bytes, "Sternheimer chi0 v1 block payload"); + } + + static_assert(sizeof(Complex) == 2 * sizeof(double), + "Sternheimer chi0 v1 output expects complex as two doubles."); + std::ofstream out(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); + if (!out.good()) + { + throw std::runtime_error("Failed to open " + filename); + } + + const std::int32_t iq = metadata.iq; + const std::int32_t ifrequency = metadata.ifrequency; + const std::int32_t naux_i32 = naux; + const std::int32_t natom_i32 = checked_i32_from_size(natom, "Sternheimer chi0 v1 atom count"); + write_scalar(out, kChi0V1Marker, filename); + write_scalar(out, iq, filename); + write_scalar(out, ifrequency, filename); + write_scalar(out, naux_i32, filename); + write_scalar(out, kChi0V1ComplexFlag, filename); + write_scalar(out, natom_i32, filename); + write_scalar(out, metadata.omega, filename); + write_scalar(out, metadata.weight, filename); + write_scalar(out, nblocks, filename); + for (const int atom_aux: metadata.atom_naux) + { + const std::int32_t atom_aux_i32 = atom_aux; + write_scalar(out, atom_aux_i32, filename); + } + for (const Chi0Block& block: blocks) + { + const std::int32_t pair_index = block.pair_index; + write_scalar(out, pair_index, filename); + write_scalar(out, block.offset, filename); + } + for (const Chi0Block& block: blocks) + { + checked_write(out, block.payload.data(), block.payload.size() * sizeof(Complex), filename); + } + out.close(); +} + +SternheimerRPA::Complex SternheimerRPA::local_grid_dot(const Vector& lhs, const Vector& rhs, const double grid_weight) +{ + assert_same_size(lhs, rhs, "SternheimerRPA::local_grid_dot"); + double real_part = 0.0; + double imag_part = 0.0; +#pragma omp parallel for reduction(+ : real_part, imag_part) schedule(static) + for (std::size_t ir = 0; ir != lhs.size(); ++ir) + { + const Complex term = std::conj(lhs[ir]) * rhs[ir]; + real_part += term.real(); + imag_part += term.imag(); + } + return grid_weight * Complex(real_part, imag_part); +} + +SternheimerSubspaceProjector::SternheimerSubspaceProjector(const std::vector& subspace, Dot dot) + : SternheimerSubspaceProjector(make_subspace_views(subspace), std::move(dot)) +{ +} + +SternheimerSubspaceProjector::SternheimerSubspaceProjector(std::vector subspace, Dot dot) + : subspace_(std::move(subspace)), dot_(std::move(dot)) +{ + if (!dot_) + { + throw std::invalid_argument("SternheimerSubspaceProjector requires a dot callback."); + } + for (const Vector* basis: subspace_) + { + if (basis == nullptr) + { + throw std::invalid_argument("SternheimerSubspaceProjector requires non-null basis views."); + } + } + for (const Vector* basis: subspace_) + { + assert_same_size(*subspace_.front(), *basis, "SternheimerSubspaceProjector basis"); + } + basis_norms_.reserve(subspace_.size()); + for (const Vector* basis: subspace_) + { + basis_norms_.push_back(dot_(*basis, *basis)); + } +} + +SternheimerSubspaceProjector::SternheimerSubspaceProjector(const std::vector& subspace, + const double grid_weight) + : SternheimerSubspaceProjector(make_subspace_views(subspace), grid_weight, false) +{ +} + +SternheimerSubspaceProjector::SternheimerSubspaceProjector(const std::vector& subspace, + const double grid_weight, + const bool orthonormal_batch) + : SternheimerSubspaceProjector(make_subspace_views(subspace), grid_weight, orthonormal_batch) +{ +} + +SternheimerSubspaceProjector::SternheimerSubspaceProjector(std::vector subspace, + const double grid_weight) + : SternheimerSubspaceProjector(std::move(subspace), grid_weight, false) +{ +} + +SternheimerSubspaceProjector::SternheimerSubspaceProjector(std::vector subspace, + const double grid_weight, + const bool orthonormal_batch) + : SternheimerSubspaceProjector(std::move(subspace), [grid_weight](const Vector& lhs, const Vector& rhs) { + return SternheimerRPA::local_grid_dot(lhs, rhs, grid_weight); + }) +{ + if (grid_weight <= 0.0) + { + throw std::invalid_argument("SternheimerSubspaceProjector requires a positive grid weight."); + } + batch_grid_weight_ = grid_weight; + use_direct_batch_dot_ = true; + use_orthonormal_batch_projection_ = orthonormal_batch; + if (use_orthonormal_batch_projection_ && !subspace_.empty()) + { + const std::size_t grid_size = subspace_.front()->size(); + const std::size_t basis_size = subspace_.size(); + if (grid_size != 0 && basis_size > basis_by_grid_.max_size() / grid_size) + { + throw std::overflow_error("SternheimerSubspaceProjector packed basis size overflow."); + } + basis_by_grid_.resize(grid_size * basis_size); +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != grid_size; ++ir) + { + for (std::size_t basis = 0; basis != basis_size; ++basis) + { + basis_by_grid_[ir * basis_size + basis] = (*subspace_[basis])[ir]; + } + } + } +} + +std::size_t SternheimerSubspaceProjector::basis_size() const +{ + return subspace_.size(); +} + +std::size_t SternheimerSubspaceProjector::storage_bytes() const +{ + return subspace_.capacity() * sizeof(const Vector*) + + (basis_norms_.capacity() + basis_by_grid_.capacity()) * sizeof(Complex); +} + +void SternheimerSubspaceProjector::project(Vector& vec) const +{ + for (std::size_t index = 0; index != subspace_.size(); ++index) + { + const Vector& basis_vec = *subspace_[index]; + assert_same_size(basis_vec, vec, "SternheimerSubspaceProjector::project"); + const Complex norm = basis_norms_[index]; + if (std::abs(norm) == 0.0) + { + continue; + } + const Complex coeff = dot_(basis_vec, vec) / norm; +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i != vec.size(); ++i) + { + vec[i] -= coeff * basis_vec[i]; + } + } +} + +void SternheimerSubspaceProjector::project_batch(std::vector& vectors) const +{ + if (vectors.empty() || subspace_.empty()) + { + return; + } + for (const Vector& vector: vectors) + { + assert_same_size(*subspace_.front(), vector, "SternheimerSubspaceProjector::project_batch"); + } + if (use_orthonormal_batch_projection_) + { + const std::size_t basis_size = subspace_.size(); + const std::size_t column_size = vectors.size(); + const std::size_t grid_size = vectors.front().size(); + std::vector coefficients(basis_size * column_size, Complex(0.0, 0.0)); +#pragma omp parallel for schedule(static) + for (std::size_t basis = 0; basis != basis_size; ++basis) + { + Complex* coefficient_row = coefficients.data() + basis * column_size; + // The borrowed column is contiguous; the packed transpose is for reconstruction. + const Vector& basis_column = *subspace_[basis]; + for (std::size_t ir = 0; ir != grid_size; ++ir) + { + const Complex conjugate_basis = std::conj(basis_column[ir]); + for (std::size_t column = 0; column != column_size; ++column) + { + coefficient_row[column] += conjugate_basis * vectors[column][ir]; + } + } + const Complex scale = std::abs(basis_norms_[basis]) == 0.0 + ? Complex(0.0, 0.0) + : Complex(batch_grid_weight_, 0.0) / basis_norms_[basis]; + for (std::size_t column = 0; column != column_size; ++column) + { + coefficient_row[column] *= scale; + } + } +#pragma omp parallel for schedule(static) + for (std::size_t ir = 0; ir != grid_size; ++ir) + { + const Complex* basis_row = basis_by_grid_.data() + ir * basis_size; + for (std::size_t column = 0; column != column_size; ++column) + { + Complex correction(0.0, 0.0); + for (std::size_t basis = 0; basis != basis_size; ++basis) + { + correction += coefficients[basis * column_size + column] * basis_row[basis]; + } + vectors[column][ir] -= correction; + } + } + return; + } + for (std::size_t index = 0; index != subspace_.size(); ++index) + { + const Vector& basis_vec = *subspace_[index]; + const Complex norm = basis_norms_[index]; + std::vector coefficients(vectors.size(), Complex(0.0, 0.0)); + if (use_direct_batch_dot_) + { +#pragma omp parallel for schedule(static) + for (std::size_t column = 0; column != vectors.size(); ++column) + { + Complex overlap(0.0, 0.0); + for (std::size_t i = 0; i != basis_vec.size(); ++i) + { + overlap += std::conj(basis_vec[i]) * vectors[column][i]; + } + if (std::abs(norm) != 0.0) + { + coefficients[column] = batch_grid_weight_ * overlap / norm; + } + } + } + else + { + for (std::size_t column = 0; column != vectors.size(); ++column) + { + assert_same_size(basis_vec, vectors[column], "SternheimerSubspaceProjector::project_batch"); + if (std::abs(norm) != 0.0) + { + coefficients[column] = dot_(basis_vec, vectors[column]) / norm; + } + } + } + if (std::abs(norm) == 0.0) + { + continue; + } +#pragma omp parallel for schedule(static) + for (std::size_t column = 0; column != vectors.size(); ++column) + { + for (std::size_t i = 0; i != basis_vec.size(); ++i) + { + vectors[column][i] -= coefficients[column] * basis_vec[i]; + } + } + } +} + +void SternheimerRPA::project_out_subspace(const std::vector& subspace, + const std::function& dot, + Vector& vec) +{ + SternheimerSubspaceProjector(subspace, dot).project(vec); +} + +void SternheimerRPA::apply_kinetic_preconditioner(const std::vector& kinetic_energy, + const double eigenvalue, + const double omega, + const double eta, + const Vector& input, + Vector& output) +{ + if (kinetic_energy.size() != input.size()) + { + throw std::invalid_argument("SternheimerRPA::apply_kinetic_preconditioner size mismatch."); + } + output.resize(input.size()); +#pragma omp parallel for schedule(static) + for (std::size_t ig = 0; ig != input.size(); ++ig) + { + const Complex denominator(kinetic_energy[ig] - eigenvalue + eta, omega); + output[ig] = input[ig] / denominator; + } +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h new file mode 100644 index 00000000000..17c0f852118 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h @@ -0,0 +1,290 @@ +#ifndef STERNHEIMER_RPA_H +#define STERNHEIMER_RPA_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" + +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +class SternheimerSubspaceProjector +{ + public: + using Complex = std::complex; + using Vector = std::vector; + using Matrix = std::vector; + using Dot = std::function; + + SternheimerSubspaceProjector(const std::vector& subspace, Dot dot); + SternheimerSubspaceProjector(const std::vector& subspace, double grid_weight); + SternheimerSubspaceProjector(const std::vector& subspace, + double grid_weight, + bool orthonormal_batch); + // Borrowed vectors must remain alive, at stable addresses and unchanged. + // The pointer list is owned; an orthonormal batch projector packs the basis + // once. Concurrent projections require distinct outputs and a thread-safe Dot. + SternheimerSubspaceProjector(std::vector subspace, Dot dot); + SternheimerSubspaceProjector(std::vector subspace, double grid_weight); + SternheimerSubspaceProjector(std::vector subspace, + double grid_weight, + bool orthonormal_batch); + + void project(Vector& vec) const; + void project_batch(std::vector& vectors) const; + std::size_t basis_size() const; + // Owned vector capacities only; excludes borrowed grids and callback storage. + std::size_t storage_bytes() const; + + private: + std::vector subspace_; + Dot dot_; + std::vector basis_norms_; + double batch_grid_weight_ = 0.0; + bool use_direct_batch_dot_ = false; + bool use_orthonormal_batch_projection_ = false; + std::vector basis_by_grid_; +}; + +class SternheimerRPA +{ + public: + using Complex = std::complex; + using Vector = std::vector; + using Matrix = std::vector; + + struct SolverOptions + { + int max_iter = 300; + double residual_tol = 1.0e-10; + double breakdown_tol = 1.0e-14; + bool use_fd_spectral_preconditioner = true; + double fd_spectral_preconditioner_regularization = 0.0; + }; + + struct SolverResult + { + bool converged = false; + int iterations = 0; + double absolute_residual = 0.0; + double relative_residual = 0.0; + }; + + struct AuxiliaryChannel + { + int channel_index = -1; + int atom_index = -1; + int atom_local_index = -1; + }; + + struct Chi0V1Metadata + { + int iq = 1; + int ifrequency = 1; + double omega = 0.0; + double weight = 1.0; + std::vector atom_naux; + }; + + struct CoulombV1Matrix + { + int iq = 0; + std::vector atom_naux; + std::vector values; + }; + + struct TransitionEnergyWindow + { + double emin_ha = 0.0; + double emax_ha = 0.0; + }; + + struct FrequencyGrid + { + std::vector omega_ha; + std::vector weights_ha; + }; + + struct FrequencyMPIAssignment + { + bool owns_frequency = false; + int frequency_leader_rank = -1; + int frequency_group_size = 0; + int frequency_group_local_rank = -1; + }; + + struct LinearProblem + { + // Applies Pc (H - eps_i + i omega) Pc to a band-limited grid/PW vector. + std::function apply; + + // Applies a right preconditioner. If unset, identity is used. + std::function precondition; + + // Global inner product, including MPI reduction when running parallel. + std::function dot; + }; + + struct BatchLinearProblem + { + // Applies the same linear operator to independent active columns. + std::function apply; + + // Applies the same right preconditioner to independent active columns. + std::function precondition; + + // Per-column global inner product, including MPI reduction when needed. + std::function dot; + }; + + static SolverResult solve_bicgstab(const LinearProblem& problem, const Vector& rhs, Vector& solution); + + static SolverResult solve_bicgstab(const LinearProblem& problem, + const Vector& rhs, + Vector& solution, + const SolverOptions& options); + + static SolverResult solve_gmres(const LinearProblem& problem, + const Vector& rhs, + Vector& solution, + const SolverOptions& options, + int restart_dimension = 50); + + static std::vector solve_gmres_batch(const BatchLinearProblem& problem, + const Matrix& rhs, + Matrix& solution, + const SolverOptions& options, + int restart_dimension = 50); + + static void build_rhs_from_hartree_perturbation(const std::vector& hartree_potential_r, + const Vector& psi_r, + Vector& rhs_r); + + static void build_rhs_from_hartree_perturbation(const Vector& hartree_potential_r, + const Vector& psi_r, + Vector& rhs_r); + + static Complex accumulate_polarizability_grid_element(const std::vector& hartree_potential_r, + const Vector& psi_r, + const Vector& delta_psi_r, + double grid_weight); + + static Complex accumulate_polarizability_grid_element(const Vector& hartree_potential_r, + const Vector& psi_r, + const Vector& delta_psi_r, + double grid_weight); + + static void accumulate_chi0_branch_column(const std::vector>& hartree_potentials_r, + const Vector& psi_r, + const Vector& delta_psi_r, + double grid_weight, + double occupation, + int column_index, + std::vector& branch_matrix); + + static void accumulate_chi0_branch_column(const std::vector& hartree_potentials_r, + const Vector& psi_r, + const Vector& delta_psi_r, + double grid_weight, + double occupation, + int column_index, + std::vector& branch_matrix); + + static void accumulate_chi0_branch_column(const std::vector& channels, + const Vector& psi_r, + const Vector& delta_psi_r, + double grid_weight, + double occupation, + int column_index, + std::vector& branch_matrix); + + static void accumulate_chi0_branch_columns(const std::vector& channels, + const Vector& psi_r, + const std::vector& delta_psi_batch, + double grid_weight, + double occupation, + int column_begin, + std::vector& branch_matrix); + + static std::vector symmetrize_chi0_imaginary_frequency(const std::vector& branch_matrix, + int num_channels); + + static std::int32_t chi0_v1_marker(); + + static TransitionEnergyWindow transition_energy_window_from_eigenvalues_ry( + const std::vector& eigenvalues_ry, + const std::vector& occupations, + double occupation_tolerance = 1.0e-8); + + static bool try_transition_energy_window_from_eigenvalues_ry( + const std::vector& eigenvalues_ry, + const std::vector& occupations, + TransitionEnergyWindow& window, + double occupation_tolerance = 1.0e-8); + + static TransitionEnergyWindow merge_transition_energy_windows( + const std::vector& windows); + + static FrequencyGrid generate_greenx_minimax_frequency_grid(int nfreq, double emin_ha, double emax_ha); + + static FrequencyGrid read_frequency_grid_file(const std::string& filename, int expected_size); + + static int frequency_owner_rank(int ifrequency_zero_based, int mpi_ranks, int rank_shift = 0); + + static FrequencyMPIAssignment frequency_mpi_assignment(int ifrequency_zero_based, + int frequency_count, + int mpi_ranks, + int mpi_rank, + int rank_shift, + bool use_channel_mpi); + + static int channel_group_owner(int occupied_state, + int auxiliary_channel, + int auxiliary_channel_count, + int frequency_group_size); + + static int global_equation_owner(int occupied_state, + int frequency_index, + int auxiliary_channel, + int frequency_count, + int auxiliary_channel_count, + int mpi_ranks, + int rank_shift = 0); + + static void validate_mpi_layout(const std::string& layout, + bool use_frequency_mpi, + bool use_channel_mpi, + bool write_siab, + bool write_librpa, + int frequency_count, + int mpi_ranks); + + static void write_chi0_v1_file(const std::string& filename, + const Chi0V1Metadata& metadata, + const std::vector& channels, + const std::vector& chi0_matrix); + + static CoulombV1Matrix read_coulomb_v1_files(const std::vector& filenames); + + static Complex local_grid_dot(const Vector& lhs, const Vector& rhs, double grid_weight); + + static void project_out_subspace(const std::vector& subspace, + const std::function& dot, + Vector& vec); + + static void apply_kinetic_preconditioner(const std::vector& kinetic_energy, + double eigenvalue, + double omega, + double eta, + const Vector& input, + Vector& output); +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.cpp new file mode 100644 index 00000000000..451312c4f5f --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.cpp @@ -0,0 +1,165 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.h" + +#include "source_io/module_parameter/input_parameter.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +bool validate_sternheimer_molecular_coulomb(const Input_para& input) +{ + if (input.sternheimer_molecular_coulomb == "none") + { + if (!input.sternheimer_ao_file.empty()) + { + throw std::invalid_argument( + "sternheimer_ao_file requires sternheimer_molecular_coulomb isolated_ri."); + } + return false; + } + if (input.sternheimer_molecular_coulomb != "isolated_ri") + { + throw std::invalid_argument("sternheimer_molecular_coulomb must be none or isolated_ri."); + } + if (input.sternheimer_ao_file.empty() || input.basis_type != "lcao" || !input.sternheimer_delta + || !input.out_sternheimer_librpa || input.out_sternheimer_siab || input.sternheimer_q_index != 0 + || input.symmetry != "-1") + { + throw std::invalid_argument("isolated_ri requires an AO tensor, LCAO Delta-ST LibRPA output, q_index 0, " + "symmetry -1 and no SIAB output. Gamma alone does not declare an isolated system."); + } + if (input.exx_singularity_correction != "limits" || input.out_v2d_method != "ewald" + || input.out_v3d_method != "ewald") + { + throw std::invalid_argument("isolated_ri requires the complete free-space Center2/limits producer metric, " + "not a periodic Ewald or direct-reciprocal metric."); + } + if (input.exx_rotate_abfs || input.exx_coul_moment || input.shrink_abfs_pca_thr >= 0.0 || input.cal_force) + { + throw std::invalid_argument( + "isolated_ri does not yet support rotated, moment-split or shrunken ABFS, or forces; " + "producer and response must use the same finalized radial basis."); + } + return true; +} + +bool sternheimer_environment_flag(const char* name, const bool default_value) +{ + if (name == nullptr || name[0] == '\0') + { + throw std::invalid_argument("Sternheimer environment flag name must not be empty."); + } + + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_value; + } + + std::string value(raw); + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char character) { + return static_cast(std::tolower(character)); + }); + if (value == "1" || value == "true" || value == "on" || value == "yes") + { + return true; + } + if (value == "0" || value == "false" || value == "off" || value == "no") + { + return false; + } + + throw std::invalid_argument("Invalid boolean value for " + std::string(name) + ": " + value); +} + +int sternheimer_channel_batch_width() +{ + constexpr const char* name = "ABACUS_STERNHEIMER_CHANNEL_BATCH_WIDTH"; + constexpr int default_width = 2; + constexpr int maximum_width = 64; + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_width; + } + + errno = 0; + char* end = nullptr; + const long parsed = std::strtol(raw, &end, 10); + if (errno != 0 || end == raw || end == nullptr || *end != '\0' || parsed <= 0 || parsed > maximum_width + || parsed > std::numeric_limits::max()) + { + throw std::invalid_argument("Invalid channel batch width in " + std::string(name) + ": " + raw); + } + return static_cast(parsed); +} + +SternheimerWeakPreconditionerMode sternheimer_weak_preconditioner_mode() +{ + constexpr const char* name = "ABACUS_STERNHEIMER_WEAK_PRECONDITIONER"; + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return SternheimerWeakPreconditionerMode::Spectral; + } + + std::string value(raw); + std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char character) { + return static_cast(std::tolower(character)); + }); + if (value == "none") + { + return SternheimerWeakPreconditionerMode::None; + } + if (value == "spectral" || value == "fd_spectral") + { + return SternheimerWeakPreconditionerMode::Spectral; + } + throw std::invalid_argument("Invalid weak preconditioner in " + + std::string(name) + ": " + value); +} + +const char* sternheimer_weak_preconditioner_name( + const SternheimerWeakPreconditionerMode mode) noexcept +{ + switch (mode) + { + case SternheimerWeakPreconditionerMode::None: + return "none"; + case SternheimerWeakPreconditionerMode::Spectral: + return "fd_spectral"; + } + return "invalid"; +} + +double sternheimer_weak_residual_tolerance() +{ + constexpr const char* name = "ABACUS_STERNHEIMER_WEAK_RESIDUAL_TOL"; + constexpr double default_tolerance = 1.0e-6; + const char* raw = std::getenv(name); + if (raw == nullptr) + { + return default_tolerance; + } + + errno = 0; + char* end = nullptr; + const double parsed = std::strtod(raw, &end); + if (errno != 0 || end == raw || end == nullptr || *end != '\0' + || !std::isfinite(parsed) || parsed <= 0.0) + { + throw std::invalid_argument("Invalid weak residual tolerance in " + + std::string(name) + ": " + raw); + } + return parsed; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.h new file mode 100644 index 00000000000..04ec18dba9c --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.h @@ -0,0 +1,27 @@ +#ifndef STERNHEIMER_RUNTIME_OPTIONS_H +#define STERNHEIMER_RUNTIME_OPTIONS_H + +struct Input_para; + +namespace ModuleRI +{ + +enum class SternheimerWeakPreconditionerMode +{ + None, + Spectral +}; + +bool sternheimer_environment_flag(const char* name, bool default_value); +int sternheimer_channel_batch_width(); +SternheimerWeakPreconditionerMode sternheimer_weak_preconditioner_mode(); +const char* sternheimer_weak_preconditioner_name( + SternheimerWeakPreconditionerMode mode) noexcept; +double sternheimer_weak_residual_tolerance(); + +// Validate the declared isolated response, not a geometry inferred from Gamma sampling. +bool validate_sternheimer_molecular_coulomb(const Input_para& input); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_data.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_data.h new file mode 100644 index 00000000000..67d88a182e5 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_data.h @@ -0,0 +1,70 @@ +#ifndef STERNHEIMER_SIAB_DATA_H +#define STERNHEIMER_SIAB_DATA_H + +#include +#include +#include + +namespace module_ri +{ +namespace sternheimer_siab +{ + +struct PrimitiveBlock +{ + std::string element; + int atom_index; + int l; + int m; + int n_primitive; + int offset; +}; + +struct ReferenceRow +{ + int occupied_state; + int auxiliary_channel; + double frequency_ha; + double occupation; + double frequency_weight; + double norm; + std::vector> q; + int frequency_index; +}; + +// The eight fields below are required-core v1; Task 4 may append deterministic optional provenance fields. +struct Provenance +{ + std::string abacus_commit; + std::string auxiliary_basis_sha256; + std::vector cell_bohr; ///< complete 3x3 row-major lattice vectors in Bohr + double ecut_ry; + std::string kernel; + std::string orbital_sha256; + std::string pseudopotential_sha256; + std::string spin_convention; + + // Task 4 production fields. Keeping these unset preserves the exact Task 3 canonical fixture. + std::string executable_sha256; + double exx_pca_thr = -1.0; + int sternheimer_nfreq = 0; + std::vector frequency_ha; + std::vector frequency_weights_ha; + int mpi_ranks = 0; + int omp_threads = 0; + + // Global full-Coulomb whitening fields for SIAB-only production targets. + std::string auxiliary_whitening; + int raw_auxiliary_dimension = 0; + int whitened_auxiliary_rank = 0; + int discarded_auxiliary_rank = 0; + double coulomb_relative_threshold = -1.0; + std::vector coulomb_eigenvalues; + double coulomb_max_orthonormality_error = -1.0; + std::string coulomb_transform_sha256; +}; + +} // namespace sternheimer_siab +} // namespace module_ri + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.cpp new file mode 100644 index 00000000000..f349a4d3c70 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.cpp @@ -0,0 +1,67 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.h" + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" + +#include +#include +#include +#include + +namespace ModuleRI +{ + +SternheimerSIABMemoryEstimate estimate_sternheimer_siab_dense_memory(const int grid_size, + const int raw_auxiliary_channels, + const int response_channels, + const int primitive_count, + const int reciprocal_count, + const int occupied_states, + const int frequency_count) +{ + if (grid_size <= 0 || raw_auxiliary_channels <= 0 || response_channels <= 0 || primitive_count <= 0 + || reciprocal_count <= 0 || occupied_states <= 0 || frequency_count <= 0) + { + throw std::invalid_argument("Sternheimer SIAB memory estimate requires positive dimensions."); + } + const auto multiply = [](const std::uint64_t left, const std::uint64_t right) { + if (right != 0 && left > std::numeric_limits::max() / right) + { + throw std::overflow_error("Sternheimer SIAB memory estimate overflowed uint64_t."); + } + return left * right; + }; + const auto add = [](const std::uint64_t left, const std::uint64_t right) { + if (left > std::numeric_limits::max() - right) + { + throw std::overflow_error("Sternheimer SIAB memory estimate overflowed uint64_t."); + } + return left + right; + }; + + SternheimerSIABMemoryEstimate result; + result.coulomb_metric_bytes = multiply(multiply(raw_auxiliary_channels, raw_auxiliary_channels), sizeof(double)); + result.transformed_potential_bytes = multiply(multiply(grid_size, response_channels), sizeof(double)); + const std::uint64_t transform_chunk = static_cast(sternheimer_abfs_transform_grid_chunk); + const std::uint64_t transform_columns + = add(static_cast(raw_auxiliary_channels), + static_cast(response_channels)); + result.channel_transform_workspace_bytes + = add(multiply(multiply(transform_chunk, transform_columns), sizeof(double)), + multiply(transform_chunk, sizeof(std::size_t))); + result.reciprocal_primitive_bytes + = multiply(multiply(primitive_count, reciprocal_count), sizeof(std::complex)); + result.primitive_overlap_bytes + = multiply(multiply(primitive_count, primitive_count), sizeof(std::complex)); + const std::uint64_t row_bytes + = add(7U * sizeof(double), multiply(primitive_count, sizeof(std::complex))); + const std::uint64_t row_count + = multiply(multiply(occupied_states, response_channels), frequency_count); + result.gathered_reference_row_bytes = multiply(3U, multiply(row_count, row_bytes)); + result.total_bytes = add(add(add(result.coulomb_metric_bytes, result.transformed_potential_bytes), + result.channel_transform_workspace_bytes), + add(result.reciprocal_primitive_bytes, + add(result.primitive_overlap_bytes, result.gathered_reference_row_bytes))); + return result; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.h new file mode 100644 index 00000000000..14d2eed0ce3 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_memory.h @@ -0,0 +1,30 @@ +#ifndef STERNHEIMER_SIAB_MEMORY_H +#define STERNHEIMER_SIAB_MEMORY_H + +#include + +namespace ModuleRI +{ + +struct SternheimerSIABMemoryEstimate +{ + std::uint64_t coulomb_metric_bytes = 0; + std::uint64_t transformed_potential_bytes = 0; + std::uint64_t channel_transform_workspace_bytes = 0; + std::uint64_t reciprocal_primitive_bytes = 0; + std::uint64_t primitive_overlap_bytes = 0; + std::uint64_t gathered_reference_row_bytes = 0; + std::uint64_t total_bytes = 0; +}; + +SternheimerSIABMemoryEstimate estimate_sternheimer_siab_dense_memory(int grid_size, + int raw_auxiliary_channels, + int response_channels, + int primitive_count, + int reciprocal_count, + int occupied_states, + int frequency_count); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.cpp new file mode 100644 index 00000000000..2c2f53fc064 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.cpp @@ -0,0 +1,365 @@ +#include "sternheimer_siab_mpi.h" + +#include +#include +#include +#include +#include + +namespace module_ri +{ +namespace sternheimer_siab +{ +namespace +{ + +using Complex = std::complex; + +std::vector sorted_rows(std::vector rows) +{ + const auto key = [](const ReferenceRow& row) { + return std::make_tuple(row.occupied_state, row.auxiliary_channel, row.frequency_index); + }; + std::sort(rows.begin(), rows.end(), [&key](const ReferenceRow& left, const ReferenceRow& right) { + return key(left) < key(right); + }); + for (std::size_t index = 1; index < rows.size(); ++index) + { + if (key(rows[index - 1]) == key(rows[index])) + { + throw std::invalid_argument("Sternheimer SIAB gathered reference rows contain a duplicate key."); + } + } + return rows; +} + +bool valid_local_rows(const std::vector& rows, const std::size_t nprimitive) +{ + for (const ReferenceRow& row: rows) + { + if (row.q.size() != nprimitive || row.occupied_state < 0 || row.auxiliary_channel < 0 || row.frequency_index < 0 + || !std::isfinite(row.frequency_ha) || !std::isfinite(row.occupation) + || !std::isfinite(row.frequency_weight) || !std::isfinite(row.norm)) + { + return false; + } + for (const Complex& value: row.q) + { + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + { + return false; + } + } + } + return true; +} + +std::size_t row_width(const std::size_t nprimitive) +{ + if (nprimitive > (std::numeric_limits::max() - 7) / 2) + { + throw std::overflow_error("Sternheimer SIAB reference row width overflows size_t."); + } + return 7 + 2 * nprimitive; +} + +std::vector pack_rows(const std::vector& rows, const std::size_t nprimitive) +{ + const std::size_t width = row_width(nprimitive); + std::vector packed; + packed.reserve(rows.size() * width); + for (const ReferenceRow& row: rows) + { + packed.push_back(static_cast(row.occupied_state)); + packed.push_back(static_cast(row.auxiliary_channel)); + packed.push_back(static_cast(row.frequency_index)); + packed.push_back(row.frequency_ha); + packed.push_back(row.occupation); + packed.push_back(row.frequency_weight); + packed.push_back(row.norm); + for (const Complex& value: row.q) + { + packed.push_back(value.real()); + packed.push_back(value.imag()); + } + } + return packed; +} + +int unpack_index(const double value) +{ + if (!std::isfinite(value) || value < 0.0 || value > static_cast(std::numeric_limits::max()) + || value != std::floor(value)) + { + throw std::runtime_error("Sternheimer SIAB gathered reference row contains an invalid integer index."); + } + return static_cast(value); +} + +std::vector unpack_rows(const std::vector& packed, const std::size_t nprimitive) +{ + const std::size_t width = row_width(nprimitive); + if (packed.size() % width != 0) + { + throw std::runtime_error("Sternheimer SIAB gathered reference row payload has an invalid size."); + } + std::vector rows(packed.size() / width); + for (std::size_t irow = 0; irow != rows.size(); ++irow) + { + const std::size_t offset = irow * width; + ReferenceRow& row = rows[irow]; + row.occupied_state = unpack_index(packed[offset]); + row.auxiliary_channel = unpack_index(packed[offset + 1]); + row.frequency_index = unpack_index(packed[offset + 2]); + row.frequency_ha = packed[offset + 3]; + row.occupation = packed[offset + 4]; + row.frequency_weight = packed[offset + 5]; + row.norm = packed[offset + 6]; + row.q.resize(nprimitive); + for (std::size_t ie = 0; ie != nprimitive; ++ie) + { + row.q[ie] = Complex(packed[offset + 7 + 2 * ie], packed[offset + 8 + 2 * ie]); + } + } + return sorted_rows(std::move(rows)); +} + +} // namespace + +std::vector> assemble_full_primitive_grids(const std::vector& slabs, + const int nxy, + const int nz) +{ + if (nxy <= 0 || nz <= 0 || slabs.empty()) + { + throw std::invalid_argument("Sternheimer SIAB primitive assembly requires positive grid dimensions and slabs."); + } + const std::size_t nprimitive = slabs.front().values.size(); + if (nprimitive == 0) + { + throw std::invalid_argument("Sternheimer SIAB primitive assembly requires at least one primitive."); + } + + std::vector plane_coverage(static_cast(nz), 0); + std::vector> full( + nprimitive, + std::vector(static_cast(nxy) * static_cast(nz), Complex(0.0, 0.0))); + for (const PrimitiveSlab& slab: slabs) + { + if (slab.startz < 0 || slab.nplane < 0 || slab.startz + slab.nplane > nz || slab.values.size() != nprimitive) + { + throw std::invalid_argument("Sternheimer SIAB primitive slab metadata is inconsistent."); + } + const std::size_t local_size = static_cast(nxy) * static_cast(slab.nplane); + for (const auto& primitive: slab.values) + { + if (primitive.size() != local_size) + { + throw std::invalid_argument("Sternheimer SIAB primitive slab payload size is inconsistent."); + } + } + for (int iz = 0; iz != slab.nplane; ++iz) + { + ++plane_coverage[static_cast(slab.startz + iz)]; + } + for (std::size_t ie = 0; ie != nprimitive; ++ie) + { + for (int ixy = 0; ixy != nxy; ++ixy) + { + for (int iz = 0; iz != slab.nplane; ++iz) + { + full[ie][static_cast(ixy) * static_cast(nz) + + static_cast(slab.startz + iz)] + = slab.values[ie][static_cast(ixy) * static_cast(slab.nplane) + + static_cast(iz)]; + } + } + } + } + if (!std::all_of(plane_coverage.begin(), plane_coverage.end(), [](const int count) { return count == 1; })) + { + throw std::invalid_argument("Sternheimer SIAB primitive slabs must cover every PW z plane exactly once."); + } + return full; +} + +#ifdef __MPI +std::vector> allgather_full_primitive_grids( + const std::vector>& local_primitives, + const int nxy, + const int nz, + const int startz, + const int nplane, + MPI_Comm communicator) +{ + int rank_count = 0; + MPI_Comm_size(communicator, &rank_count); + const bool local_valid + = nxy > 0 && nz > 0 && startz >= 0 && nplane >= 0 && startz + nplane <= nz && !local_primitives.empty() + && std::all_of(local_primitives.begin(), local_primitives.end(), [nxy, nplane](const auto& values) { + return values.size() == static_cast(nxy) * static_cast(nplane); + }); + int valid = local_valid ? 1 : 0; + MPI_Allreduce(MPI_IN_PLACE, &valid, 1, MPI_INT, MPI_MIN, communicator); + if (valid == 0) + { + throw std::invalid_argument("Sternheimer SIAB local primitive slab is invalid on at least one MPI rank."); + } + + const int local_nprimitive = static_cast(local_primitives.size()); + int min_nprimitive = local_nprimitive; + int max_nprimitive = local_nprimitive; + MPI_Allreduce(MPI_IN_PLACE, &min_nprimitive, 1, MPI_INT, MPI_MIN, communicator); + MPI_Allreduce(MPI_IN_PLACE, &max_nprimitive, 1, MPI_INT, MPI_MAX, communicator); + if (min_nprimitive != max_nprimitive) + { + throw std::invalid_argument("Sternheimer SIAB primitive counts differ between MPI ranks."); + } + + const std::size_t local_grid_size = static_cast(nxy) * static_cast(nplane); + const std::size_t local_payload_size = local_grid_size * local_primitives.size(); + if (local_payload_size > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Sternheimer SIAB local primitive payload exceeds MPI int count capacity."); + } + std::vector local_payload; + local_payload.reserve(local_payload_size); + for (const auto& primitive: local_primitives) + { + local_payload.insert(local_payload.end(), primitive.begin(), primitive.end()); + } + + std::vector startz_by_rank(static_cast(rank_count)); + std::vector nplane_by_rank(static_cast(rank_count)); + std::vector counts(static_cast(rank_count)); + const int local_count = static_cast(local_payload_size); + MPI_Allgather(&startz, 1, MPI_INT, startz_by_rank.data(), 1, MPI_INT, communicator); + MPI_Allgather(&nplane, 1, MPI_INT, nplane_by_rank.data(), 1, MPI_INT, communicator); + MPI_Allgather(&local_count, 1, MPI_INT, counts.data(), 1, MPI_INT, communicator); + + std::vector displacements(static_cast(rank_count), 0); + std::size_t total_count = 0; + for (int rank = 0; rank != rank_count; ++rank) + { + if (total_count > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Sternheimer SIAB primitive allgather exceeds MPI int displacement capacity."); + } + displacements[static_cast(rank)] = static_cast(total_count); + total_count += static_cast(counts[static_cast(rank)]); + } + std::vector gathered(total_count); + MPI_Allgatherv(local_payload.data(), + local_count, + MPI_DOUBLE_COMPLEX, + gathered.data(), + counts.data(), + displacements.data(), + MPI_DOUBLE_COMPLEX, + communicator); + + std::vector slabs(static_cast(rank_count)); + for (int rank = 0; rank != rank_count; ++rank) + { + PrimitiveSlab& slab = slabs[static_cast(rank)]; + slab.startz = startz_by_rank[static_cast(rank)]; + slab.nplane = nplane_by_rank[static_cast(rank)]; + const std::size_t rank_grid_size = static_cast(nxy) * static_cast(slab.nplane); + slab.values.resize(static_cast(local_nprimitive)); + for (int ie = 0; ie != local_nprimitive; ++ie) + { + const std::size_t begin = static_cast(displacements[static_cast(rank)]) + + static_cast(ie) * rank_grid_size; + slab.values[static_cast(ie)].assign(gathered.begin() + begin, + gathered.begin() + begin + rank_grid_size); + } + } + return assemble_full_primitive_grids(slabs, nxy, nz); +} + +std::vector gather_reference_rows_to_root(const std::vector& local_rows, + const std::size_t nprimitive, + const int root, + MPI_Comm communicator) +{ + int rank = 0; + int rank_count = 0; + MPI_Comm_rank(communicator, &rank); + MPI_Comm_size(communicator, &rank_count); + if (root < 0 || root >= rank_count) + { + throw std::invalid_argument("Sternheimer SIAB row gather root is out of range."); + } + int valid = valid_local_rows(local_rows, nprimitive) ? 1 : 0; + MPI_Allreduce(MPI_IN_PLACE, &valid, 1, MPI_INT, MPI_MIN, communicator); + if (valid == 0) + { + throw std::invalid_argument("Sternheimer SIAB local reference rows are invalid on at least one MPI rank."); + } + + const std::vector local_packed = pack_rows(local_rows, nprimitive); + if (local_packed.size() > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Sternheimer SIAB local row payload exceeds MPI int count capacity."); + } + const int local_count = static_cast(local_packed.size()); + std::vector counts(rank == root ? static_cast(rank_count) : 0); + MPI_Gather(&local_count, 1, MPI_INT, counts.data(), 1, MPI_INT, root, communicator); + + std::vector displacements; + std::vector gathered; + if (rank == root) + { + displacements.assign(static_cast(rank_count), 0); + std::size_t total_count = 0; + for (int source_rank = 0; source_rank != rank_count; ++source_rank) + { + if (total_count > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Sternheimer SIAB row gather exceeds MPI int displacement capacity."); + } + displacements[static_cast(source_rank)] = static_cast(total_count); + total_count += static_cast(counts[static_cast(source_rank)]); + } + gathered.resize(total_count); + } + MPI_Gatherv(local_packed.data(), + local_count, + MPI_DOUBLE, + gathered.data(), + counts.data(), + displacements.data(), + MPI_DOUBLE, + root, + communicator); + return rank == root ? unpack_rows(gathered, nprimitive) : std::vector(); +} +#else +std::vector> allgather_full_primitive_grids( + const std::vector>& local_primitives, + const int nxy, + const int nz, + const int startz, + const int nplane) +{ + PrimitiveSlab slab; + slab.startz = startz; + slab.nplane = nplane; + slab.values = local_primitives; + return assemble_full_primitive_grids({slab}, nxy, nz); +} + +std::vector gather_reference_rows_to_root(const std::vector& local_rows, + const std::size_t nprimitive, + const int root) +{ + if (root != 0 || !valid_local_rows(local_rows, nprimitive)) + { + throw std::invalid_argument("Sternheimer SIAB serial reference row gather input is invalid."); + } + return sorted_rows(local_rows); +} +#endif + +} // namespace sternheimer_siab +} // namespace module_ri diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.h new file mode 100644 index 00000000000..88f918285ab --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.h @@ -0,0 +1,59 @@ +#ifndef STERNHEIMER_SIAB_MPI_H +#define STERNHEIMER_SIAB_MPI_H + +#include "sternheimer_siab_data.h" + +#include +#include + +#ifdef __MPI +#include +#endif + +namespace module_ri +{ +namespace sternheimer_siab +{ + +struct PrimitiveSlab +{ + int startz = 0; + int nplane = 0; + /// Primitive-major values in PW local order [primitive][ixy][local iz]. + std::vector>> values; +}; + +std::vector>> assemble_full_primitive_grids(const std::vector& slabs, + int nxy, + int nz); + +#ifdef __MPI +std::vector>> allgather_full_primitive_grids( + const std::vector>>& local_primitives, + int nxy, + int nz, + int startz, + int nplane, + MPI_Comm communicator); + +std::vector gather_reference_rows_to_root(const std::vector& local_rows, + std::size_t nprimitive, + int root, + MPI_Comm communicator); +#else +std::vector>> allgather_full_primitive_grids( + const std::vector>>& local_primitives, + int nxy, + int nz, + int startz, + int nplane); + +std::vector gather_reference_rows_to_root(const std::vector& local_rows, + std::size_t nprimitive, + int root); +#endif + +} // namespace sternheimer_siab +} // namespace module_ri + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.cpp new file mode 100644 index 00000000000..42b63ec1bb7 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.cpp @@ -0,0 +1,183 @@ +#include "sternheimer_siab_overlap.h" + +#include "source_base/module_external/blas_connector.h" + +#include +#include +#include + +namespace module_ri +{ +namespace sternheimer_siab +{ +namespace +{ + +using Complex = std::complex; +using GridVector = std::vector; +using PrimitiveGrid = std::vector; + +void validate_delta_omega(const double delta_omega) +{ + if (!std::isfinite(delta_omega) || delta_omega <= 0.0) + { + throw std::invalid_argument("Sternheimer SIAB overlap requires a positive finite grid volume element."); + } +} + +std::size_t validate_primitives(const PrimitiveGrid& primitives) +{ + if (primitives.empty()) + { + throw std::invalid_argument("Sternheimer SIAB overlap requires a non-empty primitive basis."); + } + + const std::size_t grid_size = primitives.front().size(); + for (const auto& primitive : primitives) + { + if (primitive.size() != grid_size) + { + throw std::invalid_argument("Sternheimer SIAB primitive grids must have equal sizes."); + } + } + return grid_size; +} + +Complex dot(const GridVector& left, const GridVector& right, const double delta_omega) +{ + Complex result(0.0, 0.0); + for (std::size_t ir = 0; ir != left.size(); ++ir) + { + result += std::conj(left[ir]) * right[ir]; + } + return result * delta_omega; +} + +} // namespace + +double norm(const GridVector& y, const double delta_omega) +{ + validate_delta_omega(delta_omega); + + double result = 0.0; + for (const Complex& value : y) + { + result += std::norm(value); + } + return result * delta_omega; +} + +std::vector overlap_q(const GridVector& y, + const PrimitiveGrid& primitives, + const double delta_omega) +{ + validate_delta_omega(delta_omega); + if (validate_primitives(primitives) != y.size()) + { + throw std::invalid_argument("Sternheimer SIAB reference and primitive grid sizes differ."); + } + + std::vector result; + result.reserve(primitives.size()); + for (const auto& primitive : primitives) + { + result.push_back(dot(y, primitive, delta_omega)); + } + return result; +} + +std::vector overlap_s(const PrimitiveGrid& primitives, const double delta_omega) +{ + validate_delta_omega(delta_omega); + validate_primitives(primitives); + + const std::size_t nprimitive = primitives.size(); + std::vector result(nprimitive * nprimitive); + for (std::size_t i = 0; i != nprimitive; ++i) + { + for (std::size_t j = i; j != nprimitive; ++j) + { + const Complex value = dot(primitives[i], primitives[j], delta_omega); + result[i * nprimitive + j] = value; + if (i != j) + { + result[j * nprimitive + i] = std::conj(value); + } + } + } + return result; +} + +std::vector overlap_q_reciprocal(const GridVector& response_coefficients, + const PrimitiveGrid& primitive_coefficients) +{ + if (validate_primitives(primitive_coefficients) != response_coefficients.size()) + { + throw std::invalid_argument( + "Sternheimer SIAB response and primitive reciprocal coefficient sizes differ."); + } + std::vector result; + result.reserve(primitive_coefficients.size()); + for (const GridVector& primitive: primitive_coefficients) + { + result.push_back(dot(response_coefficients, primitive, 1.0)); + } + return result; +} + +std::vector overlap_q_reciprocal_contiguous( + const GridVector& response_coefficients, + const GridVector& primitive_coefficients, + const int primitive_count, + const int reciprocal_count) +{ + if (primitive_count <= 0 || reciprocal_count <= 0 + || response_coefficients.size() != static_cast(reciprocal_count) + || static_cast(primitive_count) + > std::numeric_limits::max() / static_cast(reciprocal_count) + || primitive_coefficients.size() + != static_cast(primitive_count) * static_cast(reciprocal_count)) + { + throw std::invalid_argument("Sternheimer SIAB contiguous reciprocal coefficient dimensions differ."); + } + + std::vector result(static_cast(primitive_count), Complex(0.0, 0.0)); + // B Y^H gives q_e = sum_G B_eG conj(Y_G), i.e. . + BlasConnector::gemm('N', + 'C', + primitive_count, + 1, + reciprocal_count, + Complex(1.0, 0.0), + primitive_coefficients.data(), + reciprocal_count, + response_coefficients.data(), + reciprocal_count, + Complex(0.0, 0.0), + result.data(), + 1); + return result; +} + +std::vector overlap_s_reciprocal(const PrimitiveGrid& primitive_coefficients) +{ + validate_primitives(primitive_coefficients); + const std::size_t nprimitive = primitive_coefficients.size(); + std::vector result(nprimitive * nprimitive); + for (std::size_t i = 0; i != nprimitive; ++i) + { + for (std::size_t j = i; j != nprimitive; ++j) + { + const Complex value = dot(primitive_coefficients[i], primitive_coefficients[j], 1.0); + result[i * nprimitive + j] = value; + if (i != j) + { + result[j * nprimitive + i] = std::conj(value); + } + } + } + return result; +} + +} // namespace sternheimer_siab +} // namespace module_ri diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.h new file mode 100644 index 00000000000..7bc8b8eaf5b --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.h @@ -0,0 +1,39 @@ +#ifndef STERNHEIMER_SIAB_OVERLAP_H +#define STERNHEIMER_SIAB_OVERLAP_H + +#include +#include + +namespace module_ri +{ +namespace sternheimer_siab +{ + +double norm(const std::vector>& y, double delta_omega); + +std::vector> overlap_q( + const std::vector>& y, + const std::vector>>& primitives, + double delta_omega); + +std::vector> overlap_s( + const std::vector>>& primitives, + double delta_omega); + +std::vector> overlap_q_reciprocal( + const std::vector>& response_coefficients, + const std::vector>>& primitive_coefficients); + +std::vector> overlap_q_reciprocal_contiguous( + const std::vector>& response_coefficients, + const std::vector>& primitive_coefficients, + int primitive_count, + int reciprocal_count); + +std::vector> overlap_s_reciprocal( + const std::vector>>& primitive_coefficients); + +} // namespace sternheimer_siab +} // namespace module_ri + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.cpp new file mode 100644 index 00000000000..e99ed666da0 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.cpp @@ -0,0 +1,387 @@ +#include "sternheimer_siab_provenance.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#endif + +namespace module_ri +{ +namespace sternheimer_siab +{ +namespace +{ + +constexpr std::array kRoundConstants{{ + 0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U, + 0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U, + 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU, + 0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, 0x06ca6351U, 0x14292967U, + 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, + 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U, + 0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, 0x682e6ff3U, + 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U, +}}; + +std::uint32_t rotate_right(const std::uint32_t value, const unsigned int shift) +{ + return (value >> shift) | (value << (32U - shift)); +} + +bool is_hex_string(const std::string& value, const std::size_t size) +{ + return value.size() == size && std::all_of(value.begin(), value.end(), [](const unsigned char character) { + return (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') + || (character >= 'A' && character <= 'F'); + }); +} + +std::string normalized_commit_token(const std::string& value) +{ + std::size_t begin = 0; + while (begin != value.size() && std::isspace(static_cast(value[begin]))) + { + ++begin; + } + std::size_t end = begin; + while (end != value.size() && !std::isspace(static_cast(value[end])) && value[end] != '(') + { + ++end; + } + return value.substr(begin, end - begin); +} + +std::string join_path(const std::string& directory, const std::string& filename) +{ + if (filename.empty() || (!filename.empty() && filename.front() == '/') || directory.empty()) + { + return filename; + } + return directory.back() == '/' ? directory + filename : directory + "/" + filename; +} + +void update_u64(Sha256& digest, const std::uint64_t value) +{ + std::array bytes; + for (std::size_t index = 0; index != bytes.size(); ++index) + { + bytes[index] = static_cast((value >> (56U - 8U * index)) & 0xffU); + } + digest.update(bytes.data(), bytes.size()); +} + +void update_file(Sha256& digest, const std::string& path) +{ + std::ifstream input(path.c_str(), std::ios::binary); + if (!input) + { + throw std::runtime_error("Cannot open required Sternheimer SIAB provenance file: " + path); + } + std::array buffer; + while (input) + { + input.read(buffer.data(), static_cast(buffer.size())); + const std::streamsize count = input.gcount(); + if (count > 0) + { + digest.update(reinterpret_cast(buffer.data()), static_cast(count)); + } + } + if (!input.eof()) + { + throw std::runtime_error("Failed while hashing Sternheimer SIAB provenance file: " + path); + } +} + +std::uint64_t file_size(const std::string& path) +{ + std::ifstream input(path.c_str(), std::ios::binary | std::ios::ate); + if (!input) + { + throw std::runtime_error("Cannot open required Sternheimer SIAB provenance file: " + path); + } + const std::streamoff position = input.tellg(); + if (position < 0) + { + throw std::runtime_error("Cannot determine size of Sternheimer SIAB provenance file: " + path); + } + return static_cast(position); +} + +} // namespace + +Sha256::Sha256() + : state_{{0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU, 0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U}} +{ + buffer_.fill(0); +} + +void Sha256::transform(const unsigned char* block) +{ + std::array words; + for (std::size_t index = 0; index != 16; ++index) + { + words[index] = (static_cast(block[4 * index]) << 24U) + | (static_cast(block[4 * index + 1]) << 16U) + | (static_cast(block[4 * index + 2]) << 8U) + | static_cast(block[4 * index + 3]); + } + for (std::size_t index = 16; index != words.size(); ++index) + { + const std::uint32_t s0 + = rotate_right(words[index - 15], 7) ^ rotate_right(words[index - 15], 18) ^ (words[index - 15] >> 3U); + const std::uint32_t s1 + = rotate_right(words[index - 2], 17) ^ rotate_right(words[index - 2], 19) ^ (words[index - 2] >> 10U); + words[index] = words[index - 16] + s0 + words[index - 7] + s1; + } + + std::uint32_t a = state_[0]; + std::uint32_t b = state_[1]; + std::uint32_t c = state_[2]; + std::uint32_t d = state_[3]; + std::uint32_t e = state_[4]; + std::uint32_t f = state_[5]; + std::uint32_t g = state_[6]; + std::uint32_t h = state_[7]; + for (std::size_t index = 0; index != words.size(); ++index) + { + const std::uint32_t sigma1 = rotate_right(e, 6) ^ rotate_right(e, 11) ^ rotate_right(e, 25); + const std::uint32_t choice = (e & f) ^ (~e & g); + const std::uint32_t temporary1 = h + sigma1 + choice + kRoundConstants[index] + words[index]; + const std::uint32_t sigma0 = rotate_right(a, 2) ^ rotate_right(a, 13) ^ rotate_right(a, 22); + const std::uint32_t majority = (a & b) ^ (a & c) ^ (b & c); + const std::uint32_t temporary2 = sigma0 + majority; + h = g; + g = f; + f = e; + e = d + temporary1; + d = c; + c = b; + b = a; + a = temporary1 + temporary2; + } + state_[0] += a; + state_[1] += b; + state_[2] += c; + state_[3] += d; + state_[4] += e; + state_[5] += f; + state_[6] += g; + state_[7] += h; +} + +void Sha256::update(const unsigned char* data, const std::size_t size) +{ + if (finished_) + { + throw std::logic_error("Cannot update a finalized SHA256 digest."); + } + if (size != 0 && data == nullptr) + { + throw std::invalid_argument("Cannot hash a null non-empty byte range."); + } + if (size > std::numeric_limits::max() - total_size_) + { + throw std::overflow_error("SHA256 input length exceeds uint64 capacity."); + } + total_size_ += static_cast(size); + std::size_t consumed = 0; + if (buffer_size_ != 0) + { + const std::size_t take = std::min(size, buffer_.size() - buffer_size_); + std::memcpy(buffer_.data() + buffer_size_, data, take); + buffer_size_ += take; + consumed += take; + if (buffer_size_ == buffer_.size()) + { + transform(buffer_.data()); + buffer_size_ = 0; + } + } + while (size - consumed >= buffer_.size()) + { + transform(data + consumed); + consumed += buffer_.size(); + } + if (consumed != size) + { + buffer_size_ = size - consumed; + std::memcpy(buffer_.data(), data + consumed, buffer_size_); + } +} + +std::string Sha256::finish() +{ + if (finished_) + { + throw std::logic_error("Cannot finalize a SHA256 digest twice."); + } + if (total_size_ > std::numeric_limits::max() / 8U) + { + throw std::overflow_error("SHA256 bit length exceeds uint64 capacity."); + } + const std::uint64_t bit_size = total_size_ * 8U; + buffer_[buffer_size_++] = 0x80U; + if (buffer_size_ > 56) + { + std::fill(buffer_.begin() + static_cast(buffer_size_), buffer_.end(), 0U); + transform(buffer_.data()); + buffer_size_ = 0; + } + std::fill(buffer_.begin() + static_cast(buffer_size_), buffer_.begin() + 56, 0U); + for (std::size_t index = 0; index != 8; ++index) + { + buffer_[56 + index] = static_cast((bit_size >> (56U - 8U * index)) & 0xffU); + } + transform(buffer_.data()); + finished_ = true; + + std::ostringstream output; + output << std::hex << std::nouppercase << std::setfill('0'); + for (const std::uint32_t word: state_) + { + output << std::setw(8) << word; + } + return output.str(); +} + +std::string sha256_bytes(const std::vector& bytes) +{ + Sha256 digest; + digest.update(bytes.data(), bytes.size()); + return digest.finish(); +} + +std::string sha256_file(const std::string& path) +{ + Sha256 digest; + update_file(digest, path); + return digest.finish(); +} + +std::string sha256_file_manifest(const std::vector& paths) +{ + if (paths.empty()) + { + throw std::invalid_argument("Sternheimer SIAB provenance file manifest must not be empty."); + } + if (paths.size() == 1) + { + return sha256_file(paths.front()); + } + + Sha256 digest; + const std::string marker = "ABACUS_STERNHEIMER_SIAB_FILE_MANIFEST_V1"; + digest.update(reinterpret_cast(marker.data()), marker.size()); + update_u64(digest, static_cast(paths.size())); + for (const std::string& path: paths) + { + update_u64(digest, file_size(path)); + update_file(digest, path); + } + return digest.finish(); +} + +std::string sha256_unique_file_manifest(const std::vector& paths) +{ + if (paths.empty()) + { + throw std::invalid_argument("Sternheimer SIAB provenance file manifest must not be empty."); + } + + std::vector unique_paths; + std::vector unique_hashes; + unique_paths.reserve(paths.size()); + unique_hashes.reserve(paths.size()); + for (const std::string& path: paths) + { + const std::string hash = sha256_file(path); + if (std::find(unique_hashes.begin(), unique_hashes.end(), hash) == unique_hashes.end()) + { + unique_paths.push_back(path); + unique_hashes.push_back(hash); + } + } + return sha256_file_manifest(unique_paths); +} + +std::vector resolve_required_input_files(const std::string& directory, + const std::vector& filenames, + const std::string& label) +{ + if (filenames.empty()) + { + throw std::runtime_error("Sternheimer SIAB production provenance requires at least one " + label + " file."); + } + std::vector paths; + paths.reserve(filenames.size()); + for (const std::string& filename: filenames) + { + if (filename.empty() || filename == "auto") + { + throw std::runtime_error("Sternheimer SIAB production provenance requires an explicit " + label + + " filename, not an empty/auto placeholder."); + } + const std::string path = join_path(directory, filename); + std::ifstream input(path.c_str(), std::ios::binary); + if (!input) + { + throw std::runtime_error("Cannot open required Sternheimer SIAB " + label + " file: " + path); + } + paths.push_back(path); + } + return paths; +} + +double require_single_primitive_rcut(const std::vector& rcuts) +{ + if (rcuts.size() != 1 || !std::isfinite(rcuts.front()) || rcuts.front() <= 0.0) + { + throw std::invalid_argument( + "out_sternheimer_siab requires exactly one explicit positive finite bessel_nao_rcut."); + } + return rcuts.front(); +} + +std::string resolve_executable_path() +{ +#if defined(__linux__) + std::vector path(4096, '\0'); + const ssize_t size = ::readlink("/proc/self/exe", path.data(), path.size() - 1); + if (size <= 0 || static_cast(size) >= path.size()) + { + throw std::runtime_error("Cannot resolve /proc/self/exe for required Sternheimer SIAB executable provenance."); + } + path[static_cast(size)] = '\0'; + return std::string(path.data()); +#else + throw std::runtime_error("Required Sternheimer SIAB executable provenance currently needs Linux /proc/self/exe."); +#endif +} + +std::string require_source_commit(const std::string& compiled_commit) +{ + const std::string selected = normalized_commit_token(compiled_commit); + if (!is_hex_string(selected, 40) && !is_hex_string(selected, 64)) + { + throw std::runtime_error( + "out_sternheimer_siab requires a build configured from an exact Git checkout with full COMMIT metadata."); + } + std::string normalized = selected; + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](const unsigned char character) { + return static_cast(std::tolower(character)); + }); + return normalized; +} + +} // namespace sternheimer_siab +} // namespace module_ri diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.h new file mode 100644 index 00000000000..5c5624d9ea7 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.h @@ -0,0 +1,50 @@ +#ifndef STERNHEIMER_SIAB_PROVENANCE_H +#define STERNHEIMER_SIAB_PROVENANCE_H + +#include +#include +#include +#include +#include + +namespace module_ri +{ +namespace sternheimer_siab +{ + +class Sha256 +{ + public: + Sha256(); + + void update(const unsigned char* data, std::size_t size); + std::string finish(); + + private: + void transform(const unsigned char* block); + + std::array state_; + std::array buffer_; + std::size_t buffer_size_ = 0; + std::uint64_t total_size_ = 0; + bool finished_ = false; +}; + +std::string sha256_bytes(const std::vector& bytes); +std::string sha256_file(const std::string& path); +std::string sha256_file_manifest(const std::vector& paths); +std::string sha256_unique_file_manifest(const std::vector& paths); + +std::vector resolve_required_input_files(const std::string& directory, + const std::vector& filenames, + const std::string& label); + +double require_single_primitive_rcut(const std::vector& rcuts); + +std::string resolve_executable_path(); +std::string require_source_commit(const std::string& compiled_commit); + +} // namespace sternheimer_siab +} // namespace module_ri + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.cpp new file mode 100644 index 00000000000..fbafbb924b4 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.cpp @@ -0,0 +1,852 @@ +#include "sternheimer_siab_writer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace module_ri +{ +namespace sternheimer_siab +{ +namespace +{ + +const double hermitian_tolerance = 1.0e-10; + +bool finite_complex(const std::complex& value) +{ + return std::isfinite(value.real()) && std::isfinite(value.imag()); +} + +bool valid_hex(const std::string& value, const std::size_t length) +{ + if (value.size() != length) + { + return false; + } + return std::all_of(value.begin(), value.end(), [](const char character) { + return (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') + || (character >= 'A' && character <= 'F'); + }); +} + +bool valid_commit(const std::string& value) +{ + return valid_hex(value, 40) || valid_hex(value, 64); +} + +bool ascii_letter(const unsigned char character) +{ + return (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z'); +} + +bool valid_element_token(const std::string& element) +{ + if (element.empty() || !ascii_letter(static_cast(element[0]))) + { + return false; + } + for (std::size_t index = 1; index < element.size(); ++index) + { + const unsigned char character = static_cast(element[index]); + if (!ascii_letter(character) && !(character >= '0' && character <= '9') && character != '_' && character != '+' + && character != '-') + { + return false; + } + } + return true; +} + +bool continuation_byte(const unsigned char byte) +{ + return byte >= 0x80 && byte <= 0xBF; +} + +bool valid_utf8(const std::string& value) +{ + std::size_t position = 0; + while (position < value.size()) + { + const unsigned char first = static_cast(value[position]); + if (first <= 0x7F) + { + ++position; + continue; + } + if (first >= 0xC2 && first <= 0xDF) + { + if (position + 1 >= value.size() || !continuation_byte(static_cast(value[position + 1]))) + { + return false; + } + position += 2; + continue; + } + if (first >= 0xE0 && first <= 0xEF) + { + if (position + 2 >= value.size()) + { + return false; + } + const unsigned char second = static_cast(value[position + 1]); + const unsigned char third = static_cast(value[position + 2]); + if (!continuation_byte(third) || (first == 0xE0 && (second < 0xA0 || second > 0xBF)) + || (first == 0xED && (second < 0x80 || second > 0x9F)) + || ((first != 0xE0 && first != 0xED) && !continuation_byte(second))) + { + return false; + } + position += 3; + continue; + } + if (first >= 0xF0 && first <= 0xF4) + { + if (position + 3 >= value.size()) + { + return false; + } + const unsigned char second = static_cast(value[position + 1]); + const unsigned char third = static_cast(value[position + 2]); + const unsigned char fourth = static_cast(value[position + 3]); + if (!continuation_byte(third) || !continuation_byte(fourth) + || (first == 0xF0 && (second < 0x90 || second > 0xBF)) + || (first == 0xF4 && (second < 0x80 || second > 0x8F)) + || ((first != 0xF0 && first != 0xF4) && !continuation_byte(second))) + { + return false; + } + position += 4; + continue; + } + return false; + } + return true; +} + +void validate_utf8_field(const std::string& value, const std::string& field) +{ + if (!valid_utf8(value)) + { + throw std::invalid_argument("Sternheimer SIAB provenance " + field + " must be valid UTF-8"); + } +} + +std::size_t validate_blocks(const std::vector& blocks) +{ + std::size_t expected_offset = 0; + for (const PrimitiveBlock& block: blocks) + { + if (!valid_element_token(block.element)) + { + throw std::invalid_argument("Sternheimer SIAB primitive block element must match [A-Za-z][A-Za-z0-9_+-]*"); + } + if (block.atom_index < 0 || block.l < 0 || block.m < -block.l || block.m > block.l) + { + throw std::invalid_argument("Sternheimer SIAB primitive block has invalid atom or angular indices"); + } + if (block.n_primitive <= 0 || block.offset < 0 || static_cast(block.offset) != expected_offset) + { + throw std::invalid_argument("Sternheimer SIAB primitive block offsets must be contiguous from zero"); + } + expected_offset += static_cast(block.n_primitive); + } + return expected_offset; +} + +void validate_provenance(const Provenance& provenance) +{ + validate_utf8_field(provenance.abacus_commit, "abacus_commit"); + validate_utf8_field(provenance.auxiliary_basis_sha256, "auxiliary_basis_sha256"); + validate_utf8_field(provenance.kernel, "kernel"); + validate_utf8_field(provenance.orbital_sha256, "orbital_sha256"); + validate_utf8_field(provenance.pseudopotential_sha256, "pseudopotential_sha256"); + validate_utf8_field(provenance.spin_convention, "spin_convention"); + validate_utf8_field(provenance.executable_sha256, "executable_sha256"); + validate_utf8_field(provenance.auxiliary_whitening, "auxiliary_whitening"); + validate_utf8_field(provenance.coulomb_transform_sha256, "coulomb_transform_sha256"); + if (!valid_commit(provenance.abacus_commit)) + { + throw std::invalid_argument("Sternheimer SIAB provenance requires a 40- or 64-digit hexadecimal ABACUS commit"); + } + if (!valid_hex(provenance.auxiliary_basis_sha256, 64) || !valid_hex(provenance.orbital_sha256, 64) + || !valid_hex(provenance.pseudopotential_sha256, 64)) + { + throw std::invalid_argument("Sternheimer SIAB provenance hashes must be 64-digit hexadecimal SHA256 values"); + } + if (provenance.cell_bohr.size() != 9) + { + throw std::invalid_argument( + "Sternheimer SIAB provenance cell_bohr must contain complete 3x3 row-major lattice vectors"); + } + for (const double cell_value: provenance.cell_bohr) + { + if (!std::isfinite(cell_value)) + { + throw std::invalid_argument("Sternheimer SIAB provenance cell_bohr values must be finite"); + } + } + const long double a = provenance.cell_bohr[0]; + const long double b = provenance.cell_bohr[1]; + const long double c = provenance.cell_bohr[2]; + const long double d = provenance.cell_bohr[3]; + const long double e = provenance.cell_bohr[4]; + const long double f = provenance.cell_bohr[5]; + const long double g = provenance.cell_bohr[6]; + const long double h = provenance.cell_bohr[7]; + const long double i = provenance.cell_bohr[8]; + const long double determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); + if (!std::isfinite(determinant) || determinant == 0.0L) + { + throw std::invalid_argument("Sternheimer SIAB provenance cell_bohr lattice must be nonsingular"); + } + if (!std::isfinite(provenance.ecut_ry) || provenance.ecut_ry <= 0.0) + { + throw std::invalid_argument("Sternheimer SIAB provenance ecut_ry must be finite and positive"); + } + if (provenance.kernel.empty() || provenance.spin_convention.empty()) + { + throw std::invalid_argument("Sternheimer SIAB provenance kernel and spin convention must not be empty"); + } + + const bool has_task4 = !provenance.executable_sha256.empty() || provenance.exx_pca_thr != -1.0 + || provenance.sternheimer_nfreq != 0 || !provenance.frequency_ha.empty() + || !provenance.frequency_weights_ha.empty() || provenance.mpi_ranks != 0 + || provenance.omp_threads != 0; + if (has_task4) + { + if (!valid_hex(provenance.executable_sha256, 64)) + { + throw std::invalid_argument( + "Sternheimer SIAB production provenance requires a 64-digit executable SHA256 value"); + } + if (!std::isfinite(provenance.exx_pca_thr) || provenance.exx_pca_thr < 0.0 + || provenance.sternheimer_nfreq <= 0 || provenance.mpi_ranks <= 0 || provenance.omp_threads <= 0 + || provenance.frequency_ha.size() != static_cast(provenance.sternheimer_nfreq) + || provenance.frequency_weights_ha.size() != static_cast(provenance.sternheimer_nfreq)) + { + throw std::invalid_argument("Sternheimer SIAB production provenance fields are incomplete or invalid"); + } + for (std::size_t index = 0; index != provenance.frequency_ha.size(); ++index) + { + if (!std::isfinite(provenance.frequency_ha[index]) || provenance.frequency_ha[index] < 0.0 + || !std::isfinite(provenance.frequency_weights_ha[index]) + || provenance.frequency_weights_ha[index] < 0.0) + { + throw std::invalid_argument( + "Sternheimer SIAB production frequencies and weights must be finite and non-negative"); + } + if (index != 0 && provenance.frequency_ha[index] <= provenance.frequency_ha[index - 1]) + { + throw std::invalid_argument( + "Sternheimer SIAB production frequencies must be strictly increasing in frequency-index order"); + } + } + } + + const bool has_whitening = !provenance.auxiliary_whitening.empty() + || provenance.raw_auxiliary_dimension != 0 + || provenance.whitened_auxiliary_rank != 0 + || provenance.discarded_auxiliary_rank != 0 + || provenance.coulomb_relative_threshold != -1.0 + || !provenance.coulomb_eigenvalues.empty() + || provenance.coulomb_max_orthonormality_error != -1.0 + || !provenance.coulomb_transform_sha256.empty(); + if (has_whitening) + { + if (provenance.auxiliary_whitening != "global_full_coulomb_v1" + || provenance.raw_auxiliary_dimension <= 0 || provenance.whitened_auxiliary_rank <= 0 + || provenance.discarded_auxiliary_rank < 0 + || provenance.whitened_auxiliary_rank + provenance.discarded_auxiliary_rank + != provenance.raw_auxiliary_dimension + || provenance.coulomb_eigenvalues.size() + != static_cast(provenance.raw_auxiliary_dimension) + || !std::isfinite(provenance.coulomb_relative_threshold) + || provenance.coulomb_relative_threshold <= 0.0 || provenance.coulomb_relative_threshold >= 1.0 + || !std::isfinite(provenance.coulomb_max_orthonormality_error) + || provenance.coulomb_max_orthonormality_error < 0.0 + || provenance.coulomb_max_orthonormality_error > 1.0e-8 + || !valid_hex(provenance.coulomb_transform_sha256, 64)) + { + throw std::invalid_argument( + "Sternheimer SIAB Coulomb-whitening provenance fields are incomplete or inconsistent"); + } + for (std::size_t index = 0; index != provenance.coulomb_eigenvalues.size(); ++index) + { + if (!std::isfinite(provenance.coulomb_eigenvalues[index]) + || (index != 0 + && provenance.coulomb_eigenvalues[index] < provenance.coulomb_eigenvalues[index - 1])) + { + throw std::invalid_argument( + "Sternheimer SIAB Coulomb eigenvalues must be finite and sorted in ascending order"); + } + } + const double largest = provenance.coulomb_eigenvalues.back(); + if (largest <= 0.0) + { + throw std::invalid_argument("Sternheimer SIAB Coulomb spectrum must contain a positive eigenvalue"); + } + const double cutoff = provenance.coulomb_relative_threshold * largest; + const int retained = static_cast(std::count_if(provenance.coulomb_eigenvalues.begin(), + provenance.coulomb_eigenvalues.end(), + [cutoff](const double value) { return value > cutoff; })); + if (retained != provenance.whitened_auxiliary_rank) + { + throw std::invalid_argument( + "Sternheimer SIAB Coulomb spectrum does not reproduce the recorded retained rank"); + } + } +} + +std::vector validate_and_sort_rows(const std::vector& rows, + const std::size_t n_primitive) +{ + std::vector sorted_rows; + sorted_rows.reserve(rows.size()); + for (const ReferenceRow& row: rows) + { + if (row.q.size() != n_primitive) + { + throw std::invalid_argument("Sternheimer SIAB Q row size does not match n_primitive"); + } + if (row.occupied_state < 0 || row.auxiliary_channel < 0 || row.frequency_index < 0) + { + throw std::invalid_argument("Sternheimer SIAB reference indices must be non-negative"); + } + if (!std::isfinite(row.frequency_ha) || !std::isfinite(row.occupation) || !std::isfinite(row.frequency_weight) + || !std::isfinite(row.norm) || row.occupation < 0.0 || row.frequency_weight < 0.0 || row.norm <= 0.0) + { + throw std::invalid_argument("Sternheimer SIAB reference metadata is invalid"); + } + if (!std::all_of(row.q.begin(), row.q.end(), finite_complex)) + { + throw std::invalid_argument("Sternheimer SIAB Q contains a non-finite value"); + } + sorted_rows.push_back(&row); + } + + const auto key = [](const ReferenceRow* row) { + return std::make_tuple(row->occupied_state, row->auxiliary_channel, row->frequency_index); + }; + std::sort(sorted_rows.begin(), sorted_rows.end(), [&key](const ReferenceRow* left, const ReferenceRow* right) { + return key(left) < key(right); + }); + for (std::size_t i = 1; i < sorted_rows.size(); ++i) + { + if (key(sorted_rows[i - 1]) == key(sorted_rows[i])) + { + throw std::invalid_argument("Sternheimer SIAB reference row keys must be unique"); + } + } + return sorted_rows; +} + +void validate_overlap_s(const std::vector>& overlap_s, const std::size_t n_primitive) +{ + if (n_primitive != 0 && n_primitive > static_cast(-1) / n_primitive) + { + throw std::invalid_argument("Sternheimer SIAB primitive overlap dimensions overflow"); + } + if (overlap_s.size() != n_primitive * n_primitive) + { + throw std::invalid_argument("Sternheimer SIAB S size does not match n_primitive squared"); + } + if (!std::all_of(overlap_s.begin(), overlap_s.end(), finite_complex)) + { + throw std::invalid_argument("Sternheimer SIAB S contains a non-finite value"); + } + for (std::size_t row = 0; row < n_primitive; ++row) + { + for (std::size_t column = 0; column < n_primitive; ++column) + { + if (std::abs(overlap_s[row * n_primitive + column] - std::conj(overlap_s[column * n_primitive + row])) + > hermitian_tolerance) + { + throw std::invalid_argument("Sternheimer SIAB S must be Hermitian within absolute tolerance 1e-10"); + } + } + } +} + +bool round_trips_exactly(const std::string& text, const double value) +{ + std::istringstream input(text); + input.imbue(std::locale::classic()); + double parsed = 0.0; + input >> parsed; + const bool exact_subnormal + = value != 0.0 && std::abs(value) < std::numeric_limits::min() && parsed == value; + if (input.bad() || (input.fail() && !exact_subnormal)) + { + return false; + } + return input.rdbuf()->in_avail() == 0 && parsed == value + && (value != 0.0 || std::signbit(parsed) == std::signbit(value)); +} + +std::string normalize_exponent(const std::string& text) +{ + const std::size_t exponent_position = text.find_first_of("eE"); + if (exponent_position == std::string::npos) + { + return text; + } + + std::size_t exponent_digit = exponent_position + 1; + bool negative_exponent = false; + if (exponent_digit < text.size() && (text[exponent_digit] == '+' || text[exponent_digit] == '-')) + { + negative_exponent = text[exponent_digit] == '-'; + ++exponent_digit; + } + while (exponent_digit + 1 < text.size() && text[exponent_digit] == '0') + { + ++exponent_digit; + } + return text.substr(0, exponent_position) + 'e' + (negative_exponent ? "-" : "") + text.substr(exponent_digit); +} + +int parse_exponent(const std::string& text, const std::size_t exponent_position) +{ + std::size_t exponent_digit = exponent_position + 1; + bool negative_exponent = false; + if (exponent_digit < text.size() && text[exponent_digit] == '-') + { + negative_exponent = true; + ++exponent_digit; + } + int exponent = 0; + for (; exponent_digit < text.size(); ++exponent_digit) + { + exponent = exponent * 10 + (text[exponent_digit] - '0'); + } + return negative_exponent ? -exponent : exponent; +} + +std::string expand_exponent(const std::string& text) +{ + const std::size_t exponent_position = text.find('e'); + if (exponent_position == std::string::npos) + { + return text; + } + + std::size_t mantissa_begin = 0; + std::string sign; + if (text[0] == '-' || text[0] == '+') + { + sign.assign(1, text[0]); + mantissa_begin = 1; + } + const std::size_t decimal_position = text.find('.', mantissa_begin); + std::string digits = text.substr(mantissa_begin, exponent_position - mantissa_begin); + if (decimal_position != std::string::npos && decimal_position < exponent_position) + { + digits.erase(decimal_position - mantissa_begin, 1); + } + + const int exponent = parse_exponent(text, exponent_position); + const int digits_before_decimal = decimal_position == std::string::npos + ? static_cast(digits.size()) + : static_cast(decimal_position - mantissa_begin); + const int expanded_decimal_position = digits_before_decimal + exponent; + std::string expanded = sign; + if (expanded_decimal_position <= 0) + { + expanded += "0."; + expanded.append(static_cast(-expanded_decimal_position), '0'); + expanded += digits; + } + else if (expanded_decimal_position >= static_cast(digits.size())) + { + expanded += digits; + expanded.append(static_cast(expanded_decimal_position - static_cast(digits.size())), '0'); + } + else + { + expanded += digits.substr(0, static_cast(expanded_decimal_position)); + expanded += '.'; + expanded += digits.substr(static_cast(expanded_decimal_position)); + } + return expanded; +} + +std::string format_exponent(const int exponent) +{ + std::ostringstream output; + output.imbue(std::locale::classic()); + output << exponent; + return output.str(); +} + +std::string decimal_to_scientific(const std::string& decimal) +{ + std::size_t digits_begin = 0; + std::string sign; + if (decimal[0] == '-' || decimal[0] == '+') + { + sign.assign(1, decimal[0]); + digits_begin = 1; + } + const std::size_t decimal_position = decimal.find('.', digits_begin); + const int digits_before_decimal = decimal_position == std::string::npos + ? static_cast(decimal.size() - digits_begin) + : static_cast(decimal_position - digits_begin); + std::string digits = decimal.substr(digits_begin); + if (decimal_position != std::string::npos) + { + digits.erase(decimal_position - digits_begin, 1); + } + const std::size_t first_nonzero = digits.find_first_not_of('0'); + if (first_nonzero == std::string::npos) + { + return sign + "0e0"; + } + const std::size_t last_nonzero = digits.find_last_not_of('0'); + const int exponent = digits_before_decimal - static_cast(first_nonzero) - 1; + const std::string significant = digits.substr(first_nonzero, last_nonzero - first_nonzero + 1); + std::string scientific = sign + significant.substr(0, 1); + if (significant.size() > 1) + { + scientific += '.'; + scientific += significant.substr(1); + } + scientific += 'e'; + scientific += format_exponent(exponent); + return scientific; +} + +void add_exact_candidate(std::vector& candidates, const std::string& candidate, const double value) +{ + std::string compatible = candidate; + if (compatible.find_first_of(".eE") == std::string::npos) + { + compatible += ".0"; + } + if (round_trips_exactly(compatible, value) + && std::find(candidates.begin(), candidates.end(), compatible) == candidates.end()) + { + candidates.push_back(compatible); + } +} + +bool scientific_notation(const std::string& value) +{ + return value.find('e') != std::string::npos; +} + +bool better_candidate(const std::string& left, const std::string& right) +{ + if (left.size() != right.size()) + { + return left.size() < right.size(); + } + if (scientific_notation(left) != scientific_notation(right)) + { + return !scientific_notation(left); + } + return left < right; +} + +std::string small_integral_compatibility(const double value) +{ + std::ostringstream output; + output.imbue(std::locale::classic()); + output << std::fixed << std::setprecision(0) << value << ".0"; + return output.str(); +} + +std::string format_double(const double value) +{ + if (std::trunc(value) == value && std::abs(value) < 100.0) + { + return small_integral_compatibility(value); + } + + std::vector candidates; + for (int precision = 1; precision <= std::numeric_limits::max_digits10; ++precision) + { + std::ostringstream output; + output.imbue(std::locale::classic()); + output << std::defaultfloat << std::setprecision(precision) << value; + if (!output.good() || !round_trips_exactly(output.str(), value)) + { + continue; + } + const std::string normalized = normalize_exponent(output.str()); + const std::string decimal = expand_exponent(normalized); + add_exact_candidate(candidates, normalized, value); + add_exact_candidate(candidates, decimal, value); + add_exact_candidate(candidates, decimal_to_scientific(decimal), value); + } + if (candidates.empty()) + { + throw std::runtime_error("Failed to format Sternheimer SIAB floating-point value exactly"); + } + return *std::min_element(candidates.begin(), candidates.end(), better_candidate); +} + +std::string json_string(const std::string& value) +{ + static const char hexadecimal[] = "0123456789ABCDEF"; + std::string escaped; + escaped.reserve(value.size() + 2); + escaped += '"'; + for (const unsigned char character: value) + { + switch (character) + { + case '"': + escaped += "\\\""; + break; + case '\\': + escaped += "\\\\"; + break; + case '\b': + escaped += "\\b"; + break; + case '\f': + escaped += "\\f"; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + default: + if (character < 0x20) + { + escaped += "\\u00"; + escaped += hexadecimal[character >> 4]; + escaped += hexadecimal[character & 0x0F]; + } + else + { + escaped += static_cast(character); + } + } + } + escaped += '"'; + return escaped; +} + +std::string provenance_json(const Provenance& provenance) +{ + std::ostringstream output; + output.imbue(std::locale::classic()); + output << "{\"abacus_commit\":" << json_string(provenance.abacus_commit) + << ",\"auxiliary_basis_sha256\":" << json_string(provenance.auxiliary_basis_sha256) << ",\"cell_bohr\":["; + for (std::size_t index = 0; index < provenance.cell_bohr.size(); ++index) + { + if (index != 0) + { + output << ','; + } + output << format_double(provenance.cell_bohr[index]); + } + output << "],\"ecut_ry\":" << format_double(provenance.ecut_ry) << ",\"kernel\":" << json_string(provenance.kernel) + << ",\"orbital_sha256\":" << json_string(provenance.orbital_sha256) + << ",\"pseudopotential_sha256\":" << json_string(provenance.pseudopotential_sha256) + << ",\"spin_convention\":" << json_string(provenance.spin_convention); + if (!provenance.executable_sha256.empty()) + { + output << ",\"executable_sha256\":" << json_string(provenance.executable_sha256) + << ",\"exx_pca_thr\":" << format_double(provenance.exx_pca_thr) << ",\"frequency_ha\":["; + for (std::size_t index = 0; index != provenance.frequency_ha.size(); ++index) + { + if (index != 0) + { + output << ','; + } + output << format_double(provenance.frequency_ha[index]); + } + output << "],\"frequency_weights_ha\":["; + for (std::size_t index = 0; index != provenance.frequency_weights_ha.size(); ++index) + { + if (index != 0) + { + output << ','; + } + output << format_double(provenance.frequency_weights_ha[index]); + } + output << "],\"mpi_ranks\":" << provenance.mpi_ranks << ",\"omp_threads\":" << provenance.omp_threads + << ",\"sternheimer_nfreq\":" << provenance.sternheimer_nfreq; + } + if (!provenance.auxiliary_whitening.empty()) + { + output << ",\"auxiliary_whitening\":" << json_string(provenance.auxiliary_whitening) + << ",\"coulomb_eigenvalues\":["; + for (std::size_t index = 0; index != provenance.coulomb_eigenvalues.size(); ++index) + { + if (index != 0) + { + output << ','; + } + output << format_double(provenance.coulomb_eigenvalues[index]); + } + output << "],\"coulomb_max_orthonormality_error\":" + << format_double(provenance.coulomb_max_orthonormality_error) + << ",\"coulomb_relative_threshold\":" << format_double(provenance.coulomb_relative_threshold) + << ",\"coulomb_transform_sha256\":" << json_string(provenance.coulomb_transform_sha256) + << ",\"discarded_auxiliary_rank\":" << provenance.discarded_auxiliary_rank + << ",\"raw_auxiliary_dimension\":" << provenance.raw_auxiliary_dimension + << ",\"whitened_auxiliary_rank\":" << provenance.whitened_auxiliary_rank; + } + output << '}'; + if (!output.good()) + { + throw std::runtime_error("Failed to format Sternheimer SIAB provenance JSON"); + } + return output.str(); +} + +class TemporaryFile +{ + public: + explicit TemporaryFile(const std::string& path) : path_(path), keep_(false) + { + } + + ~TemporaryFile() + { + if (!keep_) + { + std::remove(path_.c_str()); + } + } + + void keep() + { + keep_ = true; + } + + private: + const std::string path_; + bool keep_; +}; + +} // namespace + +void write_v1(const std::string& path, + const double grid_volume_bohr3, + const std::vector& blocks, + const std::vector& rows, + const std::vector>& overlap_s, + const Provenance& provenance) +{ + if (path.empty()) + { + throw std::invalid_argument("Sternheimer SIAB output path must not be empty"); + } + if (!std::isfinite(grid_volume_bohr3) || grid_volume_bohr3 <= 0.0) + { + throw std::invalid_argument("Sternheimer SIAB grid volume must be finite and positive"); + } + + const std::size_t n_primitive = validate_blocks(blocks); + const std::vector sorted_rows = validate_and_sort_rows(rows, n_primitive); + validate_overlap_s(overlap_s, n_primitive); + validate_provenance(provenance); + if (!provenance.auxiliary_whitening.empty()) + { + for (const ReferenceRow* row: sorted_rows) + { + if (row->auxiliary_channel >= provenance.whitened_auxiliary_rank) + { + throw std::invalid_argument( + "Sternheimer SIAB reference row exceeds the retained Coulomb-whitened auxiliary rank"); + } + } + } + const std::string json = provenance_json(provenance); + + const std::string temporary_path = path + ".tmp"; + TemporaryFile temporary_file(temporary_path); + std::ofstream output(temporary_path.c_str(), std::ios::binary | std::ios::trunc); + output.imbue(std::locale::classic()); + if (!output.is_open()) + { + throw std::runtime_error("Failed to open Sternheimer SIAB temporary output: " + temporary_path); + } + + output << "\n" + << "format_version 1\n" + << "n_reference " << sorted_rows.size() << "\n" + << "n_primitive " << n_primitive << "\n" + << "n_blocks " << blocks.size() << "\n" + << "grid_volume_bohr3 " << format_double(grid_volume_bohr3) << "\n" + << "\n" + << "\n" + << "# element atom_index l m n_primitive offset\n"; + for (const PrimitiveBlock& block: blocks) + { + output << block.element << " " << block.atom_index << " " << block.l << " " << block.m << " " + << block.n_primitive << " " << block.offset << "\n"; + } + + output << "\n" + << "\n" + << "# occupied_state auxiliary_channel frequency_ha occupation frequency_weight norm\n"; + for (const ReferenceRow* row: sorted_rows) + { + output << row->occupied_state << " " << row->auxiliary_channel << " " << format_double(row->frequency_ha) << " " + << format_double(row->occupation) << " " << format_double(row->frequency_weight) << " " + << format_double(row->norm) << "\n"; + } + + output << "\n" + << "\n" + << "# row-major , real imag\n"; + for (const ReferenceRow* row: sorted_rows) + { + for (const std::complex& value: row->q) + { + output << format_double(value.real()) << " " << format_double(value.imag()) << "\n"; + } + } + + output << "\n" + << "\n" + << "# row-major , real imag\n"; + for (const std::complex& value: overlap_s) + { + output << format_double(value.real()) << " " << format_double(value.imag()) << "\n"; + } + output << "\n" + << "\n" + << json << "\n" + << "\n"; + + output.flush(); + if (!output.good()) + { + throw std::runtime_error("Failed to flush Sternheimer SIAB temporary output: " + temporary_path); + } + output.close(); + if (output.fail()) + { + throw std::runtime_error("Failed to close Sternheimer SIAB temporary output: " + temporary_path); + } + if (std::rename(temporary_path.c_str(), path.c_str()) != 0) + { + const std::string reason = std::strerror(errno); + throw std::runtime_error("Failed to replace Sternheimer SIAB output " + path + ": " + reason); + } + temporary_file.keep(); +} + +} // namespace sternheimer_siab +} // namespace module_ri diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.h new file mode 100644 index 00000000000..46d2c0a8042 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.h @@ -0,0 +1,25 @@ +#ifndef STERNHEIMER_SIAB_WRITER_H +#define STERNHEIMER_SIAB_WRITER_H + +#include "sternheimer_siab_data.h" + +#include +#include +#include + +namespace module_ri +{ +namespace sternheimer_siab +{ + +void write_v1(const std::string& path, + double grid_volume_bohr3, + const std::vector& blocks, + const std::vector& rows, + const std::vector>& overlap_s, + const Provenance& provenance); + +} // namespace sternheimer_siab +} // namespace module_ri + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.cpp new file mode 100644 index 00000000000..c5ba61644d5 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.cpp @@ -0,0 +1,276 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +int parse_integer(const std::string& text, const std::string& key) +{ + std::size_t parsed = 0; + int value = 0; + try + { + value = std::stoi(text, &parsed); + } + catch (const std::exception&) + { + throw std::invalid_argument("Invalid integer for supercell translation key " + key + "."); + } + if (parsed != text.size()) + { + throw std::invalid_argument("Invalid integer for supercell translation key " + key + "."); + } + return value; +} + +double parse_double(const std::string& text, const std::string& key) +{ + std::size_t parsed = 0; + double value = 0.0; + try + { + value = std::stod(text, &parsed); + } + catch (const std::exception&) + { + throw std::invalid_argument("Invalid floating-point value for supercell translation key " + key + "."); + } + if (parsed != text.size() || !std::isfinite(value)) + { + throw std::invalid_argument("Invalid floating-point value for supercell translation key " + key + "."); + } + return value; +} + +template +std::array parse_triplet(const std::string& text, + const char separator, + const std::string& key, + Parser parser) +{ + std::array result{}; + std::istringstream input(text); + std::string item; + for (int i = 0; i != 3; ++i) + { + if (!std::getline(input, item, separator) || item.empty()) + { + throw std::invalid_argument("Invalid triplet for supercell translation key " + key + "."); + } + result[static_cast(i)] = parser(item, key); + } + if (std::getline(input, item, separator)) + { + throw std::invalid_argument("Invalid triplet for supercell translation key " + key + "."); + } + return result; +} + +void validate_config(const SternheimerSupercellTranslationSum& config) +{ + if (config.atoms_per_primitive <= 0 || config.basis_atom < 0 + || config.basis_atom >= config.atoms_per_primitive || config.channel_within_atom < 0) + { + throw std::invalid_argument("Invalid supercell translation atom or channel selector."); + } + for (int dimension = 0; dimension != 3; ++dimension) + { + const int repeat = config.repeats[static_cast(dimension)]; + const double q = config.primitive_qpoint[static_cast(dimension)]; + if (repeat <= 0 || std::abs(q * repeat - std::round(q * repeat)) > 1.0e-10) + { + throw std::invalid_argument("Supercell translation q point is not commensurate with the repeats."); + } + } +} + +const SternheimerABFBlochGridChannel& channel_within_atom( + const std::vector& channels, + const int atom_index, + const int ordinal) +{ + int current = 0; + const SternheimerABFBlochGridChannel* selected = nullptr; + for (const auto& channel: channels) + { + if (channel.atom_index != atom_index) + { + continue; + } + if (current == ordinal) + { + if (selected != nullptr) + { + throw std::invalid_argument("Duplicated supercell translation channel ordinal."); + } + selected = &channel; + } + ++current; + } + if (selected == nullptr) + { + throw std::invalid_argument("Missing equivalent-atom channel in supercell translation sum."); + } + return *selected; +} + +} // namespace + +int sternheimer_supercell_primitive_cell_count(const SternheimerSupercellTranslationSum& config) +{ + validate_config(config); + return config.repeats[0] * config.repeats[1] * config.repeats[2]; +} + +SternheimerSupercellTranslationSum parse_sternheimer_supercell_translation_sum( + const std::string& specification) +{ + std::map fields; + std::istringstream input(specification); + std::string field; + while (std::getline(input, field, ',')) + { + const std::size_t separator = field.find('='); + if (separator == std::string::npos || separator == 0 || separator + 1 == field.size()) + { + throw std::invalid_argument("Invalid supercell translation perturbation field."); + } + if (!fields.insert({field.substr(0, separator), field.substr(separator + 1)}).second) + { + throw std::invalid_argument("Duplicated supercell translation perturbation key."); + } + } + const std::set required{ + "repeats", "primitive_q", "atoms_per_primitive", "basis_atom", "channel_within_atom"}; + if (fields.size() != required.size()) + { + throw std::invalid_argument("Incomplete supercell translation perturbation specification."); + } + for (const auto& entry: fields) + { + if (required.count(entry.first) == 0) + { + throw std::invalid_argument("Unknown supercell translation perturbation key " + entry.first + "."); + } + } + + SternheimerSupercellTranslationSum config; + config.repeats = parse_triplet(fields.at("repeats"), 'x', "repeats", parse_integer); + config.primitive_qpoint + = parse_triplet(fields.at("primitive_q"), ':', "primitive_q", parse_double); + config.atoms_per_primitive = parse_integer(fields.at("atoms_per_primitive"), "atoms_per_primitive"); + config.basis_atom = parse_integer(fields.at("basis_atom"), "basis_atom"); + config.channel_within_atom = parse_integer(fields.at("channel_within_atom"), "channel_within_atom"); + validate_config(config); + return config; +} + +SternheimerABFBlochGridChannel combine_sternheimer_supercell_translation_channel( + const std::vector& channels, + const SternheimerSupercellTranslationSum& config) +{ + const int cell_count = sternheimer_supercell_primitive_cell_count(config); + if (channels.empty()) + { + throw std::invalid_argument("Supercell translation sum requires auxiliary channels."); + } + + SternheimerABFBlochGridChannel result; + bool initialized = false; + const double normalization = 1.0 / std::sqrt(static_cast(cell_count)); + const double two_pi = 2.0 * std::acos(-1.0); + for (int ix = 0; ix != config.repeats[0]; ++ix) + { + for (int iy = 0; iy != config.repeats[1]; ++iy) + { + for (int iz = 0; iz != config.repeats[2]; ++iz) + { + const int cell_index = (ix * config.repeats[1] + iy) * config.repeats[2] + iz; + const int atom_index = cell_index * config.atoms_per_primitive + config.basis_atom; + const auto& channel = channel_within_atom(channels, atom_index, config.channel_within_atom); + if (!initialized) + { + result = channel; + result.channel_index = 0; + result.atom_index = config.basis_atom; + result.atom_local_index = config.channel_within_atom; + result.label = "supercell_translation_sum:" + channel.label; + result.potential_r.assign(channel.potential_r.size(), std::complex(0.0, 0.0)); + initialized = true; + } + if (channel.potential_r.size() != result.potential_r.size() + || channel.type_index != result.type_index + || channel.angular_momentum != result.angular_momentum + || channel.radial_index != result.radial_index + || channel.magnetic_index != result.magnetic_index) + { + throw std::invalid_argument("Incompatible equivalent-atom channels in supercell translation sum."); + } + const double angle = two_pi * (config.primitive_qpoint[0] * ix + + config.primitive_qpoint[1] * iy + + config.primitive_qpoint[2] * iz); + const std::complex phase = normalization * std::exp(std::complex(0.0, angle)); + for (std::size_t ir = 0; ir != result.potential_r.size(); ++ir) + { + result.potential_r[ir] += phase * channel.potential_r[ir]; + } + } + } + } + result.max_abs = 0.0; + for (const auto& value: result.potential_r) + { + result.max_abs = std::max(result.max_abs, std::abs(value)); + } + return result; +} + +std::vector +combine_all_sternheimer_supercell_translation_channels( + const std::vector& channels, + const SternheimerSupercellTranslationSum& config) +{ + sternheimer_supercell_primitive_cell_count(config); + if (channels.empty()) + { + throw std::invalid_argument("Supercell translation sum requires auxiliary channels."); + } + + std::vector combined; + for (int basis_atom = 0; basis_atom != config.atoms_per_primitive; ++basis_atom) + { + const int channels_on_atom = static_cast(std::count_if( + channels.begin(), channels.end(), [basis_atom](const auto& channel) { + return channel.atom_index == basis_atom; + })); + if (channels_on_atom <= 0) + { + throw std::invalid_argument( + "Supercell translation sum found no auxiliary channels on a primitive atom."); + } + for (int ordinal = 0; ordinal != channels_on_atom; ++ordinal) + { + SternheimerSupercellTranslationSum channel_config = config; + channel_config.basis_atom = basis_atom; + channel_config.channel_within_atom = ordinal; + SternheimerABFBlochGridChannel channel + = combine_sternheimer_supercell_translation_channel(channels, channel_config); + channel.channel_index = static_cast(combined.size()); + channel.atom_index = basis_atom; + channel.atom_local_index = ordinal; + combined.push_back(std::move(channel)); + } + } + return combined; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.h new file mode 100644 index 00000000000..2654d7a009a --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.h @@ -0,0 +1,38 @@ +#ifndef STERNHEIMER_SUPERCELL_PERTURBATION_H +#define STERNHEIMER_SUPERCELL_PERTURBATION_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" + +#include +#include +#include + +namespace ModuleRI +{ + +struct SternheimerSupercellTranslationSum +{ + std::array repeats{1, 1, 1}; + SternheimerReducedKPoint primitive_qpoint{0.0, 0.0, 0.0}; + int atoms_per_primitive = 0; + int basis_atom = -1; + int channel_within_atom = -1; +}; + +SternheimerSupercellTranslationSum parse_sternheimer_supercell_translation_sum( + const std::string& specification); + +SternheimerABFBlochGridChannel combine_sternheimer_supercell_translation_channel( + const std::vector& channels, + const SternheimerSupercellTranslationSum& config); + +std::vector +combine_all_sternheimer_supercell_translation_channels( + const std::vector& channels, + const SternheimerSupercellTranslationSum& config); + +int sternheimer_supercell_primitive_cell_count(const SternheimerSupercellTranslationSum& config); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.cpp new file mode 100644 index 00000000000..55a8ad32f48 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.cpp @@ -0,0 +1,574 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.h" + +#include "source_base/module_external/lapack_connector.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_lapack_gesv.h" + +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +using Complex = std::complex; + +std::size_t column_major_index(const int row, const int column, const int rows) +{ + return static_cast(row) + + static_cast(rows) * static_cast(column); +} + +int validated_cell_count(const std::array& repeats, + const SternheimerReducedKPoint& primitive_kpoint) +{ + int cell_count = 1; + for (int direction = 0; direction != 3; ++direction) + { + const int repeat = repeats[static_cast(direction)]; + const double kpoint = primitive_kpoint[static_cast(direction)]; + if (repeat <= 0 || !std::isfinite(kpoint) + || std::abs(kpoint * repeat - std::round(kpoint * repeat)) > 1.0e-10) + { + throw std::invalid_argument( + "Sternheimer supercell sector requires positive repeats and a commensurate finite k point."); + } + if (cell_count > std::numeric_limits::max() / repeat) + { + throw std::overflow_error("Sternheimer supercell sector cell count overflowed."); + } + cell_count *= repeat; + } + return cell_count; +} + +std::vector translation_sector_basis(const int primitive_basis_size, + const std::array& repeats, + const SternheimerReducedKPoint& primitive_kpoint) +{ + const int cell_count = repeats[0] * repeats[1] * repeats[2]; + const int full_basis_size = cell_count * primitive_basis_size; + std::vector basis( + static_cast(full_basis_size) * static_cast(primitive_basis_size), + Complex(0.0, 0.0)); + const double normalization = 1.0 / std::sqrt(static_cast(cell_count)); + const double two_pi = 2.0 * std::acos(-1.0); + for (int ix = 0; ix != repeats[0]; ++ix) + { + for (int iy = 0; iy != repeats[1]; ++iy) + { + for (int iz = 0; iz != repeats[2]; ++iz) + { + const int cell_index = (ix * repeats[1] + iy) * repeats[2] + iz; + const double angle = two_pi + * (primitive_kpoint[0] * ix + primitive_kpoint[1] * iy + + primitive_kpoint[2] * iz); + const Complex phase = normalization * std::exp(Complex(0.0, angle)); + for (int orbital = 0; orbital != primitive_basis_size; ++orbital) + { + const int row = cell_index * primitive_basis_size + orbital; + basis[column_major_index(row, orbital, full_basis_size)] = phase; + } + } + } + } + return basis; +} + +SternheimerSupercellSector recover_sector_from_eigenbasis_coefficients( + const std::vector& eigenvalues, + const std::vector& eigenbasis_sector_coefficients, + const std::array& repeats, + const SternheimerReducedKPoint& primitive_kpoint, + const int primitive_basis_size, + const int full_basis_size) +{ + const std::size_t expected_coefficients + = static_cast(full_basis_size) + * static_cast(primitive_basis_size); + if (eigenbasis_sector_coefficients.size() != expected_coefficients) + { + throw std::invalid_argument("Recovered Sternheimer sector coefficient dimensions are inconsistent."); + } + + const std::size_t sector_matrix_size + = static_cast(primitive_basis_size) + * static_cast(primitive_basis_size); + std::vector overlap(sector_matrix_size, Complex(0.0, 0.0)); + std::vector hamiltonian(sector_matrix_size, Complex(0.0, 0.0)); + for (int column = 0; column != primitive_basis_size; ++column) + { + for (int row = 0; row != primitive_basis_size; ++row) + { + Complex overlap_element(0.0, 0.0); + Complex hamiltonian_element(0.0, 0.0); + for (int band = 0; band != full_basis_size; ++band) + { + const Complex bra = std::conj(eigenbasis_sector_coefficients[ + column_major_index(band, row, full_basis_size)]); + const Complex ket = eigenbasis_sector_coefficients[ + column_major_index(band, column, full_basis_size)]; + overlap_element += bra * ket; + hamiltonian_element + += bra * eigenvalues[static_cast(band)] * ket; + } + const std::size_t index + = column_major_index(row, column, primitive_basis_size); + overlap[index] = overlap_element; + hamiltonian[index] = hamiltonian_element; + } + } + const std::vector overlap_original = overlap; + + constexpr int problem_type = 1; + constexpr char jobz = 'V'; + constexpr char uplo = 'U'; + std::vector sector_eigenvalues( + static_cast(primitive_basis_size), 0.0); + int info = 0; + int lwork = -1; + Complex work_query(0.0, 0.0); + std::vector rwork( + static_cast(std::max(1, 3 * primitive_basis_size - 2)), 0.0); + zhegv_(&problem_type, + &jobz, + &uplo, + &primitive_basis_size, + hamiltonian.data(), + &primitive_basis_size, + overlap.data(), + &primitive_basis_size, + sector_eigenvalues.data(), + &work_query, + &lwork, + rwork.data(), + &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer supercell sector LAPACK workspace query failed."); + } + lwork = std::max(1, static_cast(std::real(work_query))); + std::vector work(static_cast(lwork), Complex(0.0, 0.0)); + zhegv_(&problem_type, + &jobz, + &uplo, + &primitive_basis_size, + hamiltonian.data(), + &primitive_basis_size, + overlap.data(), + &primitive_basis_size, + sector_eigenvalues.data(), + work.data(), + &lwork, + rwork.data(), + &info); + if (info != 0) + { + std::ostringstream message; + message << "Sternheimer supercell sector generalized eigensolve failed; zhegv info=" + << info << '.'; + throw std::runtime_error(message.str()); + } + + const std::vector sector_basis + = translation_sector_basis(primitive_basis_size, repeats, primitive_kpoint); + SternheimerSupercellSector result; + result.eigenvalues = std::move(sector_eigenvalues); + result.coefficients.assign( + static_cast(primitive_basis_size), + std::vector(static_cast(full_basis_size), + Complex(0.0, 0.0))); + for (int state = 0; state != primitive_basis_size; ++state) + { + for (int primitive_orbital = 0; primitive_orbital != primitive_basis_size; + ++primitive_orbital) + { + const Complex coefficient + = hamiltonian[column_major_index(primitive_orbital, + state, + primitive_basis_size)]; + for (int full_orbital = 0; full_orbital != full_basis_size; ++full_orbital) + { + result.coefficients[static_cast(state)] + [static_cast(full_orbital)] + += sector_basis[column_major_index(full_orbital, + primitive_orbital, + full_basis_size)] + * coefficient; + } + } + } + + for (int column = 0; column != primitive_basis_size; ++column) + { + for (int row = 0; row != primitive_basis_size; ++row) + { + Complex metric_element(0.0, 0.0); + for (int left = 0; left != primitive_basis_size; ++left) + { + for (int right = 0; right != primitive_basis_size; ++right) + { + metric_element + += std::conj(hamiltonian[column_major_index(left, + row, + primitive_basis_size)]) + * overlap_original[column_major_index(left, + right, + primitive_basis_size)] + * hamiltonian[column_major_index(right, + column, + primitive_basis_size)]; + } + } + const Complex expected + = row == column ? Complex(1.0, 0.0) : Complex(0.0, 0.0); + result.max_orthonormality_error + = std::max(result.max_orthonormality_error, + std::abs(metric_element - expected)); + } + } + + for (int state = 0; state != primitive_basis_size; ++state) + { + double residual_squared = 0.0; + double norm_squared = 0.0; + for (int full_band = 0; full_band != full_basis_size; ++full_band) + { + Complex eigenbasis_coefficient(0.0, 0.0); + for (int primitive_orbital = 0; primitive_orbital != primitive_basis_size; + ++primitive_orbital) + { + eigenbasis_coefficient + += eigenbasis_sector_coefficients[column_major_index( + full_band, primitive_orbital, full_basis_size)] + * hamiltonian[column_major_index(primitive_orbital, + state, + primitive_basis_size)]; + } + const double coefficient_norm = std::norm(eigenbasis_coefficient); + const double difference = eigenvalues[static_cast(full_band)] + - result.eigenvalues[static_cast(state)]; + norm_squared += coefficient_norm; + residual_squared += difference * difference * coefficient_norm; + } + const double relative_residual + = norm_squared > 0.0 ? std::sqrt(residual_squared / norm_squared) + : std::sqrt(residual_squared); + result.max_full_space_residual + = std::max(result.max_full_space_residual, relative_residual); + } + return result; +} + +} // namespace + +SternheimerSupercellSector recover_sternheimer_supercell_sector( + const std::vector& eigenvalues, + const std::vector>>& eigenvectors, + const std::array& repeats, + const SternheimerReducedKPoint& primitive_kpoint) +{ + const int cell_count = validated_cell_count(repeats, primitive_kpoint); + const int full_basis_size = static_cast(eigenvectors.size()); + if (full_basis_size <= 0 || eigenvalues.size() != eigenvectors.size() + || full_basis_size % cell_count != 0) + { + throw std::invalid_argument( + "Sternheimer supercell sector requires a complete square eigensystem and translation-major AO blocks."); + } + for (const auto& eigenvector: eigenvectors) + { + if (eigenvector.size() != static_cast(full_basis_size)) + { + throw std::invalid_argument("Sternheimer supercell sector eigensystem is not square."); + } + } + + const int primitive_basis_size = full_basis_size / cell_count; + std::vector eigenvector_matrix( + static_cast(full_basis_size) * static_cast(full_basis_size)); + for (int band = 0; band != full_basis_size; ++band) + { + for (int orbital = 0; orbital != full_basis_size; ++orbital) + { + eigenvector_matrix[column_major_index(orbital, band, full_basis_size)] + = eigenvectors[static_cast(band)][static_cast(orbital)]; + } + } + + std::vector eigenbasis_sector_coefficients + = translation_sector_basis(primitive_basis_size, repeats, primitive_kpoint); + std::vector pivots(static_cast(full_basis_size), 0); + int info = 0; + zgesv_(&full_basis_size, + &primitive_basis_size, + eigenvector_matrix.data(), + &full_basis_size, + pivots.data(), + eigenbasis_sector_coefficients.data(), + &full_basis_size, + &info); + if (info != 0) + { + std::ostringstream message; + message << "Sternheimer supercell sector could not invert the complete LCAO eigenvector matrix; zgesv info=" + << info << '.'; + throw std::runtime_error(message.str()); + } + + const std::size_t sector_matrix_size + = static_cast(primitive_basis_size) * static_cast(primitive_basis_size); + std::vector overlap(sector_matrix_size, Complex(0.0, 0.0)); + std::vector hamiltonian(sector_matrix_size, Complex(0.0, 0.0)); + for (int column = 0; column != primitive_basis_size; ++column) + { + for (int row = 0; row != primitive_basis_size; ++row) + { + Complex overlap_element(0.0, 0.0); + Complex hamiltonian_element(0.0, 0.0); + for (int band = 0; band != full_basis_size; ++band) + { + const Complex bra = std::conj(eigenbasis_sector_coefficients[ + column_major_index(band, row, full_basis_size)]); + const Complex ket = eigenbasis_sector_coefficients[ + column_major_index(band, column, full_basis_size)]; + overlap_element += bra * ket; + hamiltonian_element += bra * eigenvalues[static_cast(band)] * ket; + } + const std::size_t index = column_major_index(row, column, primitive_basis_size); + overlap[index] = overlap_element; + hamiltonian[index] = hamiltonian_element; + } + } + const std::vector overlap_original = overlap; + + constexpr int problem_type = 1; + constexpr char jobz = 'V'; + constexpr char uplo = 'U'; + std::vector sector_eigenvalues(static_cast(primitive_basis_size), 0.0); + int lwork = -1; + Complex work_query(0.0, 0.0); + std::vector rwork(static_cast(std::max(1, 3 * primitive_basis_size - 2)), 0.0); + zhegv_(&problem_type, + &jobz, + &uplo, + &primitive_basis_size, + hamiltonian.data(), + &primitive_basis_size, + overlap.data(), + &primitive_basis_size, + sector_eigenvalues.data(), + &work_query, + &lwork, + rwork.data(), + &info); + if (info != 0) + { + throw std::runtime_error("Sternheimer supercell sector LAPACK workspace query failed."); + } + lwork = std::max(1, static_cast(std::real(work_query))); + std::vector work(static_cast(lwork), Complex(0.0, 0.0)); + zhegv_(&problem_type, + &jobz, + &uplo, + &primitive_basis_size, + hamiltonian.data(), + &primitive_basis_size, + overlap.data(), + &primitive_basis_size, + sector_eigenvalues.data(), + work.data(), + &lwork, + rwork.data(), + &info); + if (info != 0) + { + std::ostringstream message; + message << "Sternheimer supercell sector generalized eigensolve failed; zhegv info=" << info << '.'; + throw std::runtime_error(message.str()); + } + + const std::vector sector_basis + = translation_sector_basis(primitive_basis_size, repeats, primitive_kpoint); + SternheimerSupercellSector result; + result.eigenvalues = std::move(sector_eigenvalues); + result.coefficients.assign(static_cast(primitive_basis_size), + std::vector(static_cast(full_basis_size), + Complex(0.0, 0.0))); + for (int state = 0; state != primitive_basis_size; ++state) + { + for (int primitive_orbital = 0; primitive_orbital != primitive_basis_size; ++primitive_orbital) + { + const Complex coefficient + = hamiltonian[column_major_index(primitive_orbital, state, primitive_basis_size)]; + for (int full_orbital = 0; full_orbital != full_basis_size; ++full_orbital) + { + result.coefficients[static_cast(state)][static_cast(full_orbital)] + += sector_basis[column_major_index(full_orbital, + primitive_orbital, + full_basis_size)] + * coefficient; + } + } + } + + for (int column = 0; column != primitive_basis_size; ++column) + { + for (int row = 0; row != primitive_basis_size; ++row) + { + Complex metric_element(0.0, 0.0); + for (int left = 0; left != primitive_basis_size; ++left) + { + for (int right = 0; right != primitive_basis_size; ++right) + { + metric_element + += std::conj(hamiltonian[column_major_index(left, row, primitive_basis_size)]) + * overlap_original[column_major_index(left, right, primitive_basis_size)] + * hamiltonian[column_major_index(right, column, primitive_basis_size)]; + } + } + const Complex expected = row == column ? Complex(1.0, 0.0) : Complex(0.0, 0.0); + result.max_orthonormality_error + = std::max(result.max_orthonormality_error, std::abs(metric_element - expected)); + } + } + + for (int state = 0; state != primitive_basis_size; ++state) + { + double residual_squared = 0.0; + double norm_squared = 0.0; + for (int full_band = 0; full_band != full_basis_size; ++full_band) + { + Complex eigenbasis_coefficient(0.0, 0.0); + for (int primitive_orbital = 0; primitive_orbital != primitive_basis_size; + ++primitive_orbital) + { + eigenbasis_coefficient + += eigenbasis_sector_coefficients[column_major_index(full_band, + primitive_orbital, + full_basis_size)] + * hamiltonian[column_major_index(primitive_orbital, + state, + primitive_basis_size)]; + } + const double coefficient_norm = std::norm(eigenbasis_coefficient); + const double difference = eigenvalues[static_cast(full_band)] + - result.eigenvalues[static_cast(state)]; + norm_squared += coefficient_norm; + residual_squared += difference * difference * coefficient_norm; + } + const double relative_residual + = norm_squared > 0.0 ? std::sqrt(residual_squared / norm_squared) : std::sqrt(residual_squared); + result.max_full_space_residual = std::max(result.max_full_space_residual, relative_residual); + } + return result; +} + +std::vector recover_all_sternheimer_supercell_sectors( + const std::vector& eigenvalues, + const std::vector>>& eigenvectors, + const std::array& repeats) +{ + const int cell_count = validated_cell_count(repeats, {0.0, 0.0, 0.0}); + const int full_basis_size = static_cast(eigenvectors.size()); + if (full_basis_size <= 0 || eigenvalues.size() != eigenvectors.size() + || full_basis_size % cell_count != 0) + { + throw std::invalid_argument( + "Sternheimer supercell sectors require a complete square eigensystem and translation-major AO blocks."); + } + for (const auto& eigenvector: eigenvectors) + { + if (eigenvector.size() != static_cast(full_basis_size)) + { + throw std::invalid_argument("Sternheimer supercell sector eigensystem is not square."); + } + } + const int primitive_basis_size = full_basis_size / cell_count; + + std::vector sectors; + sectors.reserve(static_cast(cell_count)); + std::vector all_sector_right_hand_sides( + static_cast(full_basis_size) + * static_cast(full_basis_size), + Complex(0.0, 0.0)); + for (int iz = 0; iz != repeats[2]; ++iz) + { + for (int iy = 0; iy != repeats[1]; ++iy) + { + for (int ix = 0; ix != repeats[0]; ++ix) + { + SternheimerSupercellKPointSector record; + record.kpoint = {static_cast(ix) / repeats[0], + static_cast(iy) / repeats[1], + static_cast(iz) / repeats[2]}; + const std::vector sector_basis + = translation_sector_basis(primitive_basis_size, + repeats, + record.kpoint); + const std::size_t column_offset + = sectors.size() * static_cast(primitive_basis_size) + * static_cast(full_basis_size); + std::copy(sector_basis.begin(), + sector_basis.end(), + all_sector_right_hand_sides.begin() + + static_cast(column_offset)); + sectors.push_back(std::move(record)); + } + } + } + + std::vector eigenvector_matrix( + static_cast(full_basis_size) + * static_cast(full_basis_size)); + for (int band = 0; band != full_basis_size; ++band) + { + for (int orbital = 0; orbital != full_basis_size; ++orbital) + { + eigenvector_matrix[column_major_index(orbital, band, full_basis_size)] + = eigenvectors[static_cast(band)] + [static_cast(orbital)]; + } + } + std::vector pivots(static_cast(full_basis_size), 0); + int info = 0; + zgesv_(&full_basis_size, + &full_basis_size, + eigenvector_matrix.data(), + &full_basis_size, + pivots.data(), + all_sector_right_hand_sides.data(), + &full_basis_size, + &info); + if (info != 0) + { + std::ostringstream message; + message << "Sternheimer supercell sectors could not invert the complete LCAO eigenvector matrix; zgesv info=" + << info << '.'; + throw std::runtime_error(message.str()); + } + + const std::size_t sector_coefficient_count + = static_cast(full_basis_size) + * static_cast(primitive_basis_size); + for (std::size_t ik = 0; ik != sectors.size(); ++ik) + { + const auto begin = all_sector_right_hand_sides.begin() + + static_cast(ik * sector_coefficient_count); + const std::vector sector_coefficients( + begin, begin + static_cast(sector_coefficient_count)); + sectors[ik].sector = recover_sector_from_eigenbasis_coefficients( + eigenvalues, + sector_coefficients, + repeats, + sectors[ik].kpoint, + primitive_basis_size, + full_basis_size); + } + return sectors; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.h new file mode 100644 index 00000000000..60c40cf3616 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.h @@ -0,0 +1,40 @@ +#ifndef STERNHEIMER_SUPERCELL_SECTOR_H +#define STERNHEIMER_SUPERCELL_SECTOR_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_kq.h" + +#include +#include +#include + +namespace ModuleRI +{ + +struct SternheimerSupercellSector +{ + std::vector eigenvalues; + std::vector>> coefficients; + double max_orthonormality_error = 0.0; + double max_full_space_residual = 0.0; +}; + +struct SternheimerSupercellKPointSector +{ + SternheimerReducedKPoint kpoint{0.0, 0.0, 0.0}; + SternheimerSupercellSector sector; +}; + +SternheimerSupercellSector recover_sternheimer_supercell_sector( + const std::vector& eigenvalues, + const std::vector>>& eigenvectors, + const std::array& repeats, + const SternheimerReducedKPoint& primitive_kpoint); + +std::vector recover_all_sternheimer_supercell_sectors( + const std::vector& eigenvalues, + const std::vector>>& eigenvectors, + const std::array& repeats); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.cpp new file mode 100644 index 00000000000..555cb1b17d3 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.cpp @@ -0,0 +1,338 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ + +constexpr std::int32_t kMagic = -53291471; +constexpr std::int32_t kVersion = 1; + +template +void write_scalar(std::ofstream& out, const T& value, const std::string& filename) +{ + out.write(reinterpret_cast(&value), sizeof(T)); + if (!out.good()) + { + throw std::runtime_error("Failed to write Sternheimer wavefunction diagnostic " + filename + "."); + } +} + +template +T read_scalar(std::ifstream& in, const std::string& filename) +{ + T value{}; + in.read(reinterpret_cast(&value), sizeof(T)); + if (!in.good()) + { + throw std::runtime_error("Truncated Sternheimer wavefunction diagnostic " + filename + "."); + } + return value; +} + +std::size_t checked_grid_size(const SternheimerWavefunctionDiagnostic::Metadata& metadata) +{ + if (metadata.nx <= 0 || metadata.ny <= 0 || metadata.nz <= 0) + { + throw std::invalid_argument("Sternheimer wavefunction diagnostic requires positive grid dimensions."); + } + const std::size_t nx = static_cast(metadata.nx); + const std::size_t ny = static_cast(metadata.ny); + const std::size_t nz = static_cast(metadata.nz); + if (nx > std::numeric_limits::max() / ny + || nx * ny > std::numeric_limits::max() / nz) + { + throw std::overflow_error("Sternheimer wavefunction diagnostic grid size overflows size_t."); + } + return nx * ny * nz; +} + +void validate_record(const SternheimerWavefunctionDiagnostic::Record& record) +{ + const std::size_t grid_size = checked_grid_size(record.metadata); + std::set names; + for (const auto& named_vector: record.vectors) + { + if (named_vector.first.empty()) + { + throw std::invalid_argument("Sternheimer wavefunction diagnostic vector name is empty."); + } + if (!names.insert(named_vector.first).second) + { + throw std::invalid_argument("Sternheimer wavefunction diagnostic vector name is duplicated."); + } + if (named_vector.second.size() != grid_size) + { + throw std::invalid_argument("Sternheimer wavefunction diagnostic vector size differs from the grid."); + } + } +} + +int parse_integer(const std::string& text, const std::string& key) +{ + std::size_t parsed = 0; + int value = 0; + try + { + value = std::stoi(text, &parsed); + } + catch (const std::exception&) + { + throw std::invalid_argument("Invalid integer for Sternheimer wavefunction diagnostic key " + key + "."); + } + if (parsed != text.size()) + { + throw std::invalid_argument("Invalid integer for Sternheimer wavefunction diagnostic key " + key + "."); + } + return value; +} + +} // namespace + +bool SternheimerWavefunctionDiagnostic::Selector::matches(const int candidate_iq, + const int candidate_ik_full, + const int candidate_ib, + const int candidate_ifrequency, + const int candidate_channel) const +{ + return iq == candidate_iq && ik_full == candidate_ik_full && ib == candidate_ib + && ifrequency == candidate_ifrequency && channel == candidate_channel; +} + +SternheimerWavefunctionDiagnostic::Configuration +SternheimerWavefunctionDiagnostic::parse_configuration(const std::string& specification) +{ + std::map fields; + std::istringstream input(specification); + std::string field; + while (std::getline(input, field, ',')) + { + const std::size_t separator = field.find('='); + if (separator == std::string::npos || separator == 0 || separator + 1 == field.size()) + { + throw std::invalid_argument("Invalid Sternheimer wavefunction diagnostic field."); + } + const std::string key = field.substr(0, separator); + const std::string value = field.substr(separator + 1); + if (!fields.insert({key, value}).second) + { + throw std::invalid_argument("Duplicated Sternheimer wavefunction diagnostic key " + key + "."); + } + } + const std::set required{"iq", "ik", "ib", "ifreq", "channel", "out"}; + if (fields.size() != required.size()) + { + throw std::invalid_argument("Sternheimer wavefunction diagnostic requires iq,ik,ib,ifreq,channel,out."); + } + for (const auto& entry: fields) + { + if (required.count(entry.first) == 0) + { + throw std::invalid_argument("Unknown Sternheimer wavefunction diagnostic key " + entry.first + "."); + } + } + + Configuration config; + config.selector.iq = parse_integer(fields.at("iq"), "iq"); + config.selector.ik_full = parse_integer(fields.at("ik"), "ik"); + config.selector.ib = parse_integer(fields.at("ib"), "ib"); + config.selector.ifrequency = parse_integer(fields.at("ifreq"), "ifreq"); + config.selector.channel = parse_integer(fields.at("channel"), "channel"); + config.output_filename = fields.at("out"); + if (config.selector.iq <= 0 || config.selector.ifrequency <= 0 || config.selector.ik_full < 0 + || config.selector.ib < 0 || config.selector.channel < 0 || config.output_filename.empty()) + { + throw std::invalid_argument("Invalid Sternheimer wavefunction diagnostic selector range."); + } + return config; +} + +void SternheimerWavefunctionDiagnostic::write(const std::string& filename, const Record& record) +{ + validate_record(record); + std::ofstream out(filename.c_str(), std::ios::binary | std::ios::trunc); + if (!out.good()) + { + throw std::runtime_error("Failed to open Sternheimer wavefunction diagnostic " + filename + "."); + } + + write_scalar(out, kMagic, filename); + write_scalar(out, kVersion, filename); + const std::int32_t indices[] = {record.metadata.nx, + record.metadata.ny, + record.metadata.nz, + record.metadata.iq, + record.metadata.ik_full, + record.metadata.ib, + record.metadata.ifrequency, + record.metadata.channel}; + for (const std::int32_t value: indices) + { + write_scalar(out, value, filename); + } + for (const double value: record.metadata.lattice) + { + write_scalar(out, value, filename); + } + for (const double value: record.metadata.qpoint) + { + write_scalar(out, value, filename); + } + for (const double value: record.metadata.source_kpoint) + { + write_scalar(out, value, filename); + } + for (const double value: record.metadata.target_kpoint) + { + write_scalar(out, value, filename); + } + const double scalars[] = {record.metadata.omega_ha, + record.metadata.omega_ry, + record.metadata.volume_element, + record.metadata.reference_eigenvalue_ry, + record.metadata.weighted_occupation, + record.metadata.rhs_norm, + record.metadata.solver_relative_residual, + record.metadata.equation_relative_residual, + record.metadata.diagonal_branch_element.real(), + record.metadata.diagonal_branch_element.imag()}; + for (const double value: scalars) + { + write_scalar(out, value, filename); + } + if (record.vectors.size() > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Too many Sternheimer wavefunction diagnostic vectors."); + } + const std::int32_t vector_count = static_cast(record.vectors.size()); + write_scalar(out, vector_count, filename); + for (const auto& named_vector: record.vectors) + { + if (named_vector.first.size() > static_cast(std::numeric_limits::max()) + || named_vector.second.size() > static_cast(std::numeric_limits::max())) + { + throw std::overflow_error("Sternheimer wavefunction diagnostic vector metadata overflows."); + } + const std::int32_t name_size = static_cast(named_vector.first.size()); + const std::int64_t vector_size = static_cast(named_vector.second.size()); + write_scalar(out, name_size, filename); + out.write(named_vector.first.data(), name_size); + write_scalar(out, vector_size, filename); + out.write(reinterpret_cast(named_vector.second.data()), + static_cast(named_vector.second.size() * sizeof(Complex))); + if (!out.good()) + { + throw std::runtime_error("Failed to write Sternheimer wavefunction diagnostic " + filename + "."); + } + } +} + +SternheimerWavefunctionDiagnostic::Record SternheimerWavefunctionDiagnostic::read(const std::string& filename) +{ + std::ifstream in(filename.c_str(), std::ios::binary); + if (!in.good()) + { + throw std::runtime_error("Failed to open Sternheimer wavefunction diagnostic " + filename + "."); + } + if (read_scalar(in, filename) != kMagic + || read_scalar(in, filename) != kVersion) + { + throw std::runtime_error("Unsupported Sternheimer wavefunction diagnostic format in " + filename + "."); + } + + Record record; + int* indices[] = {&record.metadata.nx, + &record.metadata.ny, + &record.metadata.nz, + &record.metadata.iq, + &record.metadata.ik_full, + &record.metadata.ib, + &record.metadata.ifrequency, + &record.metadata.channel}; + for (int* value: indices) + { + *value = read_scalar(in, filename); + } + for (double& value: record.metadata.lattice) + { + value = read_scalar(in, filename); + } + for (double& value: record.metadata.qpoint) + { + value = read_scalar(in, filename); + } + for (double& value: record.metadata.source_kpoint) + { + value = read_scalar(in, filename); + } + for (double& value: record.metadata.target_kpoint) + { + value = read_scalar(in, filename); + } + double diagonal_branch_real = 0.0; + double diagonal_branch_imag = 0.0; + double* scalars[] = {&record.metadata.omega_ha, + &record.metadata.omega_ry, + &record.metadata.volume_element, + &record.metadata.reference_eigenvalue_ry, + &record.metadata.weighted_occupation, + &record.metadata.rhs_norm, + &record.metadata.solver_relative_residual, + &record.metadata.equation_relative_residual, + &diagonal_branch_real, + &diagonal_branch_imag}; + for (double* value: scalars) + { + *value = read_scalar(in, filename); + } + record.metadata.diagonal_branch_element = Complex(diagonal_branch_real, diagonal_branch_imag); + const std::size_t grid_size = checked_grid_size(record.metadata); + + const std::int32_t vector_count = read_scalar(in, filename); + if (vector_count < 0) + { + throw std::runtime_error("Negative vector count in Sternheimer wavefunction diagnostic " + filename + "."); + } + record.vectors.reserve(static_cast(vector_count)); + for (std::int32_t ivector = 0; ivector != vector_count; ++ivector) + { + const std::int32_t name_size = read_scalar(in, filename); + if (name_size <= 0 || name_size > 4096) + { + throw std::runtime_error("Invalid vector name in Sternheimer wavefunction diagnostic " + filename + "."); + } + std::string name(static_cast(name_size), '\0'); + in.read(&name[0], name_size); + if (!in.good()) + { + throw std::runtime_error("Truncated Sternheimer wavefunction diagnostic " + filename + "."); + } + const std::int64_t vector_size = read_scalar(in, filename); + if (vector_size < 0 + || static_cast(vector_size) != static_cast(grid_size)) + { + throw std::runtime_error("Vector size differs from the grid in Sternheimer wavefunction diagnostic " + + filename + "."); + } + Vector values(static_cast(vector_size)); + in.read(reinterpret_cast(values.data()), + static_cast(values.size() * sizeof(Complex))); + if (!in.good()) + { + throw std::runtime_error("Truncated Sternheimer wavefunction diagnostic " + filename + "."); + } + record.vectors.push_back({std::move(name), std::move(values)}); + } + validate_record(record); + return record; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.h new file mode 100644 index 00000000000..738567ef004 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.h @@ -0,0 +1,79 @@ +#ifndef STERNHEIMER_WAVEFUNCTION_DIAGNOSTIC_H +#define STERNHEIMER_WAVEFUNCTION_DIAGNOSTIC_H + +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +class SternheimerWavefunctionDiagnostic +{ + public: + using Complex = std::complex; + using Vector = std::vector; + using NamedVector = std::pair; + + struct Selector + { + int iq = 0; + int ik_full = -1; + int ib = -1; + int ifrequency = 0; + int channel = -1; + + bool matches(int candidate_iq, + int candidate_ik_full, + int candidate_ib, + int candidate_ifrequency, + int candidate_channel) const; + }; + + struct Configuration + { + Selector selector; + std::string output_filename; + }; + + struct Metadata + { + int nx = 0; + int ny = 0; + int nz = 0; + int iq = 0; + int ik_full = -1; + int ib = -1; + int ifrequency = 0; + int channel = -1; + std::array lattice{}; + std::array qpoint{}; + std::array source_kpoint{}; + std::array target_kpoint{}; + double omega_ha = 0.0; + double omega_ry = 0.0; + double volume_element = 0.0; + double reference_eigenvalue_ry = 0.0; + double weighted_occupation = 0.0; + double rhs_norm = 0.0; + double solver_relative_residual = 0.0; + double equation_relative_residual = 0.0; + Complex diagonal_branch_element{}; + }; + + struct Record + { + Metadata metadata; + std::vector vectors; + }; + + static void write(const std::string& filename, const Record& record); + static Record read(const std::string& filename); + static Configuration parse_configuration(const std::string& specification); +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.cpp new file mode 100644 index 00000000000..d51d05588c9 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.cpp @@ -0,0 +1,685 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.h" + +#include "source_base/module_external/blas_connector.h" +#include "source_base/module_external/lapack_connector.h" + +#include +#include +#include +#include +#include +#include +#include + +// The existing connector declares ZGETRF/ZHEEV but not these LU operations. +extern "C" void zgetrs_(const char* trans, const int* n, const int* nrhs, + const std::complex* a, const int* lda, const int* ipiv, + std::complex* b, const int* ldb, int* info); +extern "C" void zgecon_(const char* norm, const int* n, const std::complex* a, + const int* lda, const double* anorm, double* rcond, + std::complex* work, double* rwork, int* info); + +namespace ModuleRI +{ +namespace +{ +using Blocks = SternheimerWeakAugmented; +using Complex = Blocks::Complex; +using Vector = Blocks::Vector; + +std::size_t add(const std::size_t a, const std::size_t b) +{ + if (b > std::numeric_limits::max() - a) + { + throw std::length_error("weak augmented payload size overflow"); + } + return a + b; +} + +std::size_t product(const std::size_t a, const std::size_t b) +{ + if (a != 0 && b > std::numeric_limits::max() / a) + { + throw std::length_error("weak augmented payload size overflow"); + } + return a * b; +} + +bool finite(const Complex value) +{ + return std::isfinite(value.real()) && std::isfinite(value.imag()); +} + +void validate(const Vector& values, const std::size_t size, const char* name) +{ + if (values.size() != size) + { + throw std::invalid_argument(std::string("weak augmented ") + name + " dimension mismatch"); + } + for (const auto value : values) + { + if (!finite(value)) + { + throw std::invalid_argument(std::string("weak augmented ") + name + " contains nonfinite values"); + } + } +} + +void budget(const std::size_t required, const std::size_t limit) +{ + if (required > limit) + { + throw std::length_error("weak augmented numerical payload exceeds budget"); + } +} + +// Packed column-major matrix-vector product; never forms a coarse square block. +Vector multiply(const Vector& matrix, const int rows, const int cols, const Vector& x, + const bool adjoint = false) +{ + Vector y(adjoint ? cols : rows, 0.0); + if (rows != 0 && cols != 0) + { + BlasConnector::gemv(adjoint ? 'C' : 'N', rows, cols, Complex(1.0), + matrix.data(), rows, x.data(), 1, Complex(0.0), y.data(), 1); + } + validate(y, y.size(), "matrix product"); + return y; +} + +double norm(const Vector& x) +{ + double value = 0.0; + for (const auto element : x) + { + value = std::hypot(value, std::abs(element)); + } + if (!std::isfinite(value)) + { + throw std::runtime_error("weak augmented norm overflow"); + } + return value; +} + +Complex euclidean_dot(const Vector& a, const Vector& b) +{ + validate(a, b.size(), "dot input"); + validate(b, a.size(), "dot input"); + Complex value = 0.0; + for (std::size_t i = 0; i < a.size(); ++i) + { + value += std::conj(a[i]) * b[i]; + } + if (!finite(value)) + { + throw std::runtime_error("weak augmented dot overflow"); + } + return value; +} + +} // namespace + +SternheimerWeakAugmented::SternheimerWeakAugmented(Data data, + const std::size_t max_payload_bytes, + const double metric_tolerance, + const double hermitian_tolerance) + : data_(std::move(data)) +{ + if (data_.nocc < 0 || data_.nvirtual < 0 || data_.ncoarse < 0 + || data_.nocc > std::numeric_limits::max() / 4 - data_.nvirtual + || data_.ncoarse > std::numeric_limits::max() - data_.nvirtual + || (data_.nvirtual == 0 && data_.ncoarse == 0)) + { + throw std::invalid_argument("weak augmented invalid dimensions"); + } + if (!std::isfinite(metric_tolerance) || metric_tolerance <= 0.0 || metric_tolerance >= 1.0 + || !std::isfinite(hermitian_tolerance) || hermitian_tolerance <= 0.0 + || hermitian_tolerance >= 1.0) + { + throw std::invalid_argument("weak augmented invalid diagnostic tolerance"); + } + nu_ = data_.nocc + data_.nvirtual; + const std::size_t square = product(nu_, nu_); + const std::size_t rectangular = product(nu_, data_.ncoarse); + validate(data_.hu, square, "HU"); + validate(data_.l, rectangular, "L"); + validate(data_.k, rectangular, "K"); + // ZHEEV minimum work: max(1,2*nu-1) complex, max(1,3*nu-2) real. + const std::size_t scratch = add(product(add(square, product(2, nu_)), sizeof(Complex)), + product(product(4, nu_), sizeof(double))); + // In addition to the eigensolver matrix, retain the small inverse-root + // kernel and a temporary weighted eigenvector matrix. + budget(add(storage_bytes(), add(scratch, product(product(2, square), sizeof(Complex)))), max_payload_bytes); + double scale = 1.0; + for (const auto value : data_.hu) + { + scale = std::max(scale, std::abs(value)); + } + if (!std::isfinite(scale)) + { + throw std::invalid_argument("weak augmented HU magnitude overflow"); + } + for (int j = 0; j < nu_; ++j) + { + for (int i = 0; i < nu_; ++i) + { + const auto ij = i + static_cast(nu_) * j; + const auto ji = j + static_cast(nu_) * i; + if (std::abs(data_.hu[ij] / scale - std::conj(data_.hu[ji]) / scale) > hermitian_tolerance) + { + throw std::invalid_argument("weak augmented HU is not Hermitian"); + } + } + } + if (nu_ == 0 || data_.ncoarse == 0) + { + return; + } + + // LL* and L*L share their nonzero spectrum, so this detects coherent null + // directions of W even when every individual W column has nonzero norm. + Vector gram(square, 0.0); + BlasConnector::gemm_cm('N', 'C', nu_, nu_, data_.ncoarse, Complex(1.0), + data_.l.data(), nu_, data_.l.data(), nu_, Complex(0.0), gram.data(), nu_); + validate(gram, square, "LL* metric diagnostic"); + std::vector eigenvalues(nu_); + std::vector rwork(std::max(1, 3 * nu_ - 2)); + const int lwork = std::max(1, 2 * nu_ - 1); + Vector work(lwork); + const char jobz = 'V'; + const char uplo = 'U'; + int info = 0; + zheev_(&jobz, &uplo, &nu_, gram.data(), &nu_, eigenvalues.data(), work.data(), &lwork, + rwork.data(), &info); + if (info != 0 || !std::isfinite(eigenvalues.back())) + { + throw std::runtime_error("weak augmented metric eigensolver failed"); + } + minimum_metric_eigenvalue_ = 1.0 - eigenvalues.back(); + if (minimum_metric_eigenvalue_ <= metric_tolerance) + { + std::ostringstream message; + message.precision(17); + message << "weak augmented complement metric is " + << (minimum_metric_eigenvalue_ < -metric_tolerance ? "indefinite" : "singular or ill-conditioned") + << "; minimum eigenvalue=" << minimum_metric_eigenvalue_ + << ", rejection threshold=" << metric_tolerance << "; no regularization applied"; + throw std::domain_error(message.str()); + } + Vector scaled = gram; + for (int j = 0; j < nu_; ++j) + { + if (!std::isfinite(eigenvalues[j])) + throw std::runtime_error("weak augmented metric spectrum is nonfinite"); + const double root = std::sqrt(1.0 - eigenvalues[j]); + // Stable even for lambda=0: ((1-lambda)^(-1/2)-1)/lambda. + const double factor = 1.0 / (root * (1.0 + root)); + for (int i = 0; i < nu_; ++i) scaled[i + static_cast(nu_) * j] *= factor; + } + inverse_sqrt_kernel_.resize(square); + BlasConnector::gemm_cm('N', 'C', nu_, nu_, nu_, Complex(1.0), + scaled.data(), nu_, gram.data(), nu_, Complex(0.0), + inverse_sqrt_kernel_.data(), nu_); + validate(inverse_sqrt_kernel_, square, "metric inverse square root kernel"); +} + +int SternheimerWeakAugmented::nocc() const { return data_.nocc; } +int SternheimerWeakAugmented::nvirtual() const { return data_.nvirtual; } +int SternheimerWeakAugmented::ncoarse() const { return data_.ncoarse; } + +std::size_t SternheimerWeakAugmented::storage_bytes() const +{ + return product(add(inverse_sqrt_kernel_.capacity(), + add(data_.hu.capacity(), add(data_.l.capacity(), data_.k.capacity()))), sizeof(Complex)); +} + +double SternheimerWeakAugmented::minimum_metric_eigenvalue() const +{ + return minimum_metric_eigenvalue_; +} + +void SternheimerWeakAugmented::validate_coefficients(const Vector& coefficients) const +{ + validate(coefficients, add(nvirtual(), ncoarse()), "[y;x]"); +} + +void SternheimerWeakAugmented::apply_complement_metric(const Vector& x, Vector& output) const +{ + const Vector lx = multiply(data_.l, nu_, ncoarse(), x); + Vector result = multiply(data_.l, nu_, ncoarse(), lx, true); + for (int i = 0; i < ncoarse(); ++i) + { + result[i] = x[i] - result[i]; + } + validate(result, ncoarse(), "S_W x"); + output = std::move(result); +} + +void SternheimerWeakAugmented::apply_metric(const Vector& coefficients, Vector& output) const +{ + validate_coefficients(coefficients); + const Vector x(coefficients.begin() + nvirtual(), coefficients.end()); + Vector sx; + apply_complement_metric(x, sx); + Vector result(coefficients.begin(), coefficients.begin() + nvirtual()); + result.insert(result.end(), sx.begin(), sx.end()); + output = std::move(result); +} + +void SternheimerWeakAugmented::apply_complement_inverse_sqrt(const Vector& x, Vector& output) const +{ + validate(x, ncoarse(), "inverse metric square root input"); + if (nu_ == 0 || ncoarse() == 0) + { + output = x; + return; + } + const Vector lx = multiply(data_.l, nu_, ncoarse(), x); + const Vector correction = multiply(inverse_sqrt_kernel_, nu_, nu_, lx); + Vector result = multiply(data_.l, nu_, ncoarse(), correction, true); + for (int i = 0; i < ncoarse(); ++i) result[i] += x[i]; + validate(result, ncoarse(), "inverse metric square root output"); + output = std::move(result); +} + +void SternheimerWeakAugmented::apply_complement_sqrt(const Vector& x, Vector& output) const +{ + validate(x, ncoarse(), "metric square root input"); + Vector inverse_rooted; + apply_complement_inverse_sqrt(x, inverse_rooted); + apply_complement_metric(inverse_rooted, output); + validate(output, ncoarse(), "metric square root output"); +} + +void SternheimerWeakAugmented::apply_b(const Vector& x, Vector& output) const +{ + const Vector lx = multiply(data_.l, nu_, ncoarse(), x); + const Vector hlx = multiply(data_.hu, nu_, nu_, lx); + const Vector kx = multiply(data_.k, nu_, ncoarse(), x); + Vector result(nvirtual()); + for (int i = 0; i < nvirtual(); ++i) + { + result[i] = kx[nocc() + i] - hlx[nocc() + i]; + } + validate(result, nvirtual(), "B x"); + output = std::move(result); +} + +void SternheimerWeakAugmented::apply_b_adjoint(const Vector& y, Vector& output) const +{ + Vector uy(nu_, 0.0); + std::copy(y.begin(), y.end(), uy.begin() + nocc()); + const Vector huy = multiply(data_.hu, nu_, nu_, uy, true); + Vector result = multiply(data_.k, nu_, ncoarse(), uy, true); + const Vector lhuy = multiply(data_.l, nu_, ncoarse(), huy, true); + for (int i = 0; i < ncoarse(); ++i) + { + result[i] -= lhuy[i]; + } + validate(result, ncoarse(), "B* y"); + output = std::move(result); +} + +SternheimerWeakAugmented::Vertices SternheimerWeakAugmented::project_vertices(const Vector& g_u, + const Vector& g_e) const +{ + validate(g_u, nu_, "gU"); + validate(g_e, ncoarse(), "gE"); + Vertices result; + result.f.assign(g_u.begin() + nocc(), g_u.end()); + result.w = multiply(data_.l, nu_, ncoarse(), g_u, true); + for (int i = 0; i < ncoarse(); ++i) + { + result.w[i] = g_e[i] - result.w[i]; + } + validate(result.w, ncoarse(), "gW"); + return result; +} + +SternheimerWeakAugmented::Expansion SternheimerWeakAugmented::expand_coordinates(const Vector& coefficients) const +{ + validate_coefficients(coefficients); + Expansion result; + result.e.assign(coefficients.begin() + nvirtual(), coefficients.end()); + result.u = multiply(data_.l, nu_, ncoarse(), result.e); + for (auto& value : result.u) + { + value = -value; + } + for (int i = 0; i < nvirtual(); ++i) + { + result.u[nocc() + i] += coefficients[i]; + } + validate(result.u, nu_, "expanded U coordinates"); + return result; +} + +SternheimerWeakAugmented::Worker::Worker(std::shared_ptr blocks, + Apply ce, const double eps_source, const double omega, + const std::size_t max_workspace_bytes, + Apply complement_preconditioner) + : blocks_(std::move(blocks)), ce_(std::move(ce)), + complement_preconditioner_(std::move(complement_preconditioner)), + z_(eps_source, -omega), max_workspace_bytes_(max_workspace_bytes) +{ + if (!blocks_ || (blocks_->ncoarse() != 0 && !ce_) || !finite(z_)) + { + throw std::invalid_argument("weak augmented worker requires blocks, finite shift and CE callback"); + } + budget(workspace_bytes_required(0), max_workspace_bytes_); + const int nv = blocks_->nvirtual(); + if (nv == 0) + { + return; + } + lu_.resize(product(nv, nv)); + pivots_.resize(nv); + for (int j = 0; j < nv; ++j) + { + for (int i = 0; i < nv; ++i) + { + lu_[i + static_cast(nv) * j] + = blocks_->data_.hu[blocks_->nocc() + i + + static_cast(blocks_->nu_) * (blocks_->nocc() + j)] + - (i == j ? z_ : Complex(0.0)); + } + } + validate(lu_, product(nv, nv), "virtual resolvent"); + double one_norm = 0.0; + for (int j = 0; j < nv; ++j) + { + double column_sum = 0.0; + for (int i = 0; i < nv; ++i) + { + column_sum += std::abs(lu_[i + static_cast(nv) * j]); + } + one_norm = std::max(one_norm, column_sum); + } + if (!std::isfinite(one_norm)) + { + throw std::runtime_error("weak augmented virtual resolvent norm overflow"); + } + int info = 0; + zgetrf_(&nv, &nv, lu_.data(), &nv, pivots_.data(), &info); + if (info > 0) + { + throw std::domain_error("weak augmented singular virtual resolvent A-zI; no energy shift applied"); + } + if (info < 0) + { + throw std::runtime_error("weak augmented virtual LU factorization failed"); + } + validate(lu_, product(nv, nv), "virtual LU"); + const char norm_type = '1'; + double rcond = 0.0; + Vector work(2 * nv); + std::vector rwork(2 * nv); + zgecon_(&norm_type, &nv, lu_.data(), &nv, &one_norm, &rcond, work.data(), rwork.data(), &info); + if (info != 0 || !std::isfinite(rcond)) + { + throw std::runtime_error("weak augmented virtual resolvent condition estimate failed"); + } + if (rcond <= std::numeric_limits::epsilon()) + { + std::ostringstream message; + message.precision(17); + message << "weak augmented singular or numerically unresolved virtual resolvent A-zI; rcond=" + << rcond << "; no energy shift applied"; + throw std::domain_error(message.str()); + } +} + +std::size_t SternheimerWeakAugmented::Worker::workspace_bytes_required(const int restart_dimension) const +{ + if (restart_dimension < 0) + { + throw std::invalid_argument("weak augmented negative restart dimension"); + } + const std::size_t nv = blocks_->nvirtual(); + const std::size_t nc = blocks_->ncoarse(); + const std::size_t nu = blocks_->nu_; + const std::size_t r = restart_dimension; + // Generous vector allowance covers nested products, full residual, RHS, + // reconstruction and callback output (not opaque callback internals). + std::size_t count = add(product(nv, nv), product(32, add(nu, nc))); + count = add(count, product(add(product(2, r), 1), nc)); + // Hessenberg plus the unmodified GMRES normal-equation solver and copies. + count = add(count, add(product(4, product(add(r, 1), add(r, 1))), product(8, add(r, 1)))); + return add(product(count, sizeof(Complex)), product(nv, sizeof(int))); +} + +Vector SternheimerWeakAugmented::Worker::solve_virtual(const Vector& rhs) const +{ + const int n = blocks_->nvirtual(); + validate(rhs, n, "virtual RHS"); + Vector result = rhs; + if (n != 0) + { + const char trans = 'N'; + const int nrhs = 1; + int info = 0; + zgetrs_(&trans, &n, &nrhs, lu_.data(), &n, pivots_.data(), result.data(), &n, &info); + if (info != 0) + { + throw std::runtime_error("weak augmented virtual LU solve failed"); + } + validate(result, n, "virtual solution"); + } + return result; +} + +void SternheimerWeakAugmented::Worker::apply_complement(const Vector& x, Vector& output) const +{ + const auto& d = blocks_->data_; + const int nu = blocks_->nu_; + Vector result(d.ncoarse, 0.0); + if (d.ncoarse != 0) + { + ce_(x, result); + validate(result, d.ncoarse, "CE output"); + } + const Vector lx = multiply(d.l, nu, d.ncoarse, x); + const Vector kx = multiply(d.k, nu, d.ncoarse, x); + const Vector hlx = multiply(d.hu, nu, nu, lx); + const Vector lkx = multiply(d.l, nu, d.ncoarse, kx, true); + const Vector klx = multiply(d.k, nu, d.ncoarse, lx, true); + const Vector lhlx = multiply(d.l, nu, d.ncoarse, hlx, true); + for (int i = 0; i < d.ncoarse; ++i) + { + result[i] += -lkx[i] - klx[i] + lhlx[i]; + } + validate(result, d.ncoarse, "C x"); + output = std::move(result); +} + +void SternheimerWeakAugmented::Worker::apply_hamiltonian(const Vector& coefficients, Vector& output) const +{ + blocks_->validate_coefficients(coefficients); + const int nv = blocks_->nvirtual(); + const Vector y(coefficients.begin(), coefficients.begin() + nv); + const Vector x(coefficients.begin() + nv, coefficients.end()); + Vector result; + blocks_->apply_b(x, result); + for (int j = 0; j < nv; ++j) + { + for (int i = 0; i < nv; ++i) + { + result[i] += blocks_->data_.hu[blocks_->nocc() + i + + static_cast(blocks_->nu_) * (blocks_->nocc() + j)] * y[j]; + } + } + Vector lower; + apply_complement(x, lower); + Vector by; + blocks_->apply_b_adjoint(y, by); + for (int i = 0; i < blocks_->ncoarse(); ++i) + { + lower[i] += by[i]; + } + result.insert(result.end(), lower.begin(), lower.end()); + validate(result, coefficients.size(), "H augmented output"); + output = std::move(result); +} + +void SternheimerWeakAugmented::Worker::apply_shifted(const Vector& coefficients, Vector& output) const +{ + Vector h, s; + apply_hamiltonian(coefficients, h); + blocks_->apply_metric(coefficients, s); + for (std::size_t i = 0; i < h.size(); ++i) + { + h[i] -= z_ * s[i]; + } + validate(h, coefficients.size(), "(H-zS) output"); + output = std::move(h); +} + +void SternheimerWeakAugmented::Worker::apply_schur(const Vector& x, Vector& output) const +{ + validate(x, blocks_->ncoarse(), "Schur input"); + Vector c, s, bx, correction; + apply_complement(x, c); + blocks_->apply_complement_metric(x, s); + blocks_->apply_b(x, bx); + blocks_->apply_b_adjoint(solve_virtual(bx), correction); + for (std::size_t i = 0; i < x.size(); ++i) + { + c[i] -= z_ * s[i] + correction[i]; + } + validate(c, x.size(), "Schur output"); + output = std::move(c); +} + +void SternheimerWeakAugmented::Worker::apply_normalized_preconditioner(const Vector& x, + Vector& output) const +{ + validate(x, blocks_->ncoarse(), "normalized preconditioner input"); + if (!complement_preconditioner_) + { + output = x; + return; + } + Vector preconditioned(blocks_->ncoarse(), 0.0); + complement_preconditioner_(x, preconditioned); + validate(preconditioned, blocks_->ncoarse(), "complement preconditioner output"); + output = std::move(preconditioned); +} + +void SternheimerWeakAugmented::Worker::validate_vertices(const Vertices& g) const +{ + validate(g.f, blocks_->nvirtual(), "gF"); + validate(g.w, blocks_->ncoarse(), "gW"); +} + +Vector SternheimerWeakAugmented::Worker::schur_rhs(const Vertices& g) const +{ + validate_vertices(g); + Vector result; + blocks_->apply_b_adjoint(solve_virtual(g.f), result); + for (std::size_t i = 0; i < result.size(); ++i) + { + result[i] -= g.w[i]; + } + validate(result, blocks_->ncoarse(), "Schur RHS"); + return result; +} + +Vector SternheimerWeakAugmented::Worker::reconstruct(const Vertices& g, const Vector& x) const +{ + validate_vertices(g); + validate(x, blocks_->ncoarse(), "reconstruction x"); + Vector rhs; + blocks_->apply_b(x, rhs); + for (std::size_t i = 0; i < rhs.size(); ++i) + { + rhs[i] = -g.f[i] - rhs[i]; + } + Vector result = solve_virtual(rhs); + result.insert(result.end(), x.begin(), x.end()); + return result; +} + +SternheimerWeakAugmented::Worker::SolveResult SternheimerWeakAugmented::Worker::solve( + const Vertices& g, const SternheimerRPA::SolverOptions& options, const int restart_dimension) const +{ + validate_vertices(g); + if (options.max_iter <= 0 || restart_dimension <= 0 + || !std::isfinite(options.residual_tol) || options.residual_tol <= 0.0 + || !std::isfinite(options.breakdown_tol) || options.breakdown_tol <= 0.0) + { + throw std::invalid_argument("weak augmented invalid GMRES options"); + } + budget(workspace_bytes_required(restart_dimension), max_workspace_bytes_); + SolveResult result; + Vector x(blocks_->ncoarse(), 0.0); + if (blocks_->ncoarse() != 0) + { + const Vector rhs = schur_rhs(g); + Vector normalized_rhs; + blocks_->apply_complement_inverse_sqrt(rhs, normalized_rhs); + const double rhs_norm = norm(rhs); + const double normalized_rhs_norm = norm(normalized_rhs); + Vector source = g.f; + source.insert(source.end(), g.w.begin(), g.w.end()); + auto normalized_options = options; + if (normalized_rhs_norm > 0.0) + { + // S=I-L*L <= I implies ||r|| <= ||T r||. Target both original + // Schur and full-source norms, with margin for the coordinate map. + const double ratio = std::min(1.0, std::min(rhs_norm, norm(source)) / normalized_rhs_norm); + normalized_options.residual_tol *= 0.5 * ratio; + if (!(normalized_options.residual_tol > 0.0) || !std::isfinite(normalized_options.residual_tol)) + throw std::invalid_argument("weak augmented normalized residual target is not representable"); + } + SternheimerRPA::LinearProblem problem; + problem.apply = [this](const Vector& input, Vector& output) { + Vector original, applied; + blocks_->apply_complement_inverse_sqrt(input, original); + apply_schur(original, applied); + blocks_->apply_complement_inverse_sqrt(applied, output); + }; + if (complement_preconditioner_) + { + problem.precondition = [this](const Vector& input, Vector& output) { + apply_normalized_preconditioner(input, output); + }; + } + problem.dot = euclidean_dot; + Vector normalized_x(blocks_->ncoarse(), 0.0); + result.schur = SternheimerRPA::solve_gmres(problem, normalized_rhs, normalized_x, normalized_options, restart_dimension); + blocks_->apply_complement_inverse_sqrt(normalized_x, x); + // Existing GMRES may stop on its small least-squares residual. Always + // reapply the actual operator before declaring even Schur convergence. + Vector residual; + apply_schur(x, residual); + for (std::size_t i = 0; i < rhs.size(); ++i) + { + residual[i] -= rhs[i]; + } + result.schur.absolute_residual = norm(residual); + result.schur.relative_residual = result.schur.absolute_residual / (rhs_norm > 0.0 ? rhs_norm : 1.0); + // The conservative transformed target may exhaust the iteration budget + // after the original equation is already solved; the original gate wins. + result.schur.converged = result.schur.relative_residual <= options.residual_tol; + } + else + { + result.schur.converged = true; + } + result.coefficients = reconstruct(g, x); + Vector residual; + apply_shifted(result.coefficients, residual); + Vector source = g.f; + source.insert(source.end(), g.w.begin(), g.w.end()); + for (std::size_t i = 0; i < source.size(); ++i) + { + residual[i] += source[i]; + } + result.absolute_residual = norm(residual); + const double source_norm = norm(source); + result.relative_residual = result.absolute_residual / (source_norm > 0.0 ? source_norm : 1.0); + result.converged = result.schur.converged && result.relative_residual <= options.residual_tol; + return result; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.h new file mode 100644 index 00000000000..3addfa76bae --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.h @@ -0,0 +1,170 @@ +#ifndef STERNHEIMER_WEAK_AUGMENTED_H +#define STERNHEIMER_WEAK_AUGMENTED_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" + +#include +#include +#include +#include + +namespace ModuleRI +{ + +// Low-level algebra only, with no production response/FFT/weak-form assembly. +// U=[O,F] is fine-metric orthonormal; E is normalized so E*E=I. All supplied +// coefficients and inner products are Euclidean: never insert dVc or dVf here. +// This helper cannot verify the basis normalization or full-Pc positivity. +class SternheimerWeakAugmented +{ + public: + using Complex = SternheimerRPA::Complex; + using Vector = SternheimerRPA::Vector; + using Apply = std::function; + + struct Data + { + int nocc = 0; + int nvirtual = 0; + int ncoarse = 0; + // All matrices are packed COLUMN-MAJOR, index row + leading_dim*col. + // hu: nu x nu; l and k: nu x ncoarse; nu=nocc+nvirtual. + // hu=h(U,U), l=U*E, k=h(U,E). Occupied rows precede virtual rows. + Vector hu; + Vector l; + Vector k; + }; + + struct Vertices + { + Vector f; + Vector w; + }; + + struct Expansion + { + // Response U*u + E*e = F*y + (E-U*L)*x, not a fine-grid vector. + Vector u; + Vector e; + }; + + // Owns the supplied matrices, with no mutable shared application scratch. + // Budget covers retained matrix capacities plus metric eigensolver scratch; + // excludes caller-owned input copies, object/allocator and LAPACK overhead. + // Diagnoses min eig(S_W)=1-lambda_max(L L*) in nu space, never dense coarse + // space. Throws domain_error on singular/ill-conditioned/indefinite metric. + // metric_tolerance is a rejection threshold, NOT a regularization or shift. + explicit SternheimerWeakAugmented(Data data, + std::size_t max_payload_bytes = 512ULL * 1024 * 1024, + double metric_tolerance = 1.0e-12, + double hermitian_tolerance = 1.0e-12); + SternheimerWeakAugmented(const SternheimerWeakAugmented&) = delete; + SternheimerWeakAugmented& operator=(const SternheimerWeakAugmented&) = delete; + + int nocc() const; + int nvirtual() const; + int ncoarse() const; + std::size_t storage_bytes() const; + // Returns 1 for the empty complement; it is not a Hamiltonian spectrum test. + double minimum_metric_eigenvalue() const; + + // Augmented vectors always concatenate [y;x]. Input/output may alias. + void apply_metric(const Vector& coefficients, Vector& output) const; + // Exact positive-metric coordinate normalization T=(I-L*L)^(-1/2). + // Low-rank application; no coarse square matrix, shift or mode removal. + void apply_complement_inverse_sqrt(const Vector& x, Vector& output) const; + // Exact inverse coordinate map S_W^(1/2)=S_W S_W^(-1/2), applied + // through the same low-rank factors without a dense coarse matrix. + void apply_complement_sqrt(const Vector& x, Vector& output) const; + // gF=(gU)_F; gW=gE-L* gU. Inputs are already projected fine-grid vertices. + Vertices project_vertices(const Vector& g_u, const Vector& g_e) const; + Expansion expand_coordinates(const Vector& coefficients) const; + + class Worker + { + public: + struct SolveResult + { + Vector coefficients; + SternheimerRPA::SolverResult schur; + bool converged = false; + // Recomputed residual of BOTH original augmented equations, + // normalized by ||[gF;gW]|| (absolute residual if that norm is zero). + double absolute_residual = 0.0; + double relative_residual = 0.0; + }; + + // CE applies h(E,E), must be fixed, linear and Hermitian, and must return + // exactly ncoarse finite entries. Hermiticity/linearity of an arbitrary + // callback is a caller contract, not established by finite probes here. + // A worker owns its callback and LU(A-zI). Concurrent workers may share + // Blocks; any shared callback state must itself be thread-safe. Callbacks + // receive distinct input/output vectors, with output initially zeroed. + // z=eps_source-i*omega; signed omega supports the adjoint branch. No + // energy shift, eigenmode truncation or pseudoinverse is performed. + // Rejects singular LU and a ZGECON reciprocal condition estimate at or + // below double epsilon, reporting an unresolved resolvent, not fixing it. + Worker(std::shared_ptr blocks, + Apply ce, + double eps_source, + double omega, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024, + Apply complement_preconditioner = {}); + Worker(const Worker&) = delete; + Worker& operator=(const Worker&) = delete; + + void apply_hamiltonian(const Vector& coefficients, Vector& output) const; + void apply_shifted(const Vector& coefficients, Vector& output) const; + void apply_schur(const Vector& x, Vector& output) const; + // Applies a physical/coarse-grid approximate inverse directly in the + // normalized Schur coordinates. The callback must not add complement + // metric factors; original-equation residuals remain authoritative. + void apply_normalized_preconditioner(const Vector& x, Vector& output) const; + Vector schur_rhs(const Vertices& g) const; + Vector reconstruct(const Vertices& g, const Vector& x) const; + + // Starts at zero and uses GMRES on T Q T p = T b, x=T p, where T is + // the exact inverse square root of the complement overlap. Residuals + // are still checked in the original untransformed equations. + // An optional physical preconditioner acts directly in these normalized + // coordinates. Nonconvergence is explicit; numerical breakdown, + // invalid/nonfinite data and singular LU raise exceptions. + SolveResult solve(const Vertices& g, + const SternheimerRPA::SolverOptions& options, + int restart_dimension = 50) const; + + // Conservative numerical-payload bound including LU, temporary vectors + // and GMRES/least-squares scratch. Excludes shared blocks, caller-owned + // inputs/outputs, callback storage/scratch, allocator/runtime overhead. + // This is not an RSS bound. Checked before LU and before GMRES allocation. + std::size_t workspace_bytes_required(int restart_dimension) const; + + private: + Vector solve_virtual(const Vector& rhs) const; + void apply_complement(const Vector& x, Vector& output) const; + void validate_vertices(const Vertices& g) const; + + std::shared_ptr blocks_; + Apply ce_; + Apply complement_preconditioner_; + Complex z_; + std::size_t max_workspace_bytes_; + Vector lu_; + std::vector pivots_; + }; + + private: + void validate_coefficients(const Vector& coefficients) const; + void apply_complement_metric(const Vector& x, Vector& output) const; + void apply_b(const Vector& x, Vector& output) const; + void apply_b_adjoint(const Vector& y, Vector& output) const; + + Data data_; + Vector inverse_sqrt_kernel_; + int nu_ = 0; + double minimum_metric_eigenvalue_ = 1.0; +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.cpp new file mode 100644 index 00000000000..552b16cb35f --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.cpp @@ -0,0 +1,555 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.h" + +#include "source_base/module_external/blas_connector.h" +#include "source_base/module_external/lapack_connector.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using H = ModuleRI::SternheimerFDHamiltonian; +using Operator = ModuleRI::SternheimerWeakGridOperator; +using Complex = H::Complex; + +std::size_t Add(std::size_t a, std::size_t b) +{ + if (b > std::numeric_limits::max() - a) + throw std::length_error("Sternheimer weak grid size overflow."); + return a + b; +} + +std::size_t Mul(std::size_t a, std::size_t b) +{ + if (a && b > std::numeric_limits::max() / a) + throw std::length_error("Sternheimer weak grid size overflow."); + return a * b; +} + +const H& RequireFine(const std::shared_ptr& fine) +{ + if (!fine) throw std::invalid_argument("Sternheimer weak grid requires a fine Hamiltonian."); + return *fine; +} + +void Validate(const H::Vector& input, std::size_t size) +{ + if (input.size() != size) throw std::invalid_argument("Sternheimer weak grid vector dimension mismatch."); + for (const auto& value : input) + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + throw std::invalid_argument("Sternheimer weak grid requires finite values."); +} + +void CheckOutput(const H::Vector& output) +{ + for (const auto& value : output) + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) + throw std::overflow_error("Sternheimer weak grid arithmetic overflow."); +} + +double VolumeElement(const H::Grid& grid) +{ + const auto cell = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + const auto& a = cell[0]; const auto& b = cell[1]; const auto& c = cell[2]; + return std::abs(a[0] * (b[1] * c[2] - b[2] * c[1]) + - a[1] * (b[0] * c[2] - b[2] * c[0]) + + a[2] * (b[0] * c[1] - b[1] * c[0])) / grid.size(); +} + +std::size_t CheckWorkspace(const H& fine, const H::Grid& coarse, std::size_t budget) +{ + const auto bytes = Operator::workspace_bytes_required(fine, coarse); + if (bytes > budget) throw std::length_error("Sternheimer weak grid workspace budget exceeded."); + const double fine_dv = VolumeElement(fine.grid()); + const double coarse_dv = VolumeElement(coarse); + if (!std::isfinite(fine_dv) || fine_dv <= 0.0 || !std::isfinite(coarse_dv) || coarse_dv <= 0.0) + throw std::invalid_argument("Sternheimer weak grid requires finite positive volume elements."); + if (!std::isfinite(fine.kinetic_prefactor())) + throw std::invalid_argument("Sternheimer weak grid requires a finite kinetic prefactor."); + for (double v : fine.local_potential()) + if (!std::isfinite(v)) throw std::invalid_argument("Sternheimer weak grid requires a finite potential."); + const auto* projector = fine.nonlocal_projector(); + if (projector) + { + const double projector_dv = projector->volume_element(); + if (!std::isfinite(projector_dv) || projector_dv <= 0.0 + || std::abs(projector_dv - fine_dv) > 1e-12 * std::max(projector_dv, fine_dv)) + throw std::invalid_argument("Sternheimer weak grid nonlocal projector has a different or invalid volume element."); + for (const auto& block : projector->blocks()) + { + for (const auto& beta : block.projectors) Validate(beta, fine.grid().size()); + double scale = 1.0; + for (const auto& row : block.d_matrix) + { + Validate(row, block.projectors.size()); + for (const auto value : row) scale = std::max(scale, std::abs(value)); + } + if (!std::isfinite(scale)) + throw std::invalid_argument("Sternheimer weak grid nonlocal D magnitude overflow."); + for (std::size_t i = 0; i < block.d_matrix.size(); ++i) + for (std::size_t j = 0; j < block.d_matrix.size(); ++j) + if (std::abs(block.d_matrix[i][j] / scale - std::conj(block.d_matrix[j][i]) / scale) > 1e-12) + throw std::invalid_argument("Sternheimer weak grid requires a Hermitian nonlocal D matrix."); + } + } + return bytes; +} + +H::Grid ProductGrid(const H::Grid& coarse, const H::Grid& fine) +{ + H::Grid product = fine; + const auto dimension = [](int nc, int nf) { + return static_cast(std::min(std::size_t(nf), Mul(2, std::size_t(nc)))); + }; + product.nx = dimension(coarse.nx, fine.nx); + product.ny = dimension(coarse.ny, fine.ny); + product.nz = dimension(coarse.nz, fine.nz); + product.hx = fine.hx * (double(fine.nx) / product.nx); + product.hy = fine.hy * (double(fine.ny) / product.ny); + product.hz = fine.hz * (double(fine.nz) / product.nz); + return product; +} + +struct CacheBytes +{ + std::size_t storage = 0; + std::size_t peak = 0; +}; + +CacheBytes ExactCacheBytes(const H& fine, const H::Grid& coarse, bool nonlocal, bool local) +{ + // Validate dimensions, cell and explicit Bloch label without allocating fields. + ModuleRI::SternheimerGridTransfer::workspace_bytes_required(coarse, fine.grid()); + CacheBytes result; + if (nonlocal && fine.nonlocal_projector()) + for (const auto& block : fine.nonlocal_projector()->blocks()) + { + const auto rank = block.projectors.size(); + if (rank > std::size_t(std::numeric_limits::max())) + throw std::length_error("Sternheimer exact projector cache exceeds BLAS integer range."); + const auto elements = Add(Add(Mul(coarse.size(), rank), Mul(rank, rank)), Mul(2, rank)); + result.storage = Add(result.storage, Mul(elements, sizeof(Complex))); + } + result.peak = result.storage; + if (local) + { + const auto product = ProductGrid(coarse, fine.grid()); + const auto np = std::size_t(product.size()); + // Potential construction uses fine/product FFT buffers plus its result. + result.peak = Add(result.storage, Mul(Add(fine.grid().size(), Mul(2, np)), sizeof(Complex))); + result.storage = Add(result.storage, + Add(ModuleRI::SternheimerGridTransfer::workspace_bytes_required(coarse, product), + Mul(Mul(2, np), sizeof(Complex)))); + result.peak = std::max(result.peak, result.storage); + } + return result; +} + +struct PotentialFFT +{ + fftw_complex* fine = nullptr; + fftw_complex* product = nullptr; + fftw_plan forward = nullptr; + fftw_plan backward = nullptr; + ~PotentialFFT() + { +#pragma omp critical(sternheimer_fftw_plan_management) + { + if (forward) fftw_destroy_plan(forward); + if (backward) fftw_destroy_plan(backward); + } + fftw_free(fine); + fftw_free(product); + } +}; + +H::Vector ProductPotential(const H& fine, const H::Grid& coarse, const H::Grid& product) +{ + const auto& fg = fine.grid(); + PotentialFFT fft; + fft.fine = fftw_alloc_complex(fg.size()); + fft.product = fftw_alloc_complex(product.size()); + if (!fft.fine || !fft.product) throw std::bad_alloc(); +#pragma omp critical(sternheimer_fftw_plan_management) + { + fft.forward = fftw_plan_dft_3d(fg.nx, fg.ny, fg.nz, fft.fine, fft.fine, FFTW_FORWARD, FFTW_ESTIMATE); + fft.backward = fftw_plan_dft_3d(product.nx, product.ny, product.nz, + fft.product, fft.product, FFTW_BACKWARD, FFTW_ESTIMATE); + } + if (!fft.forward || !fft.backward) + throw std::runtime_error("Sternheimer exact local cache failed to create FFTW plans."); + for (int i = 0; i < fg.size(); ++i) + { + fft.fine[i][0] = fine.local_potential()[i]; + fft.fine[i][1] = 0.0; + } + fftw_execute(fft.forward); + const std::array nc{{coarse.nx, coarse.ny, coarse.nz}}; + const std::array nf{{fg.nx, fg.ny, fg.nz}}; + const std::array np{{product.nx, product.ny, product.nz}}; + const double normalization = 1.0 / fg.size(); + for (int i = 0; i < product.size(); ++i) + { + const std::array index{{i / (product.ny * product.nz), (i / product.nz) % product.ny, i % product.nz}}; + std::array source{}; + bool retained = true; + for (int a = 0; a < 3; ++a) + { + const int mode = index[a] <= np[a] / 2 ? index[a] : index[a] - np[a]; + // Coarse modes use +Nyquist; their differences span [-(Nc-1),Nc-1]. + // On a 2*Nc product axis its unused Nyquist plane must be zero. + // On fine-limited axes use each original DFT bin once, including Nyquist. + retained = retained && std::abs(mode) <= nc[a] - 1; + source[a] = mode < 0 ? mode + nf[a] : mode; + } + const auto j = (std::size_t(source[0]) * fg.ny + source[1]) * fg.nz + source[2]; + fft.product[i][0] = retained ? normalization * fft.fine[j][0] : 0.0; + fft.product[i][1] = retained ? normalization * fft.fine[j][1] : 0.0; + } + fftw_execute(fft.backward); + H::Vector result(product.size()); + for (int i = 0; i < product.size(); ++i) + result[i] = Complex(fft.product[i][0], fft.product[i][1]); + CheckOutput(result); + return result; +} +} // namespace + +namespace ModuleRI +{ +struct SternheimerWeakGridOperator::ExactApplyCache +{ + struct NonlocalBlock + { + int rank = 0; + Vector b, d, overlaps, weighted; + }; + bool nonlocal = false; + bool local = false; + std::size_t storage_bytes = 0; + std::vector blocks; + std::unique_ptr local_transfer; + Vector local_potential, product_work; + + void add_nonlocal(const Vector& x, Vector& result) + { + const int nc = static_cast(x.size()); + for (auto& block : blocks) + { + BlasConnector::gemv('C', nc, block.rank, Complex(1.0), block.b.data(), nc, + x.data(), 1, Complex(0.0), block.overlaps.data(), 1); + BlasConnector::gemv('N', block.rank, block.rank, Complex(1.0), block.d.data(), block.rank, + block.overlaps.data(), 1, Complex(0.0), block.weighted.data(), 1); + BlasConnector::gemv('N', nc, block.rank, Complex(1.0), block.b.data(), nc, + block.weighted.data(), 1, Complex(1.0), result.data(), 1); + } + } +}; + +double orthonormalize_sternheimer_weak_states_in_place( + std::vector& states, const double volume_element, + const double min_metric, const std::size_t max_workspace_bytes) +{ + if (states.empty() || states.size() > static_cast(std::numeric_limits::max() / 4) + || !std::isfinite(volume_element) || volume_element <= 0.0 + || !std::isfinite(min_metric) || min_metric <= 0.0 || min_metric >= 1.0) + throw std::invalid_argument("Sternheimer weak normalization has invalid dimensions, volume or metric threshold."); + const std::size_t grid_size = states.front().values.size(); + if (grid_size == 0) + throw std::invalid_argument("Sternheimer weak normalization requires nonempty fields."); + for (const auto& state : states) + { + Validate(state.values, grid_size); + for (const auto& gradient : state.gradients) Validate(gradient, grid_size); + } + if (states.size() > grid_size) + throw std::domain_error("Sternheimer weak normalization metric is singular: more states than grid points."); + + const int n = static_cast(states.size()); + const std::size_t square = Mul(states.size(), states.size()); + const int lwork = std::max(1, 2 * n - 1); + const int lrwork = std::max(1, 3 * n - 2); + const std::size_t fixed_bytes = Add(Mul(Add(Mul(2, square), lwork), sizeof(Complex)), + Mul(Add(states.size(), lrwork), sizeof(double))); + const std::size_t bytes_per_row = Mul(states.size(), sizeof(Complex)); + if (Add(fixed_bytes, bytes_per_row) > max_workspace_bytes) + throw std::length_error("Sternheimer weak normalization workspace budget exceeded."); + const std::size_t tile_rows = std::min({grid_size, std::size_t(4096), + (max_workspace_bytes - fixed_bytes) / bytes_per_row}); + H::Vector gram(square, Complex(0.0)), eigen_matrix(square), work(lwork); + H::Vector tile(Mul(tile_rows, states.size())); + std::vector eigenvalues(n), rwork(lrwork); + + const auto assemble_gram = [&](H::Vector& output) { + std::fill(output.begin(), output.end(), Complex(0.0)); + for (std::size_t begin = 0; begin < grid_size; begin += tile_rows) + { + const int rows = static_cast(std::min(tile_rows, grid_size - begin)); + for (std::size_t j = 0; j < states.size(); ++j) + std::copy_n(states[j].values.data() + begin, rows, + tile.data() + static_cast(rows) * j); + BlasConnector::gemm_cm('C', 'N', n, n, rows, Complex(volume_element), tile.data(), rows, + tile.data(), rows, Complex(1.0), output.data(), n); + } + CheckOutput(output); + }; + assemble_gram(gram); + eigen_matrix = gram; + const char jobz = 'N', upper = 'U'; + int info = 0; + zheev_(&jobz, &upper, &n, eigen_matrix.data(), &n, eigenvalues.data(), work.data(), &lwork, + rwork.data(), &info); + if (info != 0) + throw std::runtime_error("Sternheimer weak normalization metric eigensolver failed."); + for (const double value : eigenvalues) + if (!std::isfinite(value)) + throw std::overflow_error("Sternheimer weak normalization metric spectrum is nonfinite."); + const double threshold = min_metric * std::max(1.0, eigenvalues.back()); + if (eigenvalues.front() <= threshold) + { + std::ostringstream message; + message.precision(17); + message << "Sternheimer weak normalization metric is singular or ill-conditioned: min=" + << eigenvalues.front() << ", max=" << eigenvalues.back() << ", threshold=" << threshold + << "; no states dropped or shifts applied."; + throw std::domain_error(message.str()); + } + zpotrf_(&upper, &n, gram.data(), &n, &info); + if (info > 0) + throw std::domain_error("Sternheimer weak normalization metric is not positive definite."); + if (info < 0) + throw std::runtime_error("Sternheimer weak normalization Cholesky factorization failed."); + CheckOutput(gram); + + // Every tile contains all states before any are overwritten. Right-solving + // with upper R mixes column j only with columns <=j, preserving each prefix. + const char right = 'R', no_transpose = 'N', nonunit = 'N'; + const Complex one(1.0); + for (std::size_t begin = 0; begin < grid_size; begin += tile_rows) + { + const int rows = static_cast(std::min(tile_rows, grid_size - begin)); + for (int component = -1; component < 3; ++component) + { + for (std::size_t j = 0; j < states.size(); ++j) + { + const auto& field = component < 0 ? states[j].values : states[j].gradients[component]; + std::copy_n(field.data() + begin, rows, tile.data() + static_cast(rows) * j); + } + ztrsm_(&right, &upper, &no_transpose, &nonunit, &rows, &n, &one, + gram.data(), &n, tile.data(), &rows); + CheckOutput(tile); + for (std::size_t j = 0; j < states.size(); ++j) + { + auto& field = component < 0 ? states[j].values : states[j].gradients[component]; + std::copy_n(tile.data() + static_cast(rows) * j, rows, field.data() + begin); + } + } + } + assemble_gram(eigen_matrix); + double maximum_error = 0.0; + for (std::size_t j = 0; j < states.size(); ++j) + for (std::size_t i = 0; i < states.size(); ++i) + maximum_error = std::max(maximum_error, + std::abs(eigen_matrix[i + states.size() * j] - Complex(i == j ? 1.0 : 0.0))); + if (!std::isfinite(maximum_error)) + throw std::overflow_error("Sternheimer weak normalization orthogonality diagnostic overflow."); + return maximum_error; +} + +SternheimerWeakGridOperator::SternheimerWeakGridOperator(std::shared_ptr fine, + const Grid& coarse, std::size_t budget) + : fine_(std::move(fine)), workspace_bytes_(CheckWorkspace(RequireFine(fine_), coarse, budget)), + transfer_(coarse, fine_->grid()), coarse_dv_(VolumeElement(coarse)), + fine_dv_(VolumeElement(fine_->grid())), fine_input_(fine_->grid().size()), + fine_output_(fine_->grid().size()), coarse_temporary_(coarse.size()), coarse_result_(coarse.size()) +{} + +SternheimerWeakGridOperator::~SternheimerWeakGridOperator() = default; + +const SternheimerWeakGridOperator::Grid& SternheimerWeakGridOperator::grid() const { return transfer_.coarse_grid(); } +const SternheimerWeakGridOperator::Hamiltonian& SternheimerWeakGridOperator::fine_hamiltonian() const { return *fine_; } +double SternheimerWeakGridOperator::coarse_volume_element() const { return coarse_dv_; } +double SternheimerWeakGridOperator::fine_volume_element() const { return fine_dv_; } + +bool SternheimerWeakGridOperator::has_exact_nonlocal_cache() const { return exact_cache_ && exact_cache_->nonlocal; } +bool SternheimerWeakGridOperator::has_exact_local_cache() const { return exact_cache_ && exact_cache_->local; } +std::size_t SternheimerWeakGridOperator::exact_cache_storage_bytes() const +{ + return exact_cache_ ? exact_cache_->storage_bytes : 0; +} + +std::size_t SternheimerWeakGridOperator::exact_cache_workspace_bytes_required( + const Hamiltonian& fine, const Grid& coarse, bool nonlocal, bool local) +{ + return ExactCacheBytes(fine, coarse, nonlocal, local).peak; +} + +void SternheimerWeakGridOperator::enable_exact_apply_cache(bool nonlocal, bool local, std::size_t budget) +{ + if (!nonlocal && !local) + { + exact_cache_.reset(); + return; + } + const auto bytes = ExactCacheBytes(*fine_, grid(), nonlocal, local); + if (Add(exact_cache_storage_bytes(), bytes.peak) > budget) + throw std::length_error("Sternheimer exact apply cache workspace budget exceeded."); + auto cache = std::make_unique(); + cache->nonlocal = nonlocal; + cache->local = local; + cache->storage_bytes = bytes.storage; + if (nonlocal && fine_->nonlocal_projector()) + { + const auto& blocks = fine_->nonlocal_projector()->blocks(); + cache->blocks.reserve(blocks.size()); + for (const auto& input : blocks) + { + ExactApplyCache::NonlocalBlock block; + block.rank = static_cast(input.projectors.size()); + block.b.resize(Mul(grid().size(), block.rank)); + block.d.resize(Mul(block.rank, block.rank)); + block.overlaps.resize(block.rank); + block.weighted.resize(block.rank); + for (int j = 0; j < block.rank; ++j) + { + // project(beta) = sqrt(dVc) R beta = E^dagger Wf beta. + project(input.projectors[j], coarse_temporary_); + std::copy(coarse_temporary_.begin(), coarse_temporary_.end(), + block.b.begin() + std::size_t(j) * grid().size()); + for (int i = 0; i < block.rank; ++i) + block.d[i + std::size_t(block.rank) * j] = input.d_matrix[i][j]; + } + cache->blocks.push_back(std::move(block)); + } + } + if (local) + { + const auto product = ProductGrid(grid(), fine_->grid()); + cache->local_potential = ProductPotential(*fine_, grid(), product); + cache->local_transfer = std::make_unique(grid(), product); + cache->product_work.resize(product.size()); + } + exact_cache_ = std::move(cache); +} + +std::size_t SternheimerWeakGridOperator::workspace_bytes_required(const Hamiltonian& fine, const Grid& coarse) +{ + const auto transfer = SternheimerGridTransfer::gradient_workspace_bytes_required(coarse, fine.grid()); + std::size_t projectors = 0; + if (fine.nonlocal_projector()) + for (const auto& block : fine.nonlocal_projector()->blocks()) + projectors = std::max(projectors, block.projectors.size()); + const auto vectors = Mul(2, Add(Add(fine.grid().size(), coarse.size()), projectors)); + return Add(transfer, Mul(vectors, sizeof(Complex))); +} + +void SternheimerWeakGridOperator::apply(const Vector& coefficients, Vector& result) +{ + Validate(coefficients, grid().size()); + transfer_.apply_negative_laplacian(coefficients, coarse_result_, workspace_bytes_); + for (auto& value : coarse_result_) value *= fine_->kinetic_prefactor(); + const bool local_cached = has_exact_local_cache(), nonlocal_cached = has_exact_nonlocal_cache(); + if (!local_cached || !nonlocal_cached) transfer_.interpolate(coefficients, fine_input_); + if (local_cached) + { + auto& cache = *exact_cache_; + cache.local_transfer->interpolate(coefficients, cache.product_work); + for (std::size_t i = 0; i < cache.product_work.size(); ++i) + cache.product_work[i] *= cache.local_potential[i]; + cache.local_transfer->restrict_adjoint(cache.product_work, coarse_temporary_); + } + else + { + fine_->apply_local_potential(fine_input_, fine_output_); + transfer_.restrict_adjoint(fine_output_, coarse_temporary_); + } + for (std::size_t i = 0; i < coarse_result_.size(); ++i) coarse_result_[i] += coarse_temporary_[i]; + if (nonlocal_cached) exact_cache_->add_nonlocal(coefficients, coarse_result_); + else + { + fine_->apply_nonlocal(fine_input_, fine_output_); + transfer_.restrict_adjoint(fine_output_, coarse_temporary_); + for (std::size_t i = 0; i < coarse_result_.size(); ++i) coarse_result_[i] += coarse_temporary_[i]; + } + CheckOutput(coarse_result_); + result = coarse_result_; +} + +void SternheimerWeakGridOperator::lift(const Vector& coefficients, Vector& fine_values) +{ + Validate(coefficients, grid().size()); + transfer_.interpolate(coefficients, fine_values); + for (auto& value : fine_values) value /= std::sqrt(coarse_dv_); + CheckOutput(fine_values); +} + +void SternheimerWeakGridOperator::project(const Vector& fine_values, Vector& coefficients) +{ + Validate(fine_values, fine_->grid().size()); + transfer_.restrict_adjoint(fine_values, coefficients); + for (auto& value : coefficients) value *= std::sqrt(coarse_dv_); + CheckOutput(coefficients); +} + +SternheimerWeakGridBlocks SternheimerWeakGridOperator::assemble_blocks(const std::vector& states, + double metric_tolerance, + std::size_t matrix_budget) +{ + if (states.empty() || !(metric_tolerance > 0.0) || !std::isfinite(metric_tolerance)) + throw std::invalid_argument("Sternheimer weak grid requires states and a finite positive metric tolerance."); + const auto n = states.size(), nc = std::size_t(grid().size()); + // Five square component matrices plus two rectangular retained couplings. + const auto count = Add(Mul(5, Mul(n, n)), Mul(2, Mul(n, nc))); + if (Mul(count, sizeof(Complex)) > matrix_budget) + throw std::length_error("Sternheimer weak grid matrix budget exceeded."); + for (const auto& state : states) + { + Validate(state.values, fine_->grid().size()); + for (const auto& gradient : state.gradients) Validate(gradient, state.values.size()); + } + auto matrices = assemble_delta_sternheimer_grid_matrices_fast(*fine_, states, fine_dv_); + SternheimerWeakGridBlocks blocks; + blocks.state_count = n; + blocks.coarse_count = nc; + for (std::size_t j = 0; j < n; ++j) + for (std::size_t i = 0; i < n; ++i) + { + const auto error = std::abs(matrices.overlap[i + n * j] - Complex(i == j ? 1.0 : 0.0)); + if (!std::isfinite(error) || error > metric_tolerance) + throw std::invalid_argument("Sternheimer weak grid states are not fine-metric orthonormal."); + blocks.maximum_metric_error = std::max(blocks.maximum_metric_error, error); + } + blocks.state_hamiltonian = std::move(matrices.hamiltonian); + CheckOutput(blocks.state_hamiltonian); + blocks.state_coarse_overlap.resize(n * nc); + blocks.state_coarse_hamiltonian.resize(n * nc); + const double normalization = std::sqrt(coarse_dv_); + for (std::size_t i = 0; i < n; ++i) + { + transfer_.restrict_adjoint(states[i].values, coarse_temporary_); + for (std::size_t j = 0; j < nc; ++j) + blocks.state_coarse_overlap[i + n * j] = normalization * std::conj(coarse_temporary_[j]); + // The derivative adjoint is essential: FFT differentiation of the NAO + // values would discard the supplied analytic-gradient weak form. + transfer_.restrict_gradient_adjoint(states[i].gradients, coarse_result_, workspace_bytes_); + for (auto& value : coarse_result_) value *= fine_->kinetic_prefactor(); + fine_->apply_local_potential(states[i].values, fine_output_); + transfer_.restrict_adjoint(fine_output_, coarse_temporary_); + for (std::size_t j = 0; j < nc; ++j) coarse_result_[j] += coarse_temporary_[j]; + fine_->apply_nonlocal(states[i].values, fine_output_); + transfer_.restrict_adjoint(fine_output_, coarse_temporary_); + for (std::size_t j = 0; j < nc; ++j) + blocks.state_coarse_hamiltonian[i + n * j] = normalization * std::conj(coarse_result_[j] + coarse_temporary_[j]); + } + CheckOutput(blocks.state_coarse_overlap); + CheckOutput(blocks.state_coarse_hamiltonian); + return blocks; +} +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.h new file mode 100644 index 00000000000..339ce020db1 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.h @@ -0,0 +1,111 @@ +#ifndef STERNHEIMER_WEAK_GRID_H +#define STERNHEIMER_WEAK_GRID_H + +#define ABACUS_STERNHEIMER_WEAK_EXACT_APPLY_CACHE 1 + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_delta.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h" + +#include + +namespace ModuleRI +{ + +// Prefix-preserving normalization U <- U R^-1, where S=dV U*U=R*R and R +// is upper triangular. Applies the SAME transform to values and all three +// supplied analytic gradients; no state dropping, shifts or full fine-U copy. +// Rejects lambda_min(S) <= min_metric*max(1,lambda_max(S)). The budget covers +// two nstate-square matrices, eigensolver work and one all-state grid tile; +// excludes caller fields, allocator and opaque BLAS/LAPACK runtime storage. +// Invalid inputs, metric or insufficient budget leave states untouched. +// Arithmetic overflow during the in-place transform may leave partial changes. +// Returns the recomputed max absolute entry of dV U*U-I; callers set their +// own acceptance gate on this diagnostic. Each original prefix span is retained. +double orthonormalize_sternheimer_weak_states_in_place( + std::vector& states, + double volume_element, + double min_metric = 1e-12, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024); + +struct SternheimerWeakGridBlocks +{ + std::size_t state_count = 0; + std::size_t coarse_count = 0; + // Column-major H_U=h(U,U), L=, K=h(U,E). U must be fine-metric + // orthonormal, including identical transformations of analytic gradients. + SternheimerFDHamiltonian::Vector state_hamiltonian; + SternheimerFDHamiltonian::Vector state_coarse_overlap; + SternheimerFDHamiltonian::Vector state_coarse_hamiltonian; + double maximum_metric_error = 0.0; +}; + +// Fine-integrated weak form, not the FullGrid-FD8 Galerkin reference. +// E=J/sqrt(dVc) has Euclidean-normalized coefficients: E* Wf E=I. +// Kinetic uses analytic i(G+k), while local/nonlocal terms retain fine H data. +// Each concurrent worker needs its own instance and immutable shared fine H. +class SternheimerWeakGridOperator +{ + public: + using Hamiltonian = SternheimerFDHamiltonian; + using Grid = Hamiltonian::Grid; + using Vector = Hamiltonian::Vector; + using Function = SternheimerDeltaGridFunction; + + SternheimerWeakGridOperator(std::shared_ptr fine_hamiltonian, + const Grid& coarse_grid, + std::size_t max_workspace_bytes = 512ULL * 1024 * 1024); + ~SternheimerWeakGridOperator(); + SternheimerWeakGridOperator(const SternheimerWeakGridOperator&) = delete; + SternheimerWeakGridOperator& operator=(const SternheimerWeakGridOperator&) = delete; + + const Grid& grid() const; + const Hamiltonian& fine_hamiltonian() const; + double coarse_volume_element() const; + double fine_volume_element() const; + + // No dense coarse matrix or explicit E is allocated. Input/output may alias. + void apply(const Vector& coefficients, Vector& result); + void lift(const Vector& coefficients, Vector& fine_values); + void project(const Vector& fine_values, Vector& coefficients); + + // Explicit opt-in, normally after assemble_blocks. Neither cache changes + // fine blocks, lift/project or analytic kinetic energy. false/false disables. + // NL retains every projector and D entry. Local retains exactly the fine + // discrete-quadrature difference frequencies on min(Nf,2*Nc) per axis. + // Budget bounds EXTRA numerical payload at construction peak, including + // any old cache until replacement succeeds; excludes the existing operator, + // shared H, allocator/object overhead and opaque FFTW/BLAS allocations. + // A failed enable leaves the old cache active. Concurrent calls need separate + // operator instances; the cache owns mutable FFT and application scratch. + void enable_exact_apply_cache(bool cache_nonlocal, bool cache_local, + std::size_t max_extra_workspace_bytes = 512ULL * 1024 * 1024); + bool has_exact_nonlocal_cache() const; + bool has_exact_local_cache() const; + std::size_t exact_cache_storage_bytes() const; + // Fresh-cache peak, not RSS. For replacement add exact_cache_storage_bytes(). + static std::size_t exact_cache_workspace_bytes_required(const Hamiltonian& fine, const Grid& coarse, + bool cache_nonlocal, bool cache_local); + + SternheimerWeakGridBlocks assemble_blocks(const std::vector& states, + double metric_tolerance = 1e-8, + std::size_t max_matrix_bytes = 512ULL * 1024 * 1024); + + // Owned numerical payload plus peak scalar nonlocal coefficients. Excludes + // H data, caller states/results, FFTW opaque plans, BLAS/runtime allocations. + // Block assembly additionally uses the blocked weak-integral routine's + // temporary tiles; max_matrix_bytes bounds its dense matrix payload only. + static std::size_t workspace_bytes_required(const Hamiltonian& fine, const Grid& coarse); + + private: + std::shared_ptr fine_; + std::size_t workspace_bytes_; + SternheimerGridTransfer transfer_; + double coarse_dv_; + double fine_dv_; + Vector fine_input_, fine_output_, coarse_temporary_, coarse_result_; + struct ExactApplyCache; + std::unique_ptr exact_cache_; +}; + +} // namespace ModuleRI +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_matrix_audit.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_matrix_audit.h new file mode 100644 index 00000000000..11cab4a4844 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_matrix_audit.h @@ -0,0 +1,72 @@ +#ifndef STERNHEIMER_WEAK_MATRIX_AUDIT_H +#define STERNHEIMER_WEAK_MATRIX_AUDIT_H + +#include +#include +#include +#include + +namespace ModuleRI +{ + +// Pure ownership policy. No environment access, sampling, or MPI communication. +class SternheimerWeakAuditShard +{ + public: + const int index; + const int count; + + static SternheimerWeakAuditShard parse(const char* raw_index, const char* raw_count, const int channels) + { + const int index = parse_integer(raw_index, 0, "WEAK_AUDIT_SHARD_INDEX"); + const int count = parse_integer(raw_count, 1, "WEAK_AUDIT_SHARD_COUNT"); + if (channels <= 0 || count <= 0 || count > channels || index >= count) + throw std::invalid_argument("Weak matrix audit requires 0 <= shard index < count <= channels."); + return SternheimerWeakAuditShard(index, count, channels); + } + + bool owns(const int column) const + { + return column >= 0 && column < channels_ && column % count == index; + } + + std::vector columns() const + { + std::vector result; + result.reserve((channels_ - 1 - index) / count + 1); + // Widen the increment so even an INT_MAX-sized space cannot wrap. + for (long long column = index; column < channels_; column += count) + result.push_back(static_cast(column)); + return result; + } + + private: + const int channels_; + + SternheimerWeakAuditShard(const int index, const int count, const int channels) + : index(index), count(count), channels_(channels) + { + } + + static int parse_integer(const char* raw, const int fallback, const char* name) + { + if (raw == nullptr) return fallback; + const auto invalid = [name]() { + return std::invalid_argument(std::string(name) + " must be a nonempty unsigned decimal integer."); + }; + if (*raw == '\0') throw invalid(); + int value = 0; + for (const char* p = raw; *p != '\0'; ++p) + { + if (*p < '0' || *p > '9') throw invalid(); + const int digit = *p - '0'; + if (value > (std::numeric_limits::max() - digit) / 10) throw invalid(); + value = value * 10 + digit; + } + return value; + } +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.cpp new file mode 100644 index 00000000000..d1e0a3b1940 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.cpp @@ -0,0 +1,340 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Preconditioner = ModuleRI::SternheimerWeakSpectralPreconditioner; +using Grid = Preconditioner::Grid; +using Vector = Preconditioner::Vector; +using Complex = Preconditioner::Complex; +using Lattice = ModuleRI::SternheimerFDLatticeVectors; + +std::size_t grid_count(const Grid& grid) +{ + if (!grid.periodic) + { + throw std::invalid_argument( + "Weak spectral preconditioner requires a periodic grid."); + } + std::size_t count = 1; + for (const int dimension : {grid.nx, grid.ny, grid.nz}) + { + if (dimension <= 0 + || count > static_cast(std::numeric_limits::max()) + / static_cast(dimension)) + { + throw std::invalid_argument( + "Weak spectral preconditioner has invalid grid dimensions."); + } + count *= static_cast(dimension); + } + for (const double spacing : {grid.hx, grid.hy, grid.hz}) + { + if (!std::isfinite(spacing) || spacing <= 0.0) + { + throw std::invalid_argument( + "Weak spectral preconditioner requires positive grid spacings."); + } + } + for (const double component : grid.kpoint) + { + if (!std::isfinite(component)) + { + throw std::invalid_argument( + "Weak spectral preconditioner requires a finite Bloch point."); + } + } + const Lattice lattice = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + for (const auto& vector : lattice) + { + for (const double component : vector) + { + if (!std::isfinite(component)) + { + throw std::invalid_argument( + "Weak spectral preconditioner requires a finite lattice."); + } + } + } + const double determinant + = lattice[0][0] * (lattice[1][1] * lattice[2][2] + - lattice[1][2] * lattice[2][1]) + - lattice[0][1] * (lattice[1][0] * lattice[2][2] + - lattice[1][2] * lattice[2][0]) + + lattice[0][2] * (lattice[1][0] * lattice[2][1] + - lattice[1][1] * lattice[2][0]); + if (!std::isfinite(determinant) || determinant == 0.0) + { + throw std::invalid_argument( + "Weak spectral preconditioner requires a nonsingular lattice."); + } + return count; +} + +std::size_t grid_index(const Grid& grid, const int ix, const int iy, const int iz) +{ + return (static_cast(ix) * grid.ny + iy) * grid.nz + iz; +} + +Lattice reciprocal_vectors(const Grid& grid) +{ + Lattice reciprocal = ModuleRI::sternheimer_fd_grid_dual_vectors(grid); + const double two_pi = 2.0 * std::acos(-1.0); + for (auto& vector : reciprocal) + { + for (double& component : vector) + { + component *= two_pi; + } + } + return reciprocal; +} + +std::array wavevector(const Grid& grid, + const Lattice& reciprocal, + const int ix, + const int iy, + const int iz) +{ + const std::array dimensions{grid.nx, grid.ny, grid.nz}; + const std::array indices{ix, iy, iz}; + std::array result{}; + for (int direction = 0; direction != 3; ++direction) + { + const int frequency = indices[direction] <= dimensions[direction] / 2 + ? indices[direction] + : indices[direction] - dimensions[direction]; + const double shifted = frequency + grid.kpoint[direction]; + for (int axis = 0; axis != 3; ++axis) + { + result[axis] += shifted * reciprocal[direction][axis]; + } + } + return result; +} + +Vector bloch_phases(const Grid& grid, const std::size_t count) +{ + Vector phases(count); + const double two_pi = 2.0 * std::acos(-1.0); + const std::array k{ + std::remainder(grid.kpoint[0], static_cast(grid.nx)), + std::remainder(grid.kpoint[1], static_cast(grid.ny)), + std::remainder(grid.kpoint[2], static_cast(grid.nz))}; + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const double phase + = two_pi + * (k[0] * static_cast(ix) / grid.nx + + k[1] * static_cast(iy) / grid.ny + + k[2] * static_cast(iz) / grid.nz); + phases[grid_index(grid, ix, iy, iz)] + = std::exp(Complex(0.0, phase)); + } + } + } + return phases; +} + +bool finite(const Complex value) +{ + return std::isfinite(value.real()) && std::isfinite(value.imag()); +} + +void validate_input(const Vector& input, const std::size_t count) +{ + if (input.size() != count) + { + throw std::invalid_argument( + "Weak spectral preconditioner input size does not match its grid."); + } + for (const Complex value : input) + { + if (!finite(value)) + { + throw std::invalid_argument( + "Weak spectral preconditioner requires finite input values."); + } + } +} + +} // namespace + +namespace ModuleRI +{ + +struct SternheimerWeakSpectralPreconditioner::Impl +{ + Impl(const Grid& input_grid, + const double kinetic_prefactor, + const double reference_eigenvalue, + const double omega, + const double regularization) + : grid(input_grid), count(grid_count(grid)), phases(bloch_phases(grid, count)), + inverse_denominator(count) + { + if (!std::isfinite(kinetic_prefactor) || kinetic_prefactor <= 0.0 + || !std::isfinite(reference_eigenvalue) || !std::isfinite(omega) + || !std::isfinite(regularization) || regularization < 0.0) + { + throw std::invalid_argument( + "Weak spectral preconditioner has an invalid kinetic shift."); + } + buffer = fftw_alloc_complex(count); + if (buffer == nullptr) + { + throw std::bad_alloc(); + } +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + forward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, buffer, buffer, + FFTW_FORWARD, FFTW_ESTIMATE); + backward = fftw_plan_dft_3d(grid.nx, grid.ny, grid.nz, buffer, + buffer, FFTW_BACKWARD, FFTW_ESTIMATE); + } + if (forward == nullptr || backward == nullptr) + { + release(); + throw std::runtime_error( + "Failed to initialize weak spectral preconditioner FFT plans."); + } + + const Lattice reciprocal = reciprocal_vectors(grid); + const double normalization = 1.0 / static_cast(count); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const auto q = wavevector(grid, reciprocal, ix, iy, iz); + const double q2 = q[0] * q[0] + q[1] * q[1] + + q[2] * q[2]; + const Complex denominator( + kinetic_prefactor * q2 - reference_eigenvalue + + regularization, + omega); + const double magnitude = std::abs(denominator); + if (!std::isfinite(magnitude) + || magnitude <= std::numeric_limits::min()) + { + release(); + throw std::domain_error( + "Weak spectral preconditioner found a singular Fourier mode."); + } + const Complex value = normalization / denominator; + if (!finite(value)) + { + release(); + throw std::overflow_error( + "Weak spectral preconditioner inverse symbol overflowed."); + } + inverse_denominator[grid_index(grid, ix, iy, iz)] = value; + } + } + } + } + + ~Impl() + { + release(); + } + + void release() + { +#ifdef _OPENMP +#pragma omp critical(sternheimer_fftw_plan_management) +#endif + { + if (forward != nullptr) + { + fftw_destroy_plan(forward); + forward = nullptr; + } + if (backward != nullptr) + { + fftw_destroy_plan(backward); + backward = nullptr; + } + } + fftw_free(buffer); + buffer = nullptr; + } + + void apply(const Vector& input, Vector& output) const + { + validate_input(input, count); + for (std::size_t i = 0; i != count; ++i) + { + const Complex periodic = std::conj(phases[i]) * input[i]; + buffer[i][0] = periodic.real(); + buffer[i][1] = periodic.imag(); + } + fftw_execute(forward); + for (std::size_t i = 0; i != count; ++i) + { + const Complex coefficient(buffer[i][0], buffer[i][1]); + const Complex value = inverse_denominator[i] * coefficient; + buffer[i][0] = value.real(); + buffer[i][1] = value.imag(); + } + fftw_execute(backward); + Vector result(count); + for (std::size_t i = 0; i != count; ++i) + { + result[i] = phases[i] * Complex(buffer[i][0], buffer[i][1]); + if (!finite(result[i])) + { + throw std::overflow_error( + "Weak spectral preconditioner produced a nonfinite result."); + } + } + output = std::move(result); + } + + Grid grid; + std::size_t count = 0; + Vector phases; + Vector inverse_denominator; + mutable fftw_complex* buffer = nullptr; + fftw_plan forward = nullptr; + fftw_plan backward = nullptr; +}; + +SternheimerWeakSpectralPreconditioner::SternheimerWeakSpectralPreconditioner( + const Grid& grid, + const double kinetic_prefactor, + const double reference_eigenvalue, + const double omega, + const double regularization) + : impl_(new Impl(grid, kinetic_prefactor, reference_eigenvalue, omega, + regularization)) +{} + +SternheimerWeakSpectralPreconditioner::~SternheimerWeakSpectralPreconditioner() + = default; + +void SternheimerWeakSpectralPreconditioner::apply(const Vector& input, + Vector& output) const +{ + impl_->apply(input, output); +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.h new file mode 100644 index 00000000000..ce10d1474c2 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.h @@ -0,0 +1,43 @@ +#ifndef STERNHEIMER_WEAK_PRECONDITIONER_H +#define STERNHEIMER_WEAK_PRECONDITIONER_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include + +namespace ModuleRI +{ + +// Analytic coarse-grid inverse kinetic shift for the weak-form complement. +// The symbol is alpha*|G+k|^2-epsilon+i*omega+regularization; it is not the +// finite-difference stencil symbol. Each worker must own one instance because +// FFT execution mutates private scratch. Input and output may alias. +class SternheimerWeakSpectralPreconditioner +{ + public: + using Complex = SternheimerFDHamiltonian::Complex; + using Vector = SternheimerFDHamiltonian::Vector; + using Grid = SternheimerFDHamiltonian::Grid; + + SternheimerWeakSpectralPreconditioner(const Grid& grid, + double kinetic_prefactor, + double reference_eigenvalue, + double omega, + double regularization = 0.0); + ~SternheimerWeakSpectralPreconditioner(); + + SternheimerWeakSpectralPreconditioner( + const SternheimerWeakSpectralPreconditioner&) = delete; + SternheimerWeakSpectralPreconditioner& operator=( + const SternheimerWeakSpectralPreconditioner&) = delete; + + void apply(const Vector& input, Vector& output) const; + + private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_dispatch.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_dispatch.h new file mode 100644 index 00000000000..9965f0734a2 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_dispatch.h @@ -0,0 +1,186 @@ +#ifndef STERNHEIMER_WEAK_Q_DISPATCH_H +#define STERNHEIMER_WEAK_Q_DISPATCH_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif +#ifdef __MKL +#include +#endif + +namespace ModuleRI +{ + +struct SternheimerWeakQLayout +{ + int workers; + int inner_threads; + int allocated_threads; + + void validate() const + { + if (workers <= 0 || inner_threads <= 0 || allocated_threads <= 0 + || workers > allocated_threads / inner_threads) + throw std::invalid_argument("Weak q dispatch requires positive workers/inner/allocation and workers*inner <= allocation."); + } +}; + +namespace sternheimer_weak_q_detail +{ +#ifdef _OPENMP +class DynamicThreadsGuard +{ + public: + DynamicThreadsGuard() : previous_dynamic_(omp_get_dynamic()) { omp_set_dynamic(0); } + ~DynamicThreadsGuard() { omp_set_dynamic(previous_dynamic_); } + DynamicThreadsGuard(const DynamicThreadsGuard&) = delete; + DynamicThreadsGuard& operator=(const DynamicThreadsGuard&) = delete; + + private: + int previous_dynamic_; +}; +#endif + +// Settings belong to the calling OpenMP task / MKL thread, never the process. +class InnerThreadsGuard +{ + public: + explicit InnerThreadsGuard(int threads) + { +#ifdef _OPENMP + previous_threads_ = omp_get_max_threads(); + previous_levels_ = omp_get_max_active_levels(); + omp_set_num_threads(threads); + const int inner_level = omp_get_active_level() + 1; + if (threads > 1 && previous_levels_ < inner_level) + omp_set_max_active_levels(inner_level); +#endif +#ifdef __MKL + previous_mkl_ = mkl_set_num_threads_local(threads); +#endif + (void)threads; + } + + ~InnerThreadsGuard() + { +#ifdef __MKL + mkl_set_num_threads_local(previous_mkl_); +#endif +#ifdef _OPENMP + omp_set_max_active_levels(previous_levels_); + omp_set_num_threads(previous_threads_); +#endif + } + + InnerThreadsGuard(const InnerThreadsGuard&) = delete; + InnerThreadsGuard& operator=(const InnerThreadsGuard&) = delete; + + private: +#ifdef _OPENMP + int previous_threads_; + int previous_levels_; +#endif +#ifdef __MKL + int previous_mkl_; +#endif +}; +} // namespace sternheimer_weak_q_detail + +// factory(index) returns a worker value (including move-only handles); callback +// receives (zero-based task, worker&, index). Both callables must support concurrent +// invocation; mutable solve state belongs in the worker, not shared captures. +// Every task is attempted once, even after callback failure. Factory failure starts +// no tasks. Like run_sternheimer_channel_tasks, exceptions leave only after the team. +// Counts have layout.workers entries. A runtime-limited team is rejected before +// any factory or callback runs; an empty task range creates no team or workers. +// Dynamic team adjustment is disabled for the dispatch and restored on exit. +template +std::vector run_sternheimer_weak_q_tasks(const std::int64_t task_count, + const SternheimerWeakQLayout& layout, + Factory&& factory, + Callback&& callback) +{ + layout.validate(); + if (task_count < 0 + || static_cast(task_count) > std::numeric_limits::max()) + throw std::invalid_argument("Weak q dispatch task count must be non-negative and fit size_t."); +#ifndef _OPENMP + if (layout.workers != 1) + throw std::invalid_argument("Weak q dispatch with multiple workers requires OpenMP."); +#endif + std::vector counts(static_cast(layout.workers), 0); + if (task_count == 0) return counts; + + using FactoryResult = decltype(factory(0)); + static_assert(!std::is_reference::value, "Weak q factory must return a worker value, not a reference."); + using Worker = typename std::decay::type; + std::vector errors(static_cast(layout.workers)); + std::atomic factory_failed{false}; + +#ifdef _OPENMP + sternheimer_weak_q_detail::DynamicThreadsGuard dynamic_threads; +#pragma omp parallel num_threads(layout.workers) +#endif + { + const int index = +#ifdef _OPENMP + omp_get_thread_num(); +#else + 0; +#endif + sternheimer_weak_q_detail::InnerThreadsGuard inner_threads(layout.inner_threads); + std::unique_ptr worker; + try + { +#ifdef _OPENMP + if (omp_get_num_threads() != layout.workers) + throw std::runtime_error("Weak q dispatch actual OpenMP team does not match requested workers."); +#endif + worker.reset(new Worker(factory(index))); + } + catch (...) + { + errors[index] = std::current_exception(); + factory_failed.store(true, std::memory_order_relaxed); + } + // All threads must agree to enter the worksharing loop, including when a + // factory throws. Keep this flag separate from callback exception slots. +#ifdef _OPENMP +#pragma omp barrier +#endif + if (!factory_failed.load(std::memory_order_relaxed)) + { +#ifdef _OPENMP +#pragma omp for schedule(dynamic, 1) +#endif + for (std::int64_t task = 0; task < task_count; ++task) + { + try + { + callback(task, *worker, index); + } + catch (...) + { + if (!errors[index]) errors[index] = std::current_exception(); + } + ++counts[index]; + } + } + } + for (const auto& error : errors) + if (error) std::rethrow_exception(error); + return counts; +} + +} // namespace ModuleRI +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.cpp b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.cpp new file mode 100644 index 00000000000..47adb35a281 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.cpp @@ -0,0 +1,420 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ +namespace +{ +using Sha256 = module_ri::sternheimer_siab::Sha256; +constexpr unsigned char magic[8] = {'S', 'T', 'W', 'Q', 'R', 'E', 'F', 0}; +constexpr std::uint32_t schema = 1; +constexpr std::uint64_t checksum_size = 64; +constexpr std::uint64_t record_header_size = 94; +static_assert(sizeof(double) == 8 && std::numeric_limits::is_iec559, + "Weak-q references require IEEE binary64"); +static_assert(sizeof(int) == 4, "Weak-q record indices require 32-bit int"); + +[[noreturn]] void fail(const std::string& message) +{ + throw std::runtime_error("Sternheimer weak-q reference: " + message); +} + +[[noreturn]] void io_fail(const std::string& operation) +{ + const int error = errno; + fail(operation + ": " + std::strerror(error)); +} + +struct Budget +{ + std::uint64_t remaining; + + void charge(const std::uint64_t count, const std::uint64_t width) + { + if (width != 0 && count > remaining / width) fail("size/allocation limit exceeded"); + remaining -= count * width; + } +}; + +void validate_arguments(const std::string& path, const std::string& contract, + const SternheimerWeakQReferenceDimensions& d, + const SternheimerWeakQReferenceLimits& limits) +{ + if (path.empty() || path.find('\0') != std::string::npos) fail("invalid path"); + if (contract.empty() || contract.size() > 65536) fail("invalid physical contract hash"); + if (d.fine_grid_size == 0 || d.full_record_count == 0 || d.orbital_count == 0) + fail("dimensions must be positive"); + if (limits.max_file_bytes < checksum_size || limits.max_allocation_bytes == 0 + || limits.max_bands_per_record == 0) + fail("invalid limits"); + for (const auto count : {d.fine_grid_size, d.full_record_count, d.orbital_count}) + if (count > std::numeric_limits::max()) fail("dimension exceeds address space"); +} + +void charge_record(Budget& file, Budget& allocation, const std::uint64_t occupied, + const std::uint64_t unoccupied, const std::uint64_t orbitals, + const SternheimerWeakQReferenceLimits& limits) +{ + if (occupied > limits.max_bands_per_record || unoccupied > limits.max_bands_per_record - occupied) + fail("band-count limit exceeded"); + file.charge(1, record_header_size); + file.charge(occupied, 16); // Eigenvalues and occupations. + file.charge(unoccupied, 8); + allocation.charge(occupied, 2 * sizeof(double)); + allocation.charge(unoccupied, sizeof(double)); + const auto bands = occupied + unoccupied; // Addition bounded by the band limit above. + if (bands != 0 && orbitals > std::numeric_limits::max() / bands) + fail("coefficient dimension overflow"); + const auto elements = bands * orbitals; + file.charge(elements, 16); + allocation.charge(bands, sizeof(std::vector>)); + allocation.charge(elements, sizeof(std::complex)); +} + +class File +{ + public: + std::FILE* stream = nullptr; + std::string temporary; + File() = default; + File(const File&) = delete; + File& operator=(const File&) = delete; + ~File() + { + if (stream) std::fclose(stream); + if (!temporary.empty()) ::unlink(temporary.c_str()); + } + + void attach(const int fd, const char* mode) + { + stream = ::fdopen(fd, mode); + if (!stream) + { + const int error = errno; + ::close(fd); + errno = error; + io_fail("fdopen"); + } + } +}; + +class Writer +{ + public: + Writer(std::FILE* stream, const std::uint64_t limit) : stream_(stream), budget_{limit} {} + + void bytes(const unsigned char* data, const std::size_t size) + { + budget_.charge(size, 1); + if (std::fwrite(data, 1, size, stream_) != size) io_fail("write"); + sha_.update(data, size); + } + + void integer(std::uint64_t value, const unsigned width = 8) + { + unsigned char data[8]; + for (unsigned i = 0; i < width; ++i) + { + data[i] = value & 255; + value >>= 8; + } + bytes(data, width); + } + + void real(const double value) + { + if (!std::isfinite(value)) fail("nonfinite value"); + std::uint64_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + integer(bits); + } + + void reals(const std::vector& values) + { + for (const double value : values) real(value); + } + + void coefficients(const std::vector>>& values) + { + for (const auto& row : values) + for (const auto& value : row) + { + real(value.real()); + real(value.imag()); + } + } + + void seal() + { + const auto digest = sha_.finish(); + budget_.charge(digest.size(), 1); + if (std::fwrite(digest.data(), 1, digest.size(), stream_) != digest.size()) io_fail("write checksum"); + } + + private: + std::FILE* stream_; + Budget budget_; + Sha256 sha_; +}; + +class Reader +{ + public: + Reader(std::FILE* stream, const std::uint64_t payload_size) : stream_(stream), remaining_(payload_size) {} + + std::uint64_t remaining() const { return remaining_; } + + void bytes(unsigned char* data, const std::size_t size) + { + if (size > remaining_) fail("truncated payload"); + if (std::fread(data, 1, size, stream_) != size) fail("truncated payload or read error"); + remaining_ -= size; + sha_.update(data, size); + } + + std::uint64_t integer(const unsigned width = 8) + { + unsigned char data[8]; + bytes(data, width); + std::uint64_t value = 0; + for (unsigned i = 0; i < width; ++i) value |= std::uint64_t(data[i]) << (8 * i); + return value; + } + + int index() + { + const auto value = integer(4); + // Avoid implementation-defined unsigned-to-signed conversion. + return value <= INT32_MAX ? static_cast(value) + : static_cast(-1 - static_cast(UINT32_MAX - value)); + } + + bool boolean() + { + const auto value = integer(1); + if (value > 1) fail("invalid boolean"); + return value == 1; + } + + double real() + { + const auto bits = integer(); + double value; + std::memcpy(&value, &bits, sizeof(value)); + if (!std::isfinite(value)) fail("nonfinite value"); + return value; + } + + void reals(std::vector& values, const std::uint64_t count) + { + if (count > remaining_ / 8 || count > values.max_size()) fail("invalid real-vector size"); + values.resize(static_cast(count)); + for (auto& value : values) value = real(); + } + + void coefficients(std::vector>>& values, + const std::uint64_t rows, const std::uint64_t columns) + { + if (rows > values.max_size()) fail("invalid coefficient row count"); + values.resize(static_cast(rows)); + for (auto& row : values) + { + if (columns > remaining_ / 16 || columns > row.max_size()) fail("invalid coefficient width"); + row.resize(static_cast(columns)); + for (auto& value : row) + { + const double re = real(); + const double im = real(); + value = {re, im}; + } + } + } + + void verify() + { + if (remaining_ != 0) fail("trailing payload bytes"); + const auto expected = sha_.finish(); + char actual[checksum_size]; + if (std::fread(actual, 1, sizeof(actual), stream_) != sizeof(actual)) fail("truncated checksum"); + if (std::memcmp(actual, expected.data(), sizeof(actual)) != 0) fail("SHA256 mismatch"); + if (std::fgetc(stream_) != EOF || std::ferror(stream_)) fail("trailing bytes or read error"); + } + + private: + std::FILE* stream_; + std::uint64_t remaining_; + Sha256 sha_; +}; + +void validate_record_shape(const SternheimerLCAOOccupiedKPoint& r, const std::uint64_t orbitals) +{ + if (r.eigenvalues.size() != r.occupations.size() || r.eigenvalues.size() != r.coefficients.size() + || r.unoccupied_eigenvalues.size() != r.unoccupied_coefficients.size()) + fail("inconsistent band dimensions"); + for (const auto* matrix : {&r.coefficients, &r.unoccupied_coefficients}) + for (const auto& row : *matrix) + if (row.size() != orbitals) fail("inconsistent orbital dimensions"); +} + +void write_record(Writer& out, const SternheimerLCAOOccupiedKPoint& r) +{ + for (const int value : {r.local_k_index, r.global_k_index, r.zero_order_k_index, + r.symmetry_spatial_isym, r.spin_index}) + out.integer(static_cast(value), 4); + out.integer(r.symmetry_time_reversal ? 1 : 0, 1); + out.integer(r.has_grid_kpoint_override ? 1 : 0, 1); + for (const double value : r.kpoint) out.real(value); + for (const double value : r.grid_kpoint) out.real(value); + out.real(r.kweight); + out.integer(r.eigenvalues.size()); + out.integer(r.unoccupied_eigenvalues.size()); + out.reals(r.eigenvalues); + out.reals(r.occupations); + out.coefficients(r.coefficients); + out.reals(r.unoccupied_eigenvalues); + out.coefficients(r.unoccupied_coefficients); +} + +void read_record(Reader& in, SternheimerLCAOOccupiedKPoint& r, Budget& allocation, + const std::uint64_t orbitals, const SternheimerWeakQReferenceLimits& limits) +{ + const auto available = in.remaining(); + r.local_k_index = in.index(); + r.global_k_index = in.index(); + r.zero_order_k_index = in.index(); + r.symmetry_spatial_isym = in.index(); + r.spin_index = in.index(); + r.symmetry_time_reversal = in.boolean(); + r.has_grid_kpoint_override = in.boolean(); + for (auto& value : r.kpoint) value = in.real(); + for (auto& value : r.grid_kpoint) value = in.real(); + r.kweight = in.real(); + const auto occupied = in.integer(); + const auto unoccupied = in.integer(); + // Check encoded size and cumulative decoded storage BEFORE any band allocation. + Budget file{available}; + charge_record(file, allocation, occupied, unoccupied, orbitals, limits); + in.reals(r.eigenvalues, occupied); + in.reals(r.occupations, occupied); + in.coefficients(r.coefficients, occupied, orbitals); + in.reals(r.unoccupied_eigenvalues, unoccupied); + in.coefficients(r.unoccupied_coefficients, unoccupied, orbitals); +} +} // namespace + +void write_sternheimer_weak_q_reference( + const std::string& path, const std::string& contract_hash, + const SternheimerWeakQReferenceDimensions& d, const std::vector& fine_potential, + const std::vector& full_records, + const SternheimerWeakQReferenceLimits& limits) +{ + validate_arguments(path, contract_hash, d, limits); + if (fine_potential.size() != d.fine_grid_size || full_records.size() != d.full_record_count) + fail("potential/record dimensions mismatch"); + Budget file_budget{limits.max_file_bytes}; + Budget allocation{limits.max_allocation_bytes}; + file_budget.charge(1, 8 + 4 + 8 + 24 + checksum_size); + file_budget.charge(contract_hash.size(), 1); + file_budget.charge(d.fine_grid_size, 8); + allocation.charge(d.fine_grid_size, sizeof(double)); + allocation.charge(d.full_record_count, sizeof(SternheimerLCAOOccupiedKPoint)); + for (const auto& r : full_records) + { + validate_record_shape(r, d.orbital_count); + charge_record(file_budget, allocation, r.eigenvalues.size(), r.unoccupied_eigenvalues.size(), + d.orbital_count, limits); + } + + File file; + // Allocate the cleanup path before creating the file; mkstemp mutates only its suffix. + file.temporary = path + ".tmp.XXXXXX"; + const int fd = ::mkstemp(&file.temporary[0]); + if (fd < 0) + { + file.temporary.clear(); + io_fail("create temporary file"); + } + file.attach(fd, "wb"); + Writer out(file.stream, limits.max_file_bytes); + out.bytes(magic, sizeof(magic)); + out.integer(schema, 4); + out.integer(contract_hash.size()); + out.bytes(reinterpret_cast(contract_hash.data()), contract_hash.size()); + out.integer(d.fine_grid_size); + out.integer(d.full_record_count); + out.integer(d.orbital_count); + out.reals(fine_potential); + for (const auto& r : full_records) write_record(out, r); + out.seal(); + if (std::fflush(file.stream) != 0) io_fail("flush temporary file"); + if (::fsync(::fileno(file.stream)) != 0) io_fail("sync temporary file"); + const int close_result = std::fclose(file.stream); + file.stream = nullptr; + if (close_result != 0) io_fail("close temporary file"); + // link(), unlike rename(), has atomic no-replace semantics on POSIX. + if (::link(file.temporary.c_str(), path.c_str()) != 0) io_fail("publish new file"); +} + +SternheimerWeakQReference read_sternheimer_weak_q_reference( + const std::string& path, const std::string& contract_hash, + const SternheimerWeakQReferenceDimensions& d, const SternheimerWeakQReferenceLimits& limits) +{ + validate_arguments(path, contract_hash, d, limits); + File file; + // Nonblocking open avoids hanging on a FIFO before the regular-file check. + const int fd = ::open(path.c_str(), O_RDONLY | O_NONBLOCK); + if (fd < 0) io_fail("open reference"); + file.attach(fd, "rb"); + struct stat st; + if (::fstat(fd, &st) != 0) io_fail("stat reference"); + if (!S_ISREG(st.st_mode) || st.st_size < static_cast(checksum_size)) fail("invalid reference file"); + const auto size = static_cast(st.st_size); + if (size > limits.max_file_bytes) fail("file-size limit exceeded"); + Reader in(file.stream, size - checksum_size); + unsigned char found_magic[sizeof(magic)]; + in.bytes(found_magic, sizeof(found_magic)); + if (std::memcmp(found_magic, magic, sizeof(magic)) != 0) fail("invalid magic"); + if (in.integer(4) != schema) fail("unsupported schema"); + if (in.integer() != contract_hash.size()) fail("physical contract mismatch"); + unsigned char chunk[4096]; + for (std::size_t offset = 0; offset < contract_hash.size();) + { + const auto length = std::min(sizeof(chunk), contract_hash.size() - offset); + in.bytes(chunk, length); + if (std::memcmp(chunk, contract_hash.data() + offset, length) != 0) fail("physical contract mismatch"); + offset += length; + } + const auto grid = in.integer(); + const auto count = in.integer(); + const auto orbitals = in.integer(); + if (grid != d.fine_grid_size || count != d.full_record_count || orbitals != d.orbital_count) + fail("physical dimensions mismatch"); + + Budget encoded{in.remaining()}; + encoded.charge(grid, 8); + encoded.charge(count, record_header_size); + Budget allocation{limits.max_allocation_bytes}; + allocation.charge(grid, sizeof(double)); + allocation.charge(count, sizeof(SternheimerLCAOOccupiedKPoint)); + SternheimerWeakQReference result; + if (count > result.full_records.max_size()) fail("invalid record count"); + in.reals(result.fine_potential, grid); + result.full_records.resize(static_cast(count)); + for (auto& r : result.full_records) read_record(in, r, allocation, orbitals, limits); + in.verify(); + return result; +} + +} // namespace ModuleRI diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.h new file mode 100644 index 00000000000..34fd74b374a --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.h @@ -0,0 +1,72 @@ +#ifndef STERNHEIMER_WEAK_Q_REFERENCE_H +#define STERNHEIMER_WEAK_Q_REFERENCE_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" + +#include +#include +#include + +namespace ModuleRI +{ + +struct SternheimerWeakQReferenceDimensions +{ + std::uint64_t fine_grid_size = 0; + std::uint64_t full_record_count = 0; + std::uint64_t orbital_count = 0; +}; + +struct SternheimerWeakQReferenceLimits +{ + std::uint64_t max_file_bytes = UINT64_C(1073741824); + // Cumulative decoded element storage, including nested vector objects; + // not a bound on allocator bookkeeping or the caller's existing copies. + std::uint64_t max_allocation_bytes = UINT64_C(1073741824); + std::uint64_t max_bands_per_record = UINT64_C(1048576); +}; + +struct SternheimerWeakQReference +{ + std::vector fine_potential; + std::vector full_records; +}; + +// contract_hash is an opaque, nonempty byte string (at most 65536 bytes). +// The caller must hash its exact physical contract: geometry, grids, PP/NAO/ +// orbital ordering, spin, units, etc. No normalization or SCF-value hashing is +// performed here. Dimensions must be positive and match exactly on both ends. +// +// Schema 1: little-endian fixed-width integers and IEEE binary64, all actual +// occupied-k-point fields, followed by 64 lowercase ASCII SHA256 hex bytes. +// The checksum covers the entire preceding file, including contract and schema. +// This detects corruption, not malicious substitution by a party able to reseal. +// Bump the schema and update both codecs when the record definition changes. +// +// POSIX publication uses a same-directory temporary file, fsync, and link: an +// existing destination (including a dangling symlink) is never overwritten. +// All validation/I/O failures throw std::runtime_error; no partial reference +// is returned or published. Allocator exhaustion may still throw std::bad_alloc. +void write_sternheimer_weak_q_reference( + const std::string& path, + const std::string& contract_hash, + const SternheimerWeakQReferenceDimensions& dimensions, + const std::vector& fine_potential, + const std::vector& full_records, + const SternheimerWeakQReferenceLimits& limits = {}); + +// After successful validation, the caller must replace BOTH its fine-potential +// copy and its full records before computing the old reference hash, e.g.: +// auto reference = read_sternheimer_weak_q_reference(path, contract, dims); +// fine_potential.swap(reference.fine_potential); +// full_records.swap(reference.full_records); +// The two swaps are nonthrowing with these standard-allocator vector types. +SternheimerWeakQReference read_sternheimer_weak_q_reference( + const std::string& path, + const std::string& contract_hash, + const SternheimerWeakQReferenceDimensions& dimensions, + const SternheimerWeakQReferenceLimits& limits = {}); + +} // namespace ModuleRI + +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_unit.h b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_unit.h new file mode 100644 index 00000000000..4166e604170 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_unit.h @@ -0,0 +1,206 @@ +#ifndef STERNHEIMER_WEAK_Q_UNIT_H +#define STERNHEIMER_WEAK_Q_UNIT_H + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_matrix_audit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ModuleRI +{ + +// Pure one-based state/frequency selection. Columns and shard indices are zero-based. +struct SternheimerWeakQUnit +{ + struct Raw + { + const char* source_k = nullptr; + const char* band_begin = nullptr; + const char* band_end = nullptr; + const char* frequency_begin = nullptr; + const char* frequency_end = nullptr; + const char* shard_index = nullptr; + const char* shard_count = nullptr; + }; + + int source_k; + int band_begin; + int band_end; + int frequency_begin; + int frequency_end; + SternheimerWeakAuditShard shard; + + static SternheimerWeakQUnit parse(const Raw& raw, const std::vector& occupied_counts, + const int nfreq, const int channels) + { + if (occupied_counts.empty() || nfreq <= 0 + || occupied_counts.size() > static_cast(std::numeric_limits::max()) + || std::any_of(occupied_counts.begin(), occupied_counts.end(), [](int n) { return n <= 0; })) + throw std::invalid_argument("Weak q unit requires full occupied counts and positive frequency count."); + const int source = integer(raw.source_k, 1, "WEAK_Q_SOURCE_K"); + if (source > static_cast(occupied_counts.size())) + throw std::invalid_argument("WEAK_Q_SOURCE_K is outside the full k mesh."); + const int bands = occupied_counts[source - 1]; + SternheimerWeakQUnit unit{source, + integer(raw.band_begin, 1, "WEAK_Q_BAND_BEGIN"), + integer(raw.band_end, bands, "WEAK_Q_BAND_END"), + integer(raw.frequency_begin, 1, "WEAK_Q_FREQ_BEGIN"), + integer(raw.frequency_end, nfreq, "WEAK_Q_FREQ_END"), + SternheimerWeakAuditShard::parse(raw.shard_index, raw.shard_count, channels)}; + if (unit.band_begin > unit.band_end || unit.band_end > bands + || unit.frequency_begin > unit.frequency_end || unit.frequency_end > nfreq) + throw std::invalid_argument("Weak q unit band/frequency ranges must be inclusive and within the full input."); + unit.expected_equations(); + return unit; + } + + std::vector columns() const { return shard.columns(); } + bool all_source_bands(int count) const { return band_begin == 1 && band_end == count; } + bool all_frequencies(int count) const { return frequency_begin == 1 && frequency_end == count; } + + std::uint64_t expected_equations() const + { + std::uint64_t result = 2; + for (const auto count : {std::uint64_t(band_end - band_begin + 1), + std::uint64_t(frequency_end - frequency_begin + 1), + std::uint64_t(columns().size())}) + { + if (count == 0 || result > std::numeric_limits::max() / count) + throw std::overflow_error("Weak q unit equation count overflow."); + result *= count; + } + return result; + } + + std::string stem(int iq) const + { + if (iq <= 0) throw std::invalid_argument("Weak q unit requires a positive q index."); + return "STERNHEIMER_WEAK_Q_iq_" + std::to_string(iq) + "_k_" + std::to_string(source_k) + + "_b_" + std::to_string(band_begin) + "_" + std::to_string(band_end) + + "_f_" + std::to_string(frequency_begin) + "_" + std::to_string(frequency_end) + + "_shard_" + std::to_string(shard.index) + "_of_" + std::to_string(shard.count); + } + + private: + static int integer(const char* raw, int fallback, const char* name) + { + if (raw == nullptr) return fallback; + const auto invalid = [name]() { + return std::invalid_argument(std::string(name) + " must be a positive decimal integer."); + }; + if (*raw == '\0') throw invalid(); + int result = 0; + for (const char* p = raw; *p; ++p) + { + if (*p < '0' || *p > '9' || result > (std::numeric_limits::max() - (*p - '0')) / 10) + throw invalid(); + result = 10 * result + (*p - '0'); + } + if (result <= 0) throw invalid(); + return result; + } +}; + +inline void validate_sternheimer_weak_q_fold(const std::array& source, + const std::array& q, + const std::array& target, + const std::array& reciprocal_shift) +{ + for (int a = 0; a < 3; ++a) + { + const double shift = source[a] + q[a] - target[a]; + if (!std::isfinite(source[a]) || !std::isfinite(q[a]) || !std::isfinite(target[a]) + || !std::isfinite(shift) || std::abs(shift - reciprocal_shift[a]) > 1e-10) + throw std::invalid_argument("Weak q unit requires canonical target and an integer k+q fold matching the plan."); + } +} + +inline double sternheimer_weak_q_weight(double kweight, double occupation) +{ + if (!std::isfinite(kweight) || kweight <= 0 || !std::isfinite(occupation) + || std::abs(occupation - 1.0) > 1e-10 || !std::isfinite(kweight * occupation)) + throw std::invalid_argument("Weak q unit requires positive kweight and integer insulating occupation."); + return kweight * occupation; // ABACUS kweight already includes spin degeneracy. +} + +inline double sternheimer_weak_q_omega_ry(double omega_ha) +{ + if (!std::isfinite(omega_ha) || omega_ha <= 0 || !std::isfinite(2.0 * omega_ha)) + throw std::invalid_argument("Weak q unit requires finite positive imaginary frequency in Ha."); + return 2.0 * omega_ha; +} + +inline double sternheimer_weak_q_left_vertex_factor() { return 0.5; } + +// Compact radial interpolation extends through the zero node AFTER the last nonzero node. +// Only exact zeros are ignored; this is not a density-tail truncation tolerance. +inline double sternheimer_weak_q_radial_support(const std::vector& radii, + const std::vector& values) +{ + if (radii.empty() || radii.size() != values.size()) + throw std::invalid_argument("Weak q unit invalid radial density support."); + std::size_t last = 0; + bool nonzero = false; + for (std::size_t i = 0; i < radii.size(); ++i) + { + if (!std::isfinite(radii[i]) || radii[i] < 0 || !std::isfinite(values[i]) + || (i > 0 && radii[i] <= radii[i - 1])) + throw std::invalid_argument("Weak q unit radial support requires finite ordered radial data."); + if (values[i] != 0) { nonzero = true; last = i; } + } + return nonzero ? radii[std::min(last + 1, radii.size() - 1)] : 0.0; +} + +inline bool sternheimer_weak_q_z_support_contained(double center, double radius, double height) +{ + if (!std::isfinite(center) || !std::isfinite(radius) || radius < 0 || !std::isfinite(height) || height <= 0) + throw std::invalid_argument("Weak q unit invalid slab support geometry."); + const double margin = 1e-12 * height; + return center - radius > margin && center + radius < height - margin; +} + +inline void validate_sternheimer_weak_q_residual(bool converged, + double relative, + double absolute, + double tolerance) +{ + if (!std::isfinite(tolerance) || tolerance <= 0) + throw std::invalid_argument("Weak q unit residual tolerance must be finite and positive."); + if (!converged || !std::isfinite(relative) || relative < 0 || relative > tolerance + || !std::isfinite(absolute) || absolute < 0) + throw std::runtime_error("Weak q unit original augmented residual did not pass requested tolerance."); +} + +// Each column already contains the Ha left-vertex contraction of ONE signed solve. +// Accumulate into column-major owned-column storage, with no Hermitian completion. +inline void accumulate_sternheimer_weak_q_column(std::vector>& partial, + const std::vector>& column, + int channels, std::size_t owned, double weight) +{ + if (channels <= 0 || column.size() != static_cast(channels) + || partial.size() % static_cast(channels) != 0 + || owned >= partial.size() / static_cast(channels) + || !std::isfinite(weight) || weight <= 0) + throw std::invalid_argument("Weak q unit invalid partial-column layout or weight."); + const auto finite = [](std::complex value) { + return std::isfinite(value.real()) && std::isfinite(value.imag()); + }; + for (int row = 0; row < channels; ++row) + { + const auto index = static_cast(row) + static_cast(channels) * owned; + if (!finite(column[row]) || !finite(partial[index]) || !finite(partial[index] + weight * column[row])) + throw std::invalid_argument("Weak q unit nonfinite weighted response."); + } + for (int row = 0; row < channels; ++row) + partial[static_cast(row) + static_cast(channels) * owned] += weight * column[row]; +} + +} // namespace ModuleRI +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/test/CMakeLists.txt b/source/source_lcao/module_rpa/module_delta_st/test/CMakeLists.txt new file mode 100644 index 00000000000..69437727652 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/CMakeLists.txt @@ -0,0 +1,292 @@ +abacus_disable_feature_definitions(__MLALGO) +abacus_disable_feature_definitions(__CUDA) +abacus_disable_feature_definitions(__ROCM) + +set(STERNHEIMER_RPA_TEST_LIBS ${math_libs}) +set(STERNHEIMER_RPA_TEST_SOURCES + sternheimer_rpa_test.cpp + sternheimer_projector_locality_test.cpp + ../sternheimer_rpa.cpp) +if(ENABLE_GREENX_MINIMAX) + list(APPEND STERNHEIMER_RPA_TEST_LIBS GreenX::MiniMax) + list(APPEND STERNHEIMER_RPA_TEST_SOURCES ../greenx_minimax_adapter.f90) +endif() +AddTest( + TARGET MODULE_RPA_sternheimer_runtime_options_test + LIBS parameter base ${math_libs} device + SOURCES sternheimer_runtime_options_test.cpp ../sternheimer_runtime_options.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_rpa_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES ${STERNHEIMER_RPA_TEST_SOURCES} +) +if(ENABLE_GREENX_MINIMAX) + target_compile_definitions(MODULE_RPA_sternheimer_rpa_test PRIVATE __GREENX_MINIMAX) +endif() +AddTest( + TARGET MODULE_RPA_sternheimer_wavefunction_diagnostic_test + LIBS ${math_libs} + SOURCES sternheimer_wavefunction_diagnostic_test.cpp ../sternheimer_wavefunction_diagnostic.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_supercell_perturbation_test + LIBS ${math_libs} + SOURCES sternheimer_supercell_perturbation_test.cpp ../sternheimer_supercell_perturbation.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_supercell_sector_test + LIBS ${math_libs} + SOURCES sternheimer_supercell_sector_test.cpp ../sternheimer_supercell_sector.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_fd_hamiltonian_test + LIBS ${math_libs} + SOURCES sternheimer_fd_hamiltonian_test.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_fd_preconditioner_test + LIBS ${math_libs} + SOURCES sternheimer_fd_preconditioner_test.cpp ../sternheimer_fd_preconditioner.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_fd_solver_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_fd_solver_test.cpp ../sternheimer_fd_solver.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_preconditioner.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp ../sternheimer_rpa.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_grid_transfer_test + LIBS ${math_libs} + SOURCES sternheimer_grid_transfer_test.cpp ../sternheimer_grid_transfer.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_galerkin_operator_test + LIBS ${math_libs} + SOURCES sternheimer_galerkin_operator_test.cpp ../sternheimer_galerkin_operator.cpp ../sternheimer_grid_transfer.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_delta_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_delta_test.cpp ../sternheimer_delta.cpp ../sternheimer_kq.cpp ../sternheimer_fd_solver.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_preconditioner.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_rpa.cpp ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_galerkin_audit_test + LIBS ${math_libs} + SOURCES sternheimer_galerkin_audit_test.cpp ../sternheimer_galerkin_audit.cpp ../sternheimer_galerkin_operator.cpp ../sternheimer_grid_transfer.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_weak_grid_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_weak_grid_test.cpp ../sternheimer_weak_grid.cpp ../sternheimer_grid_transfer.cpp ../sternheimer_delta.cpp ../sternheimer_kq.cpp ../sternheimer_fd_solver.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_preconditioner.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_rpa.cpp ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_weak_matrix_audit_test + LIBS parameter base ${math_libs} device container + SOURCES sternheimer_weak_matrix_audit_test.cpp ../sternheimer_abfs_perturbation.cpp ../sternheimer_kq.cpp + ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_weak_augmented_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_weak_augmented_test.cpp ../sternheimer_weak_augmented.cpp ../sternheimer_rpa.cpp ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_weak_preconditioner_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_weak_preconditioner_test.cpp ../sternheimer_weak_preconditioner.cpp + ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_weak_q_unit_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_weak_q_unit_test.cpp ../sternheimer_kq.cpp + ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp + ../sternheimer_weak_augmented.cpp ../sternheimer_rpa.cpp + ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_weak_q_dispatch_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_weak_q_dispatch_test.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_weak_q_reference_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_weak_q_reference_test.cpp ../sternheimer_weak_q_reference.cpp + ../sternheimer_siab_provenance.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_abfs_strict2d_test + LIBS parameter base ${math_libs} device container + SOURCES sternheimer_abfs_strict2d_test.cpp ../sternheimer_abfs_strict2d.cpp + ../sternheimer_kq.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_grid_diagnostics_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_grid_diagnostics_test.cpp ../sternheimer_grid_diagnostics.cpp ../sternheimer_delta.cpp ../sternheimer_kq.cpp ../sternheimer_fd_solver.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_preconditioner.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_rpa.cpp ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_dft_zero_order_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_dft_zero_order_test.cpp ../sternheimer_dft_zero_order.cpp ../sternheimer_fd_solver.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_preconditioner.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp ../sternheimer_rpa.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_fd_nonlocal_projector_test + LIBS ${math_libs} + SOURCES sternheimer_fd_nonlocal_projector_test.cpp ../sternheimer_fd_nonlocal_projector.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_fd_projector_sampler_test + LIBS parameter base ${math_libs} device container + SOURCES sternheimer_fd_projector_sampler_test.cpp ../sternheimer_fd_projector_sampler.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_abacus_fd_adapter_test + LIBS parameter base ${math_libs} device container + SOURCES sternheimer_abacus_fd_adapter_test.cpp ../sternheimer_abacus_fd_adapter.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_response_grid_test + LIBS parameter base ${math_libs} device container planewave + SOURCES sternheimer_response_grid_test.cpp ../sternheimer_response_grid.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_abacus_fd_nonlocal_test + LIBS parameter base ${math_libs} device container + SOURCES sternheimer_abacus_fd_nonlocal_test.cpp + ../sternheimer_abacus_fd_nonlocal.cpp + ../sternheimer_fd_projector_sampler.cpp + ../sternheimer_fd_nonlocal_projector.cpp + ../sternheimer_kq.cpp + ../../../../source_cell/atom_spec.cpp + ../../../../source_cell/atom_pseudo.cpp + ../../../../source_cell/pseudo.cpp + ../../../../source_cell/test/support/mock_unitcell.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_abacus_fd_smoke_test + LIBS ${math_libs} + SOURCES sternheimer_abacus_fd_smoke_test.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_abfs_perturbation_test + LIBS parameter base ${math_libs} device container + SOURCES sternheimer_abfs_perturbation_test.cpp ../sternheimer_abfs_perturbation.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_coulomb_tail_test + LIBS parameter base ${math_libs} device container + SOURCES sternheimer_coulomb_tail_test.cpp ../sternheimer_abfs_perturbation.cpp + ../sternheimer_kq.cpp ../sternheimer_fd_hamiltonian.cpp ../sternheimer_fd_nonlocal_projector.cpp + ../../../module_ri/conv_coulomb_pot_k.cpp ../../../../source_basis/module_ao/orb_atomic_lm.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_abacus_st_smoke_test + LIBS ${math_libs} + SOURCES sternheimer_abacus_st_smoke_test.cpp sternheimer_representative_ownership_test.cpp ../sternheimer_kq.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_channel_resources_test + LIBS ${math_libs} + SOURCES sternheimer_channel_resources_test.cpp ../sternheimer_channel_resources.cpp +) +add_executable( + MODULE_RPA_sternheimer_owned_channel_workers_test + sternheimer_owned_channel_workers_test.cpp + ../sternheimer_channel_resources.cpp +) +if(ENABLE_MPI) + target_link_libraries(MODULE_RPA_sternheimer_owned_channel_workers_test PRIVATE MPI::MPI_CXX) +endif() +install(TARGETS MODULE_RPA_sternheimer_owned_channel_workers_test DESTINATION ${CMAKE_BINARY_DIR}/tests) +add_test( + NAME MODULE_RPA_sternheimer_owned_channel_workers_test + COMMAND ./MODULE_RPA_sternheimer_owned_channel_workers_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +AddTest( + TARGET MODULE_RPA_sternheimer_periodic_solver_test + LIBS ${STERNHEIMER_RPA_TEST_LIBS} + SOURCES sternheimer_periodic_solver_test.cpp + ../sternheimer_periodic_solver.cpp + ../sternheimer_fd_solver.cpp + ../sternheimer_fd_hamiltonian.cpp + ../sternheimer_fd_preconditioner.cpp + ../sternheimer_fd_nonlocal_projector.cpp + ../sternheimer_delta.cpp + ../sternheimer_kq.cpp + ../sternheimer_rpa.cpp + ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_siab_overlap_test + LIBS ${math_libs} + SOURCES test_sternheimer_siab_overlap.cpp + ../sternheimer_siab_overlap.cpp + ../../../../source_base/module_external/blas_connector_matrix.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_siab_writer_test + LIBS parameter ${math_libs} base device io_input + SOURCES test_sternheimer_siab_writer.cpp ../sternheimer_siab_writer.cpp +) +AddTest( + TARGET MODULE_RPA_sternheimer_siab_provenance_test + LIBS ${math_libs} + SOURCES test_sternheimer_siab_provenance.cpp ../sternheimer_siab_provenance.cpp +) +if(ENABLE_MPI) + add_executable( + MODULE_RPA_sternheimer_siab_mpi_test + test_sternheimer_siab_mpi.cpp + ../sternheimer_chi0_mpi.cpp + ../sternheimer_grid_diagnostics.cpp + ../sternheimer_rpa.cpp + ../sternheimer_siab_mpi.cpp + ../sternheimer_siab_overlap.cpp + ../sternheimer_siab_writer.cpp + ) + target_link_libraries( + MODULE_RPA_sternheimer_siab_mpi_test + PRIVATE + parameter + base + device + io_input + ${STERNHEIMER_RPA_TEST_LIBS} + Threads::Threads + GTest::gtest + ) + if(USE_OPENMP) + target_link_libraries(MODULE_RPA_sternheimer_siab_mpi_test PRIVATE OpenMP::OpenMP_CXX) + endif() + if(ENABLE_COVERAGE) + add_coverage(MODULE_RPA_sternheimer_siab_mpi_test) + endif() + install(TARGETS MODULE_RPA_sternheimer_siab_mpi_test DESTINATION ${CMAKE_BINARY_DIR}/tests) + add_test( + NAME MODULE_RPA_sternheimer_siab_mpi_test + COMMAND $ + WORKING_DIRECTORY $ + ) + add_test( + NAME MODULE_RPA_sternheimer_siab_mpi_test_mpi2 + COMMAND mpirun -np 2 $ + WORKING_DIRECTORY $ + ) +endif() +AddTest( + TARGET MODULE_RPA_sternheimer_siab_primitives_test + LIBS parameter base ${math_libs} device planewave container psi + SOURCES test_sternheimer_siab_primitives.cpp + ../../../../source_io/module_bessel/numerical_basis.cpp + ../../../../source_io/module_bessel/bessel_basis.cpp + ../../../../source_cell/parallel_kpoints.cpp +) +target_compile_options(MODULE_RPA_sternheimer_siab_primitives_test PRIVATE -U__LCAO) +add_test( + NAME MODULE_RPA_sternheimer_siab_primitives_test_mpi2 + COMMAND mpirun -np 2 ./MODULE_RPA_sternheimer_siab_primitives_test + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_adapter_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_adapter_test.cpp new file mode 100644 index 00000000000..f8217041210 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_adapter_test.cpp @@ -0,0 +1,152 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_adapter.h" + +#include "source_base/matrix3.h" + +#include +#include +#include +#include +#include +#include + +TEST(SternheimerABACUSFDAdapter, BuildsOrthogonalGridFromABACUSLattice) +{ + const ModuleBase::Matrix3 latvec(3.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 5.0); + const auto grid_data = ModuleRI::make_sternheimer_fd_grid_from_lattice(6, 8, 10, 6 * 8 * 10, 2.0, latvec); + + EXPECT_EQ(grid_data.grid.nx, 6); + EXPECT_EQ(grid_data.grid.ny, 8); + EXPECT_EQ(grid_data.grid.nz, 10); + EXPECT_TRUE(grid_data.grid.periodic); + EXPECT_DOUBLE_EQ(grid_data.grid.hx, 1.0); + EXPECT_DOUBLE_EQ(grid_data.grid.hy, 1.0); + EXPECT_DOUBLE_EQ(grid_data.grid.hz, 1.0); + EXPECT_DOUBLE_EQ(grid_data.volume_element, 1.0); +} + +TEST(SternheimerABACUSFDAdapter, RejectsDistributedRealSpaceGridForDensePrototype) +{ + const ModuleBase::Matrix3 latvec(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + + EXPECT_THROW(ModuleRI::make_sternheimer_fd_grid_from_lattice(4, 4, 4, 32, 8.0, latvec), std::invalid_argument); +} + +TEST(SternheimerABACUSFDAdapter, EmbedsLocalZSlabAndAcceptsEmptyRank) +{ + const auto slab = ModuleRI::embed_sternheimer_local_z_slab({1.0, 2.0}, 2, 3, 1, 1); + EXPECT_EQ(slab, (std::vector{0.0, 1.0, 0.0, 0.0, 2.0, 0.0})); + + const auto empty = ModuleRI::embed_sternheimer_local_z_slab({}, 2, 3, 0, 3); + EXPECT_EQ(empty, std::vector(6, 0.0)); + + EXPECT_THROW(ModuleRI::embed_sternheimer_local_z_slab({1.0}, 2, 3, 1, 1), std::invalid_argument); +} + +TEST(SternheimerABACUSFDAdapter, BuildsNonOrthogonalPrimitiveGridWithDeterminantVolume) +{ + const ModuleBase::Matrix3 latvec(0.0, 0.5, 0.5, + 0.5, 0.0, 0.5, + 0.5, 0.5, 0.0); + const auto grid_data = ModuleRI::make_sternheimer_fd_grid_from_lattice(4, 4, 4, 64, 2.0, latvec); + + EXPECT_NEAR(grid_data.grid.hx, std::sqrt(2.0) / 4.0, 1.0e-15); + EXPECT_NEAR(grid_data.grid.hy, std::sqrt(2.0) / 4.0, 1.0e-15); + EXPECT_NEAR(grid_data.grid.hz, std::sqrt(2.0) / 4.0, 1.0e-15); + EXPECT_NEAR(grid_data.volume_element, 2.0 / 64.0, 1.0e-15); +} + +TEST(SternheimerABACUSFDAdapter, NonOrthogonalPlaneWaveUsesContravariantMetric) +{ + constexpr int ngrid = 4; + constexpr double pi = 3.141592653589793238462643383279502884; + const ModuleBase::Matrix3 latvec(0.0, 0.5, 0.5, + 0.5, 0.0, 0.5, + 0.5, 0.5, 0.0); + const auto grid_data + = ModuleRI::make_sternheimer_fd_grid_from_lattice(ngrid, ngrid, ngrid, ngrid * ngrid * ngrid, 2.0, latvec); + const ModuleRI::SternheimerFDHamiltonian hamiltonian( + grid_data.grid, + std::vector(static_cast(grid_data.grid.size()), 0.0), + 1.0); + + ModuleRI::SternheimerFDHamiltonian::Vector psi(static_cast(grid_data.grid.size())); + for (int ix = 0; ix != ngrid; ++ix) + { + for (int iy = 0; iy != ngrid; ++iy) + { + for (int iz = 0; iz != ngrid; ++iz) + { + const double phase = 2.0 * pi * static_cast(ix + iy) / static_cast(ngrid); + psi[static_cast(hamiltonian.index(ix, iy, iz))] + = std::exp(std::complex(0.0, phase)); + } + } + } + + ModuleRI::SternheimerFDHamiltonian::Vector hpsi; + hamiltonian.apply(psi, hpsi); + constexpr double expected_kinetic = 40.0; + for (std::size_t ir = 0; ir != psi.size(); ++ir) + { + EXPECT_NEAR(hpsi[ir].real(), expected_kinetic * psi[ir].real(), 1.0e-12); + EXPECT_NEAR(hpsi[ir].imag(), expected_kinetic * psi[ir].imag(), 1.0e-12); + } +} + +TEST(SternheimerABACUSFDAdapter, EmptyLocalPotentialSlabContributesZeros) +{ + const std::vector full_potential = ModuleRI::embed_sternheimer_local_z_slab({}, 4, 5, 0, 5); + + ASSERT_EQ(full_potential.size(), 20U); + for (const double value: full_potential) + { + EXPECT_DOUBLE_EQ(value, 0.0); + } +} + +TEST(SternheimerABACUSFDAdapter, EmbedsOwnedLocalPotentialSlabAtGlobalZOffset) +{ + const std::vector local_potential = {1.0, 2.0, 3.0, 4.0}; + const std::vector full_potential + = ModuleRI::embed_sternheimer_local_z_slab(local_potential, 2, 4, 2, 1); + + EXPECT_EQ(full_potential, (std::vector{0.0, 1.0, 2.0, 0.0, 0.0, 3.0, 4.0, 0.0})); +} + +TEST(SternheimerABACUSFDAdapter, BuildsHamiltonianWithNonlocalProjector) +{ + ModuleRI::SternheimerABACUSFDGridData grid_data; + grid_data.grid.nx = 2; + grid_data.grid.ny = 1; + grid_data.grid.nz = 1; + grid_data.grid.hx = 1.0; + grid_data.grid.hy = 1.0; + grid_data.grid.hz = 1.0; + grid_data.grid.periodic = true; + grid_data.volume_element = 1.0; + + using Projector = ModuleRI::SternheimerFDNonlocalProjector; + using Complex = Projector::Complex; + Projector::ProjectorBlock block; + block.projectors = {{Complex(1.0, 0.0), Complex(0.0, 0.0)}}; + block.d_matrix = {{Complex(2.0, 0.0)}}; + const auto nonlocal_projector = std::make_shared(2, grid_data.volume_element, std::vector{block}); + + const auto hamiltonian = ModuleRI::make_sternheimer_fd_hamiltonian_from_local_potential( + grid_data, + std::vector{0.0, 0.0}, + 0.0, + nonlocal_projector); + + ASSERT_NE(hamiltonian.nonlocal_projector(), nullptr); + + const ModuleRI::SternheimerFDHamiltonian::Vector psi = {Complex(3.0, 0.0), Complex(4.0, 0.0)}; + ModuleRI::SternheimerFDHamiltonian::Vector hpsi; + hamiltonian.apply(psi, hpsi); + + ASSERT_EQ(hpsi.size(), psi.size()); + EXPECT_NEAR(hpsi[0].real(), 6.0, 1.0e-12); + EXPECT_NEAR(hpsi[0].imag(), 0.0, 1.0e-12); + EXPECT_NEAR(hpsi[1].real(), 0.0, 1.0e-12); + EXPECT_NEAR(hpsi[1].imag(), 0.0, 1.0e-12); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_nonlocal_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_nonlocal_test.cpp new file mode 100644 index 00000000000..7aa2fbc4109 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_nonlocal_test.cpp @@ -0,0 +1,133 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_nonlocal.h" + +#include "source_base/matrix.h" +#include "source_cell/atom_spec.h" +#include "source_cell/unitcell.h" + +#include +#include + +Magnetism::Magnetism() {} +Magnetism::~Magnetism() {} + +namespace +{ + +constexpr double y00() +{ + return 0.28209479177387814347; +} + +ModuleRI::SternheimerFDHamiltonian::Grid line_grid() +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 3; + grid.ny = 1; + grid.nz = 1; + grid.hx = 1.0; + grid.hy = 1.0; + grid.hz = 1.0; + grid.periodic = false; + return grid; +} + +void fill_single_s_projector(Atom& atom) +{ + atom.ncpp.has_so = false; + atom.ncpp.tvanp = false; + atom.ncpp.nbeta = 1; + atom.ncpp.mesh = 3; + atom.ncpp.r = {0.0, 1.0, 2.0}; + atom.ncpp.lll = {0}; + atom.ncpp.betar.create(1, 3); + atom.ncpp.betar(0, 0) = 2.0; + atom.ncpp.betar(0, 1) = 4.0; + atom.ncpp.betar(0, 2) = 6.0; + atom.ncpp.dion.create(1, 1); + atom.ncpp.dion(0, 0) = 1.5; +} + +void fill_one_atom_cell(UnitCell& ucell) +{ + ucell.lat0 = 1.0; + ucell.ntype = 1; + ucell.nat = 1; + ucell.set_atom_flag = true; + ucell.atoms = new Atom[1]; + ucell.atoms[0].na = 1; + ucell.atoms[0].tau = {ModuleBase::Vector3(0.0, 0.0, 0.0)}; + fill_single_s_projector(ucell.atoms[0]); +} + +} // namespace + +TEST(SternheimerABACUSFDNonlocal, BuildsProjectorFromUnitCellAtomPseudo) +{ + UnitCell ucell; + fill_one_atom_cell(ucell); + + const auto projector + = ModuleRI::make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, line_grid(), 1.0); + + ASSERT_NE(projector, nullptr); + ASSERT_EQ(projector->blocks().size(), 1); + ASSERT_EQ(projector->blocks()[0].projectors.size(), 1); + EXPECT_NEAR(projector->blocks()[0].projectors[0][0].real(), 4.0 * y00(), 1.0e-14); + EXPECT_NEAR(projector->blocks()[0].projectors[0][1].real(), 4.0 * y00(), 1.0e-14); + EXPECT_NEAR(projector->blocks()[0].projectors[0][2].real(), 3.0 * y00(), 1.0e-14); + EXPECT_NEAR(projector->blocks()[0].d_matrix[0][0].real(), 1.5, 1.0e-14); +} + +TEST(SternheimerABACUSFDNonlocal, ConvertsAtomTauFromLat0UnitsToBohr) +{ + UnitCell ucell; + ucell.lat0 = 2.0; + ucell.ntype = 1; + ucell.nat = 1; + ucell.set_atom_flag = true; + ucell.atoms = new Atom[1]; + ucell.atoms[0].na = 1; + ucell.atoms[0].tau = {ModuleBase::Vector3(0.5, 0.0, 0.0)}; + fill_single_s_projector(ucell.atoms[0]); + ucell.atoms[0].ncpp.betar(0, 0) = 0.0; + ucell.atoms[0].ncpp.betar(0, 1) = 3.0; + ucell.atoms[0].ncpp.betar(0, 2) = 4.0; + + const auto projector + = ModuleRI::make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, line_grid(), 1.0); + + ASSERT_NE(projector, nullptr); + ASSERT_EQ(projector->blocks()[0].projectors.size(), 1); + EXPECT_NEAR(projector->blocks()[0].projectors[0][2].real(), 3.0 * y00(), 1.0e-14); +} + +TEST(SternheimerABACUSFDNonlocal, ReturnsNullWhenNoAtomsCarryProjectors) +{ + UnitCell ucell; + ucell.ntype = 1; + ucell.lat0 = 1.0; + ucell.nat = 1; + ucell.set_atom_flag = true; + ucell.atoms = new Atom[1]; + ucell.atoms[0].na = 1; + ucell.atoms[0].tau = {ModuleBase::Vector3(0.0, 0.0, 0.0)}; + + const auto projector + = ModuleRI::make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, line_grid(), 1.0); + + EXPECT_EQ(projector, nullptr); +} + +TEST(SternheimerABACUSFDNonlocal, RejectsUnsupportedUltrasoftAndSOCPseudopotentials) +{ + UnitCell ucell; + fill_one_atom_cell(ucell); + ucell.atoms[0].ncpp.tvanp = true; + EXPECT_THROW(ModuleRI::make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, line_grid(), 1.0), + std::invalid_argument); + + ucell.atoms[0].ncpp.tvanp = false; + ucell.atoms[0].ncpp.has_so = true; + EXPECT_THROW(ModuleRI::make_sternheimer_fd_nonlocal_projector_from_unitcell(ucell, line_grid(), 1.0), + std::invalid_argument); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_smoke_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_smoke_test.cpp new file mode 100644 index 00000000000..d1a2666ee2c --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_fd_smoke_test.cpp @@ -0,0 +1,41 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_fd_smoke.h" + +#include +#include + +TEST(SternheimerABACUSFDSmoke, FormatsZeroOrderComparisonReport) +{ + ModuleRI::SternheimerABACUSDFTZeroOrderResult result; + result.grid_data.grid.nx = 2; + result.grid_data.grid.ny = 3; + result.grid_data.grid.nz = 4; + result.grid_data.grid.hx = 0.5; + result.grid_data.grid.hy = 0.5; + result.grid_data.grid.hz = 0.5; + result.grid_data.volume_element = 0.125; + result.fd_states.eigenvalues = {-0.4, 0.2}; + result.fd_states.residual_norms = {1.0e-10, 2.0e-9}; + result.dft_eigenvalues = {-0.41, 0.25}; + result.dft_occupations = {1.0, 0.0}; + result.hamiltonian_mode = "full_nc_nonlocal"; + result.lanczos_max_subspace_size = 640; + + result.comparison.max_abs_fd_minus_dft = 0.05; + result.comparison.all_eigenvalues_within_tolerance = false; + result.comparison.bands = { + ModuleRI::SternheimerDFTZeroOrderBandComparison{0, -0.4, -0.41, 0.01, 1.0e-10, true}, + ModuleRI::SternheimerDFTZeroOrderBandComparison{1, 0.2, 0.25, -0.05, 2.0e-9, false}, + }; + + const std::string report = ModuleRI::format_sternheimer_fd_zero_order_report(result); + + EXPECT_NE(report.find("# ABACUS Sternheimer FD zero-order smoke test"), std::string::npos); + EXPECT_NE(report.find("grid 2 3 4 size 24 dV 0.125"), std::string::npos); + EXPECT_NE(report.find("hamiltonian_mode full_nc_nonlocal"), std::string::npos); + EXPECT_NE(report.find("lanczos_max_subspace_size 640"), std::string::npos); + EXPECT_NE(report.find("band fd_eigenvalue_Ry dft_eigenvalue_Ry fd_minus_dft_Ry occupation fd_residual_norm eigenvalue_within_tolerance"), std::string::npos); + EXPECT_NE(report.find("0 -0.4 -0.41 0.01 1 1e-10 yes"), std::string::npos); + EXPECT_NE(report.find("1 0.2 0.25 -0.05 0 2e-09 no"), std::string::npos); + EXPECT_NE(report.find("max_abs_fd_minus_dft_Ry 0.05"), std::string::npos); + EXPECT_NE(report.find("all_eigenvalues_within_tolerance no"), std::string::npos); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_st_smoke_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_st_smoke_test.cpp new file mode 100644 index 00000000000..47657e01eb6 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abacus_st_smoke_test.cpp @@ -0,0 +1,1079 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_channel_parallel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +TEST(SternheimerABACUSSTSmoke, UsesProductionDefaults) +{ + EXPECT_DOUBLE_EQ(ModuleRI::default_sternheimer_solver_tolerance(), 1.0e-6); +} + +TEST(SternheimerChannelParallel, ExecutesConcurrentlyAndReturnsChannelOrder) +{ +#ifdef _OPENMP + const int previous_threads = omp_get_max_threads(); + const int previous_dynamic = omp_get_dynamic(); + omp_set_dynamic(0); + omp_set_num_threads(4); +#endif + + std::atomic active_workers{0}; + std::atomic peak_workers{0}; + const std::vector results + = ModuleRI::run_sternheimer_channel_tasks(32, [&active_workers, &peak_workers](const int channel_index) { + const int active = active_workers.fetch_add(1) + 1; + int observed_peak = peak_workers.load(); + while (active > observed_peak && !peak_workers.compare_exchange_weak(observed_peak, active)) + { + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + active_workers.fetch_sub(1); + return channel_index * channel_index; + }); + +#ifdef _OPENMP + omp_set_num_threads(previous_threads); + omp_set_dynamic(previous_dynamic); +#endif + ASSERT_EQ(results.size(), 32U); + for (int channel_index = 0; channel_index != 32; ++channel_index) + { + EXPECT_EQ(results[static_cast(channel_index)], channel_index * channel_index); + } +#ifdef _OPENMP + EXPECT_GT(peak_workers.load(), 1); +#else + EXPECT_EQ(peak_workers.load(), 1); +#endif +} + +TEST(SternheimerChannelParallel, HonorsExplicitMaximumWorkerCount) +{ +#ifdef _OPENMP + const int previous_threads = omp_get_max_threads(); + const int previous_dynamic = omp_get_dynamic(); + omp_set_dynamic(0); + omp_set_num_threads(4); +#endif + + std::atomic active_workers{0}; + std::atomic peak_workers{0}; + const std::vector results + = ModuleRI::run_sternheimer_channel_tasks( + 32, + [&active_workers, &peak_workers](const int channel_index) { + const int active = active_workers.fetch_add(1) + 1; + int observed_peak = peak_workers.load(); + while (active > observed_peak && !peak_workers.compare_exchange_weak(observed_peak, active)) + { + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + active_workers.fetch_sub(1); + return channel_index; + }, + 2); + +#ifdef _OPENMP + omp_set_num_threads(previous_threads); + omp_set_dynamic(previous_dynamic); +#endif + ASSERT_EQ(results.size(), 32U); + for (int channel_index = 0; channel_index != 32; ++channel_index) + { + EXPECT_EQ(results[static_cast(channel_index)], channel_index); + } +#ifdef _OPENMP + EXPECT_EQ(peak_workers.load(), 2); +#else + EXPECT_EQ(peak_workers.load(), 1); +#endif +} + +TEST(SternheimerChannelParallel, SingleWorkerPreservesNestedGridTeam) +{ +#ifdef _OPENMP + const int previous_threads = omp_get_max_threads(); + const int previous_dynamic = omp_get_dynamic(); + const int previous_active_levels = omp_get_max_active_levels(); + omp_set_dynamic(0); + omp_set_num_threads(4); + omp_set_max_active_levels(1); +#endif + + const std::vector nested_team_sizes = ModuleRI::run_sternheimer_channel_tasks( + 4, + [](const int) { +#ifdef _OPENMP + int nested_team_size = 0; +#pragma omp parallel + { +#pragma omp single + nested_team_size = omp_get_num_threads(); + } + return nested_team_size; +#else + return 1; +#endif + }, + 1); + +#ifdef _OPENMP + omp_set_num_threads(previous_threads); + omp_set_dynamic(previous_dynamic); + omp_set_max_active_levels(previous_active_levels); +#endif + ASSERT_EQ(nested_team_sizes.size(), 4U); + for (const int nested_team_size: nested_team_sizes) + { +#ifdef _OPENMP + EXPECT_EQ(nested_team_size, 4); +#else + EXPECT_EQ(nested_team_size, 1); +#endif + } +} + +TEST(SternheimerChannelParallel, RethrowsFirstIndexedExceptionAfterAllTasksFinish) +{ + std::atomic completed_tasks{0}; + try + { + static_cast(ModuleRI::run_sternheimer_channel_tasks(16, [&completed_tasks](const int channel_index) { + completed_tasks.fetch_add(1); + if (channel_index == 3 || channel_index == 9) + { + throw std::runtime_error("channel " + std::to_string(channel_index)); + } + return channel_index; + })); + FAIL() << "Expected a channel task exception."; + } + catch (const std::runtime_error& error) + { + EXPECT_STREQ(error.what(), "channel 3"); + } + EXPECT_EQ(completed_tasks.load(), 16); + + EXPECT_THROW(ModuleRI::run_sternheimer_channel_tasks(-1, [](const int) { return 0; }), std::invalid_argument); + EXPECT_THROW(ModuleRI::run_sternheimer_channel_tasks(1, [](const int) { return 0; }, -1), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, FormatsLinearResponseReport) +{ + ModuleRI::SternheimerABACUSSTSmokeResult result; + result.grid_data.grid.nx = 2; + result.grid_data.grid.ny = 3; + result.grid_data.grid.nz = 4; + result.grid_data.volume_element = 0.125; + result.omega = 0.5; + result.pca_threshold = 1.0e-4; + result.ccp_rmesh_times = 10.0; + result.perturbation_source = "abfs_ccp"; + result.num_available_channels = 5; + + ModuleRI::SternheimerABACUSSTChannelResult channel; + channel.band_index = 0; + channel.channel_index = 1; + channel.atom_index = 0; + channel.angular_momentum = 1; + channel.radial_index = 2; + channel.magnetic_index = 1; + channel.fd_eigenvalue = -1.25; + channel.occupation = 2.0; + channel.rhs_norm = 0.4; + channel.projected_rhs_norm = 0.3; + channel.solver_converged = true; + channel.solver_iterations = 7; + channel.solver_relative_residual = 1.0e-9; + channel.equation_residual_norm = 2.0e-9; + channel.polarizability = std::complex(-0.12, 0.03); + result.channels.push_back(channel); + + const std::string report = ModuleRI::format_sternheimer_abacus_st_report(result); + + EXPECT_NE(report.find("# ABACUS Sternheimer FD linear-response smoke test"), std::string::npos); + EXPECT_NE(report.find("grid 2 3 4 size 24 dV 0.125"), std::string::npos); + EXPECT_NE(report.find("omega_Ry 0.5"), std::string::npos); + EXPECT_NE(report.find("pca_threshold 0.0001"), std::string::npos); + EXPECT_NE(report.find("perturbation_source abfs_ccp"), std::string::npos); + EXPECT_NE(report.find("available_channels 5"), std::string::npos); + EXPECT_NE(report.find("0 1 0 1 2 1 -1.25 2 0.4 0.3 yes 7 1e-09 2e-09 -0.12 0.03"), + std::string::npos); +} + +namespace +{ + +ModuleRI::SternheimerLCAOOccupiedKPoint make_occupied_kpoint(const int local_k_index, + const int global_k_index, + const int spin_index, + const ModuleRI::SternheimerReducedKPoint& kpoint, + const double kweight) +{ + ModuleRI::SternheimerLCAOOccupiedKPoint record; + record.local_k_index = local_k_index; + record.global_k_index = global_k_index; + record.zero_order_k_index = local_k_index; + record.spin_index = spin_index; + record.kpoint = kpoint; + record.kweight = kweight; + record.eigenvalues = {-1.0}; + record.occupations = {1.0}; + record.coefficients = {{std::complex(1.0, 0.0), + std::complex(0.0, 0.0), + std::complex(0.0, 0.0)}}; + return record; +} + +TEST(SternheimerABACUSSTSmoke, LimitsDiagnosticChannelsPerAtomWithoutEmptyBlocks) +{ + std::vector channels(4); + channels[0].atom_index = 0; + channels[0].atom_local_index = 0; + channels[1].atom_index = 0; + channels[1].atom_local_index = 1; + channels[2].atom_index = 1; + channels[2].atom_local_index = 0; + channels[3].atom_index = 1; + channels[3].atom_local_index = 1; + + const auto limited = ModuleRI::limit_sternheimer_abf_channels_per_atom(channels, 1); + + ASSERT_EQ(limited.size(), 2); + EXPECT_EQ(limited[0].atom_index, 0); + EXPECT_EQ(limited[0].atom_local_index, 0); + EXPECT_EQ(limited[0].channel_index, 0); + EXPECT_EQ(limited[1].atom_index, 1); + EXPECT_EQ(limited[1].atom_local_index, 0); + EXPECT_EQ(limited[1].channel_index, 1); +} + +TEST(SternheimerABACUSSTSmoke, UnlimitedDiagnosticChannelsReuseRvalueStorage) +{ + std::vector channels(1); + channels[0].potential_r.resize(8, std::complex(1.0, -0.5)); + const auto* original_storage = channels[0].potential_r.data(); + + const auto unlimited = ModuleRI::limit_sternheimer_abf_channels_per_atom(std::move(channels), -1); + + ASSERT_EQ(unlimited.size(), 1); + EXPECT_EQ(unlimited[0].potential_r.data(), original_storage); +} + +TEST(SternheimerABACUSSTSmoke, SamplesOnlyRequestedPeriodicKSUnoccupiedStates) +{ + EXPECT_EQ(ModuleRI::sternheimer_sampled_unoccupied_count(false, 676, 32), 0); + EXPECT_EQ(ModuleRI::sternheimer_sampled_unoccupied_count(true, 676, 0), 676); + EXPECT_EQ(ModuleRI::sternheimer_sampled_unoccupied_count(true, 676, 32), 32); + EXPECT_EQ(ModuleRI::sternheimer_sampled_unoccupied_count(true, 12, 32), 12); + EXPECT_THROW(ModuleRI::sternheimer_sampled_unoccupied_count(true, 676, -1), std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, SamplesOnlyRequestedPeriodicOccupiedSourceStates) +{ + EXPECT_EQ(ModuleRI::sternheimer_sampled_occupied_count(68, 1), 1); + EXPECT_EQ(ModuleRI::sternheimer_sampled_occupied_count(68, 0), 68); + EXPECT_EQ(ModuleRI::sternheimer_sampled_occupied_count(68, -1), 68); + EXPECT_EQ(ModuleRI::sternheimer_sampled_occupied_count(12, 32), 12); + EXPECT_THROW(ModuleRI::sternheimer_sampled_occupied_count(0, 1), std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, GathersRequestedPeriodicKSUnoccupiedStatesForDelta) +{ + EXPECT_EQ(ModuleRI::sternheimer_lcao_virtual_state_gather_count(676, false, "ks_bands", 32), 0); + EXPECT_EQ(ModuleRI::sternheimer_lcao_virtual_state_gather_count(676, true, "projected_ao", 32), 0); + EXPECT_EQ(ModuleRI::sternheimer_lcao_virtual_state_gather_count(676, true, "ks_bands", 0), 676); + EXPECT_EQ(ModuleRI::sternheimer_lcao_virtual_state_gather_count(676, true, "ks_bands", 32), 32); + EXPECT_EQ(ModuleRI::sternheimer_lcao_virtual_state_gather_count(12, true, "ks_bands", 32), 12); + EXPECT_THROW(ModuleRI::sternheimer_lcao_virtual_state_gather_count(-1, true, "ks_bands", 32), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_lcao_virtual_state_gather_count(676, true, "ks_bands", -1), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, DistinguishesTwoKPointsFromTwoSpinChannels) +{ + const auto k0 = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 0.5); + const auto k1 = make_occupied_kpoint(1, 1, 0, {0.5, 0.0, 0.0}, 0.5); + const std::vector two_kpoints = {k0, k1}; + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints(two_kpoints, 2, 2, 1, 3)); + EXPECT_EQ(ModuleRI::sternheimer_lcao_total_occupied_bands(two_kpoints), 2); + + auto spin_down = k0; + spin_down.local_k_index = 1; + spin_down.global_k_index = 1; + spin_down.zero_order_k_index = 1; + spin_down.spin_index = 1; + const std::vector two_spins = {k0, spin_down}; + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints(two_spins, 2, 2, 2, 3)); +} + +TEST(SternheimerABACUSSTSmoke, DoesNotReconstructFullKGridWhenSymmetryIsDisabled) +{ + EXPECT_FALSE(ModuleRI::sternheimer_full_k_reconstruction_required(2, 1, 2, 0)); +} + +TEST(SternheimerABACUSSTSmoke, DoesNotReconstructAnAlreadyFullKGrid) +{ + EXPECT_FALSE(ModuleRI::sternheimer_full_k_reconstruction_required(4, 4, 1, 1)); +} + +TEST(SternheimerABACUSSTSmoke, DoesNotReconstructFullGammaForTwoSpinChannels) +{ + EXPECT_FALSE(ModuleRI::sternheimer_full_k_reconstruction_required(2, 1, 2, 1)); +} + +TEST(SternheimerABACUSSTSmoke, ReconstructsASymmetryReducedSingleSpinKGrid) +{ + EXPECT_TRUE(ModuleRI::sternheimer_full_k_reconstruction_required(2, 8, 1, 1)); +} + +TEST(SternheimerABACUSSTSmoke, RejectsSymmetryReducedSpinPolarizedKGrid) +{ + EXPECT_THROW(ModuleRI::sternheimer_full_k_reconstruction_required(4, 8, 2, 1), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, MapsCanonicalFullQIndexToReaderV1StarIndex) +{ + auto q0 = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 0.25); + auto qx = make_occupied_kpoint(1, 1, 0, {0.25, 0.0, 0.0}, 0.25); + auto qx_partner = make_occupied_kpoint(2, 2, 0, {-0.25, 0.0, 0.0}, 0.25); + auto qy = make_occupied_kpoint(3, 3, 0, {0.0, 0.25, 0.0}, 0.25); + qx_partner.zero_order_k_index = 1; + qx_partner.symmetry_spatial_isym = 2; + qy.zero_order_k_index = 2; + + const std::vector records{ + q0, qx, qx_partner, qy}; + EXPECT_EQ(ModuleRI::sternheimer_coulomb_reader_q_index_one_based(records, 1), 1); + EXPECT_EQ(ModuleRI::sternheimer_coulomb_reader_q_index_one_based(records, 2), 2); + EXPECT_EQ(ModuleRI::sternheimer_coulomb_reader_q_index_one_based(records, 4), 3); + EXPECT_THROW(ModuleRI::sternheimer_coulomb_reader_q_index_one_based(records, 0), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, SelectsOnlyOccupiedGammaSpinRecordsForOpenShellResponse) +{ + auto occupied_spin = make_occupied_kpoint(1, 1, 1, {0.0, 0.0, 0.0}, 1.0); + + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints( + {occupied_spin}, 2, 2, 2, 3, -1, false)); + const auto selected = ModuleRI::select_sternheimer_gamma_spin_records({occupied_spin}, 2); + + ASSERT_EQ(selected.size(), 1U); + EXPECT_EQ(selected.front()->spin_index, 1); +} + +TEST(SternheimerABACUSSTSmoke, RejectsDuplicateOrIncompleteGlobalKRecords) +{ + const auto k0 = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 0.5); + const auto k1 = make_occupied_kpoint(1, 1, 0, {0.5, 0.0, 0.0}, 0.5); + + auto duplicate = k1; + duplicate.global_k_index = 0; + EXPECT_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints({k0, duplicate}, 2, 2, 1, 3), + std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints({k0}, 2, 2, 1, 3), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, GammaRecordPreservesLegacyWeightedOccupation) +{ + const auto gamma = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 2.0); + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints({gamma}, 1, 1, 1, 3)); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_lcao_weighted_occupation(gamma, 0), 2.0); +} + +TEST(SternheimerABACUSSTSmoke, DividesSupercellKWeightAcrossPrimitiveTranslationSectors) +{ + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_supercell_sector_kweight(2.0, 64), 0.03125); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_supercell_sector_kweight(0.5, 8), 1.0 / 16.0); + EXPECT_THROW(ModuleRI::sternheimer_supercell_sector_kweight(1.0, 0), std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, RestoresPrimitiveNormalizationForFullSupercellResponseMatrix) +{ + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_supercell_response_matrix_scale(false, 64), 1.0); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_supercell_response_matrix_scale(true, 8), 8.0); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_supercell_response_matrix_scale(true, 64), 64.0); + EXPECT_THROW(ModuleRI::sternheimer_supercell_response_matrix_scale(true, 0), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, PlansVirtualSamplingWithoutMakingTheOptionalSOSDiagnosticMandatory) +{ + const auto standard = ModuleRI::sternheimer_lcao_sampling_plan(false, false, false); + EXPECT_FALSE(standard.sample_source_unoccupied); + EXPECT_FALSE(standard.sample_target_unoccupied); + EXPECT_FALSE(standard.build_target_ao_candidates); + + const auto delta_fallback = ModuleRI::sternheimer_lcao_sampling_plan(true, false, false); + EXPECT_FALSE(delta_fallback.sample_source_unoccupied); + EXPECT_FALSE(delta_fallback.sample_target_unoccupied); + EXPECT_TRUE(delta_fallback.build_target_ao_candidates); + + const auto delta_virtuals = ModuleRI::sternheimer_lcao_sampling_plan(true, false, true); + EXPECT_FALSE(delta_virtuals.sample_source_unoccupied); + EXPECT_TRUE(delta_virtuals.sample_target_unoccupied); + EXPECT_FALSE(delta_virtuals.build_target_ao_candidates); + + EXPECT_THROW(ModuleRI::sternheimer_lcao_sampling_plan(false, true, false), std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, ReusesTargetSamplingWhenItContainsTheSameSourceRecord) +{ + const auto occupied_only = ModuleRI::sternheimer_lcao_sampling_plan(false, false, false); + const auto target_virtuals = ModuleRI::sternheimer_lcao_sampling_plan(true, false, true); + ModuleRI::SternheimerLCAOSamplingPlan missing_source_virtuals; + missing_source_virtuals.sample_source_unoccupied = true; + + EXPECT_TRUE(ModuleRI::can_reuse_sternheimer_target_lcao_sampling(true, occupied_only)); + EXPECT_TRUE(ModuleRI::can_reuse_sternheimer_target_lcao_sampling(true, target_virtuals)); + EXPECT_FALSE(ModuleRI::can_reuse_sternheimer_target_lcao_sampling(false, target_virtuals)); + EXPECT_FALSE(ModuleRI::can_reuse_sternheimer_target_lcao_sampling(true, missing_source_virtuals)); +} + +TEST(SternheimerABACUSSTSmoke, RejectsNonInsulatingSupercellSectorOccupations) +{ + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_supercell_sector_occupations({1.0, 1.0}, 2)); + EXPECT_THROW(ModuleRI::validate_sternheimer_supercell_sector_occupations({1.0}, 2), + std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_sternheimer_supercell_sector_occupations({1.0, 0.5}, 2), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, ValidatesOptionalUnoccupiedLCAOStates) +{ + auto record = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 2.0); + record.unoccupied_eigenvalues = {0.5}; + record.unoccupied_coefficients = {{std::complex(0.0, 0.0), + std::complex(1.0, 0.0), + std::complex(0.0, 0.0)}}; + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints({record}, 1, 1, 1, 3)); + + record.unoccupied_coefficients[0].pop_back(); + EXPECT_THROW(ModuleRI::validate_sternheimer_lcao_occupied_kpoints({record}, 1, 1, 1, 3), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, BuildsTwoKPointNonzeroQResponsePlan) +{ + const auto k0 = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 1.0); + const auto k1 = make_occupied_kpoint(1, 1, 0, {0.5, 0.0, 0.0}, 1.0); + + const auto plan = ModuleRI::build_sternheimer_periodic_response_plan({k1, k0}, 2); + + EXPECT_EQ(plan.iq, 2); + EXPECT_EQ(plan.qpoint, (ModuleRI::SternheimerReducedKPoint{0.5, 0.0, 0.0})); + EXPECT_EQ(plan.record_index_by_global_k, (std::vector{1, 0})); + ASSERT_EQ(plan.kq_pairs.size(), 2); + EXPECT_EQ(plan.kq_pairs[0].source_index, 0); + EXPECT_EQ(plan.kq_pairs[0].target_index, 1); + EXPECT_EQ(plan.kq_pairs[1].source_index, 1); + EXPECT_EQ(plan.kq_pairs[1].target_index, 0); + EXPECT_DOUBLE_EQ(plan.kweight_sum, 2.0); +} + +TEST(SternheimerABACUSSTSmoke, BuildsPeriodicGammaResponsePlanBySelfMappingKPoints) +{ + auto k0 = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 1.0); + const auto k1 = make_occupied_kpoint(1, 1, 0, {0.5, 0.0, 0.0}, 1.0); + + const auto plan = ModuleRI::build_sternheimer_periodic_response_plan({k0, k1}, 1); + + EXPECT_EQ(plan.iq, 1); + EXPECT_EQ(plan.qpoint, (ModuleRI::SternheimerReducedKPoint{0.0, 0.0, 0.0})); + ASSERT_EQ(plan.kq_pairs.size(), 2); + EXPECT_EQ(plan.kq_pairs[0].source_index, 0); + EXPECT_EQ(plan.kq_pairs[0].target_index, 0); + EXPECT_EQ(plan.kq_pairs[1].source_index, 1); + EXPECT_EQ(plan.kq_pairs[1].target_index, 1); +} + +TEST(SternheimerABACUSSTSmoke, BuildsSingleGammaPeriodicResponsePlanWithPositiveIndex) +{ + const auto gamma = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 2.0); + const auto plan = ModuleRI::build_sternheimer_periodic_response_plan({gamma}, 1); + + EXPECT_EQ(plan.iq, 1); + EXPECT_EQ(plan.qpoint, (ModuleRI::SternheimerReducedKPoint{0.0, 0.0, 0.0})); + EXPECT_EQ(plan.record_index_by_global_k, (std::vector{0})); + ASSERT_EQ(plan.kq_pairs.size(), 1U); + EXPECT_EQ(plan.kq_pairs[0].source_index, 0); + EXPECT_EQ(plan.kq_pairs[0].target_index, 0); + EXPECT_DOUBLE_EQ(plan.kweight_sum, 2.0); + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_periodic_kmesh({1, 1, 1}, 1)); +} + +TEST(SternheimerABACUSSTSmoke, RejectsNonGammaSinglePointPeriodicResponse) +{ + for (int direction = 0; direction != 3; ++direction) + { + auto point = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 2.0); + point.kpoint[direction] = 0.25; + EXPECT_THROW(ModuleRI::build_sternheimer_periodic_response_plan({point}, 1), + std::invalid_argument); + } +} + +TEST(SternheimerABACUSSTSmoke, RejectsFractionalOccupationForSingleGammaPeriodicResponse) +{ + auto gamma = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 2.0); + gamma.occupations[0] = 0.5; + EXPECT_THROW(ModuleRI::build_sternheimer_periodic_response_plan({gamma}, 1), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, RejectsSpinChannelForSingleGammaPeriodicResponse) +{ + const auto gamma = make_occupied_kpoint(0, 0, 1, {0.0, 0.0, 0.0}, 1.0); + EXPECT_THROW(ModuleRI::build_sternheimer_periodic_response_plan({gamma}, 1), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, BuildsDisjointFixedQKOrbitsFromLittleGroupPermutations) +{ + const std::vector identity = {0, 1, 2, 3, 4, 5, 6, 7}; + const std::vector inversion = {0, 7, 6, 5, 4, 3, 2, 1}; + + const auto orbits + = ModuleRI::build_sternheimer_fixed_q_k_orbits_from_permutations(8, {identity, inversion}); + + ASSERT_EQ(orbits.size(), 5U); + EXPECT_EQ(orbits[0].representative_ik_full, 0); + EXPECT_EQ(orbits[0].members, (std::vector{0})); + EXPECT_EQ(orbits[1].representative_ik_full, 1); + EXPECT_EQ(orbits[1].members, (std::vector{1, 7})); + EXPECT_EQ(orbits[2].members, (std::vector{2, 6})); + EXPECT_EQ(orbits[3].members, (std::vector{3, 5})); + EXPECT_EQ(orbits[4].members, (std::vector{4})); +} + +TEST(SternheimerABACUSSTSmoke, RejectsNonBijectiveFixedQOperation) +{ + EXPECT_THROW(ModuleRI::build_sternheimer_fixed_q_k_orbits_from_permutations( + 4, {{0, 1, 2, 3}, {0, 0, 2, 3}}), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, SelectsWeakQRepresentativeAndCompleteInverseRoutes) +{ + const std::vector orbits = { + {0, {0}}, + {1, {1, 3}}, + {2, {2}}, + }; + const std::vector routes = { + {2, 0, 0, 0, false, {0, 0, 0}}, + {2, 1, 1, 0, false, {0, 0, 0}}, + {2, 1, 3, 4, true, {1, 0, 0}}, + {2, 2, 2, 0, false, {0, 0, 0}}, + }; + + const auto representative + = ModuleRI::select_sternheimer_weak_q_symmetry_source(2, 2, orbits, routes); + EXPECT_EQ(representative.iq, 2); + EXPECT_EQ(representative.source_ik_full, 1); + EXPECT_EQ(representative.representative_ik_full, 1); + EXPECT_TRUE(representative.source_is_representative); + EXPECT_EQ(representative.orbit_size, 2); + ASSERT_EQ(representative.inverse_routes.size(), 2U); + EXPECT_EQ(representative.inverse_routes[0].member_ik_full, 1); + EXPECT_EQ(representative.inverse_routes[1].member_ik_full, 3); + + const auto member + = ModuleRI::select_sternheimer_weak_q_symmetry_source(2, 4, orbits, routes); + EXPECT_EQ(member.source_ik_full, 3); + EXPECT_EQ(member.representative_ik_full, 1); + EXPECT_FALSE(member.source_is_representative); + EXPECT_EQ(member.orbit_size, 2); +} + +TEST(SternheimerABACUSSTSmoke, RejectsIncompleteWeakQSymmetryRoutes) +{ + const std::vector orbits = { + {0, {0, 2}}, + {1, {1}}, + }; + const std::vector missing_member = { + {1, 0, 0, 0, false, {0, 0, 0}}, + {1, 1, 1, 0, false, {0, 0, 0}}, + }; + EXPECT_THROW(ModuleRI::select_sternheimer_weak_q_symmetry_source( + 1, 1, orbits, missing_member), + std::invalid_argument); + + auto duplicate_member = missing_member; + duplicate_member.push_back({1, 0, 0, 0, false, {0, 0, 0}}); + duplicate_member.push_back({1, 0, 2, 3, false, {0, 0, 0}}); + EXPECT_THROW(ModuleRI::select_sternheimer_weak_q_symmetry_source( + 1, 1, orbits, duplicate_member), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, WeakQSymmetryRestorationRequiresAllOccupiedBands) +{ + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_weak_q_symmetry_band_coverage( + 1, 5, 5)); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_symmetry_band_coverage( + 1, 3, 5), + std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_symmetry_band_coverage( + 2, 5, 5), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, FormatsExplicitInverseRoutesForLibRPA) +{ + const std::vector routes = { + {1, 3, 6, 2, true, {0, -1, 0}}, + {1, 0, 0, 0, false, {0, 0, 0}}, + }; + + EXPECT_EQ(ModuleRI::format_sternheimer_fixed_q_routes(routes), + "version 1\n" + "# iq representative_ik member_ik spatial_isym time_reversal fold_Gx fold_Gy fold_Gz\n" + "1 0 0 0 0 0 0 0\n" + "1 3 6 2 1 0 -1 0\n"); + EXPECT_THROW(ModuleRI::format_sternheimer_fixed_q_routes({routes[0], routes[0]}), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, BuildsDiscreteQStarRoutesFromAllowedGridPermutations) +{ + ModuleRI::SternheimerQStarPermutation identity; + identity.spatial_isym = 0; + identity.time_reversal = false; + identity.mapped_index_by_full_q = {0, 1, 2, 3}; + identity.fold_G_by_full_q = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + + ModuleRI::SternheimerQStarPermutation swap; + swap.spatial_isym = 4; + swap.time_reversal = false; + swap.mapped_index_by_full_q = {0, 2, 1, 3}; + swap.fold_G_by_full_q = {{0, 0, 0}, {-1, 0, 0}, {1, 0, 0}, {0, 0, 0}}; + + const auto routes = ModuleRI::build_sternheimer_qstar_routes_from_permutations( + 4, {identity, swap}); + + ASSERT_EQ(routes.size(), 4U); + EXPECT_EQ(routes[0].representative_iq, 1); + EXPECT_EQ(routes[0].member_iq, 1); + EXPECT_EQ(routes[1].representative_iq, 2); + EXPECT_EQ(routes[1].member_iq, 2); + EXPECT_EQ(routes[2].representative_iq, 2); + EXPECT_EQ(routes[2].member_iq, 3); + EXPECT_EQ(routes[2].spatial_isym, 4); + EXPECT_EQ(routes[2].fold_G, (std::array{1, 0, 0})); + EXPECT_EQ(routes[3].representative_iq, 4); + EXPECT_EQ(routes[3].member_iq, 4); + + EXPECT_EQ(ModuleRI::format_sternheimer_qstar_routes(routes), + "version 1\n" + "# representative_iq member_iq spatial_isym time_reversal fold_Gx fold_Gy fold_Gz\n" + "1 1 0 0 0 0 0\n" + "2 2 0 0 0 0 0\n" + "2 3 4 0 1 0 0\n" + "4 4 0 0 0 0 0\n"); +} + +TEST(SternheimerABACUSSTSmoke, HermitianizesEachPartialResponseBeforeOutput) +{ + using Complex = std::complex; + const std::vector branch{{-1.0, 0.2}, {2.0, 3.0}, {4.0, -5.0}, {-6.0, 0.5}}; + + const auto record = ModuleRI::make_sternheimer_partial_response_record(2, 3, 4, branch, 2); + + EXPECT_EQ(record.iq, 2); + EXPECT_EQ(record.ik_full, 3); + EXPECT_EQ(record.ifrequency, 4); + EXPECT_EQ(record.filename, "v1_sternheimer_chi0_iq_2_ik_3_ifreq_4.dat"); + EXPECT_EQ(record.matrix, + (std::vector{{-2.0, 0.0}, {6.0, 8.0}, {6.0, -8.0}, {-12.0, 0.0}})); +} + +TEST(SternheimerABACUSSTSmoke, SumOfPartialResponsesEqualsLegacyAggregateResponse) +{ + using Complex = std::complex; + const std::vector branch_a{{-1.0, 0.2}, {2.0, 3.0}, {4.0, -5.0}, {-6.0, 0.5}}; + const std::vector branch_b{{0.5, -0.2}, {-1.0, 1.0}, {2.0, 0.5}, {3.0, -0.5}}; + const auto partial_a + = ModuleRI::make_sternheimer_partial_response_record(1, 0, 1, branch_a, 2); + const auto partial_b + = ModuleRI::make_sternheimer_partial_response_record(1, 1, 1, branch_b, 2); + + std::vector partial_sum(partial_a.matrix.size(), Complex(0.0, 0.0)); + std::vector aggregate_branch(branch_a.size(), Complex(0.0, 0.0)); + for (std::size_t index = 0; index != branch_a.size(); ++index) + { + partial_sum[index] = partial_a.matrix[index] + partial_b.matrix[index]; + aggregate_branch[index] = branch_a[index] + branch_b[index]; + } + std::vector expected(aggregate_branch.size(), Complex(0.0, 0.0)); + for (int row = 0; row != 2; ++row) + { + for (int column = 0; column != 2; ++column) + { + const std::size_t index = static_cast(row * 2 + column); + const std::size_t transpose = static_cast(column * 2 + row); + expected[index] = aggregate_branch[index] + std::conj(aggregate_branch[transpose]); + } + } + EXPECT_EQ(partial_sum, expected); +} + +TEST(SternheimerABACUSSTSmoke, FormatsPartialManifestInDeterministicKeyOrder) +{ + ModuleRI::SternheimerPartialResponseRecord later; + later.iq = 2; + later.ik_full = 7; + later.ifrequency = 3; + later.filename = "later.dat"; + ModuleRI::SternheimerPartialResponseRecord earlier; + earlier.iq = 1; + earlier.ik_full = 0; + earlier.ifrequency = 1; + earlier.filename = "earlier.dat"; + + EXPECT_EQ(ModuleRI::format_sternheimer_partial_manifest({later, earlier}), + "# iq ik_full ifreq response_file\n" + "1 0 1 earlier.dat\n" + "2 7 3 later.dat\n"); + EXPECT_THROW(ModuleRI::format_sternheimer_partial_manifest({earlier, earlier}), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, FormatsFullKPointManifestInGlobalIndexOrder) +{ + const auto k2 = make_occupied_kpoint(2, 2, 0, {0.0, 0.5, 0.0}, 0.25); + const auto k0 = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 0.25); + const auto k1 = make_occupied_kpoint(1, 1, 0, {0.5, 0.0, 0.0}, 0.25); + + EXPECT_EQ(ModuleRI::format_sternheimer_full_kpoint_manifest({k2, k0, k1}), + "# ik_full kx ky kz\n" + "0 0 0 0\n" + "1 0.5 0 0\n" + "2 0 0.5 0\n"); +} + +TEST(SternheimerABACUSSTSmoke, RejectsFractionalOccupationForPeriodicResponse) +{ + auto k0 = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 1.0); + const auto k1 = make_occupied_kpoint(1, 1, 0, {0.5, 0.0, 0.0}, 1.0); + + k0.occupations[0] = 0.5; + EXPECT_THROW(ModuleRI::build_sternheimer_periodic_response_plan({k0, k1}, 2), std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, ZeroQIndexPreservesSingleGammaPlan) +{ + const auto gamma = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 2.0); + const auto plan = ModuleRI::build_sternheimer_periodic_response_plan({gamma}, 0); + + EXPECT_EQ(plan.iq, 1); + EXPECT_EQ(plan.qpoint, (ModuleRI::SternheimerReducedKPoint{0.0, 0.0, 0.0})); + ASSERT_EQ(plan.kq_pairs.size(), 1); + EXPECT_EQ(plan.kq_pairs[0].source_index, 0); + EXPECT_EQ(plan.kq_pairs[0].target_index, 0); + EXPECT_DOUBLE_EQ(plan.kweight_sum, 2.0); +} + +TEST(SternheimerABACUSSTSmoke, BuildsSingleGammaSupercellTranslationPlanWithPositiveOutputIndex) +{ + const auto gamma = make_occupied_kpoint(0, 0, 0, {0.0, 0.0, 0.0}, 2.0); + const auto plan = ModuleRI::build_sternheimer_periodic_response_plan({gamma}, 1, true); + + EXPECT_EQ(plan.iq, 1); + EXPECT_EQ(plan.qpoint, (ModuleRI::SternheimerReducedKPoint{0.0, 0.0, 0.0})); + ASSERT_EQ(plan.kq_pairs.size(), 1); + EXPECT_EQ(plan.kq_pairs[0].source_index, 0); + EXPECT_EQ(plan.kq_pairs[0].target_index, 0); + EXPECT_DOUBLE_EQ(plan.kweight_sum, 2.0); +} + +TEST(SternheimerABACUSSTSmoke, ExpandsGammaSupercellIntoPrimitiveKRecordsWithGammaGridBoundary) +{ + ModuleRI::SternheimerLCAOOccupiedKPoint gamma; + gamma.local_k_index = 0; + gamma.global_k_index = 0; + gamma.zero_order_k_index = 0; + gamma.spin_index = 0; + gamma.kpoint = {0.0, 0.0, 0.0}; + gamma.kweight = 2.0; + gamma.eigenvalues = {-2.0, -1.0}; + gamma.occupations = {1.0, 1.0}; + gamma.coefficients = { + {1.0, 0.0, 0.0, 0.0}, + {0.0, 1.0, 0.0, 0.0}, + }; + gamma.unoccupied_eigenvalues = {1.0, 2.0}; + gamma.unoccupied_coefficients = { + {0.0, 0.0, 1.0, 0.0}, + {0.0, 0.0, 0.0, 1.0}, + }; + + std::vector sectors(2); + sectors[0].kpoint = {0.0, 0.0, 0.0}; + sectors[0].sector.eigenvalues = {-2.0, 1.0}; + sectors[0].sector.coefficients = {gamma.coefficients[0], gamma.unoccupied_coefficients[0]}; + sectors[1].kpoint = {0.5, 0.0, 0.0}; + sectors[1].sector.eigenvalues = {-1.0, 2.0}; + sectors[1].sector.coefficients = {gamma.coefficients[1], gamma.unoccupied_coefficients[1]}; + + const auto records + = ModuleRI::build_sternheimer_supercell_full_kpoint_records(gamma, sectors); + + ASSERT_EQ(records.size(), 2U); + for (std::size_t ik = 0; ik != records.size(); ++ik) + { + EXPECT_EQ(records[ik].local_k_index, static_cast(ik)); + EXPECT_EQ(records[ik].global_k_index, static_cast(ik)); + EXPECT_EQ(records[ik].zero_order_k_index, static_cast(ik)); + EXPECT_DOUBLE_EQ(records[ik].kweight, 1.0); + EXPECT_EQ(records[ik].kpoint, sectors[ik].kpoint); + EXPECT_EQ(ModuleRI::sternheimer_lcao_grid_kpoint(records[ik]), + (ModuleRI::SternheimerReducedKPoint{0.0, 0.0, 0.0})); + ASSERT_EQ(records[ik].eigenvalues.size(), 1U); + ASSERT_EQ(records[ik].unoccupied_eigenvalues.size(), 1U); + EXPECT_DOUBLE_EQ(records[ik].occupations[0], 1.0); + } + EXPECT_DOUBLE_EQ(records[0].eigenvalues[0], -2.0); + EXPECT_DOUBLE_EQ(records[1].eigenvalues[0], -1.0); +} + +TEST(SternheimerABACUSSTSmoke, LimitsPeriodicOccupiedBandsOnlyWhenRequested) +{ + EXPECT_EQ(ModuleRI::sternheimer_periodic_band_count(8, -1), 8); + EXPECT_EQ(ModuleRI::sternheimer_periodic_band_count(8, 1), 1); + EXPECT_EQ(ModuleRI::sternheimer_periodic_band_count(8, 20), 8); + EXPECT_THROW(ModuleRI::sternheimer_periodic_band_count(0, 1), std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, TreatsSupercellTranslationResponseAsDiagnosticOnly) +{ + EXPECT_TRUE(ModuleRI::sternheimer_write_periodic_v1(false, false)); + EXPECT_FALSE(ModuleRI::sternheimer_write_periodic_v1(true, false)); + EXPECT_FALSE(ModuleRI::sternheimer_write_periodic_v1(false, true)); + EXPECT_TRUE(ModuleRI::sternheimer_write_periodic_v1(false, true, false, true)); + EXPECT_TRUE(ModuleRI::sternheimer_write_periodic_v1(true, false, true)); + EXPECT_FALSE(ModuleRI::sternheimer_write_periodic_v1(true, true, true)); + EXPECT_TRUE(ModuleRI::sternheimer_write_periodic_v1(true, true, true, true)); + + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_periodic_output_mode(true, true)); + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_periodic_output_mode(false, false)); + EXPECT_THROW(ModuleRI::validate_sternheimer_periodic_output_mode(false, true), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, SelectsMassiddaFactorOnlyForPeriodicGamma) +{ + constexpr double massidda_chi = 1.25; + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_periodic_gamma_inverse_k2( + {0.0, 0.0, 0.0}, "massidda", massidda_chi), + massidda_chi); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_periodic_gamma_inverse_k2( + {0.25, 0.0, 0.0}, "massidda", massidda_chi), + 0.0); + EXPECT_THROW(ModuleRI::sternheimer_periodic_gamma_inverse_k2( + {0.0, 0.0, 0.0}, "limits", massidda_chi), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_periodic_gamma_inverse_k2( + {0.0, 0.0, 0.0}, "massidda", 0.0), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, UsesTheTwoDimensionalMassiddaIntegralForTwoDimensionalGamma) +{ + EXPECT_TRUE(ModuleRI::sternheimer_periodic_gamma_uses_2d_massidda({0.0, 0.0, 0.0}, 2)); + EXPECT_FALSE(ModuleRI::sternheimer_periodic_gamma_uses_2d_massidda({0.25, 0.0, 0.0}, 2)); + EXPECT_FALSE(ModuleRI::sternheimer_periodic_gamma_uses_2d_massidda({0.0, 0.0, 0.0}, 3)); + EXPECT_THROW(ModuleRI::sternheimer_periodic_gamma_uses_2d_massidda({0.0, 0.0, 0.0}, 1), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, ValidatesPeriodicMonkhorstPackDimensions) +{ + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_periodic_kmesh({4, 4, 4}, 64)); + EXPECT_THROW(ModuleRI::validate_sternheimer_periodic_kmesh({4, 4, 4}, 63), + std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_sternheimer_periodic_kmesh({4, 0, 4}, 64), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, AssignsContiguousKPointOwners) +{ + const int global_kpoint_count = 10; + const int kpoint_groups = 4; + const std::vector expected_owners = {0, 0, 0, 1, 1, 1, 2, 2, 3, 3}; + + for (int ik = 0; ik != global_kpoint_count; ++ik) + { + EXPECT_EQ(ModuleRI::sternheimer_kpoint_owner_group(ik, global_kpoint_count, kpoint_groups), + expected_owners[static_cast(ik)]); + } + EXPECT_THROW(ModuleRI::sternheimer_kpoint_owner_group(-1, global_kpoint_count, kpoint_groups), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_kpoint_owner_group(0, global_kpoint_count, 0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_kpoint_owner_group(0, 3, 4), std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, SelectsIndependentSupercellKPointGroups) +{ + EXPECT_EQ(ModuleRI::sternheimer_response_kpoint_group_count(false, 1, 4, 64), 4); + EXPECT_EQ(ModuleRI::sternheimer_response_kpoint_group_count(true, 8, 1, 64), 8); + EXPECT_EQ(ModuleRI::sternheimer_response_kpoint_group_count(true, 64, 1, 64), 64); + EXPECT_THROW(ModuleRI::sternheimer_response_kpoint_group_count(true, 0, 1, 64), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_response_kpoint_group_count(true, 65, 1, 64), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, AssignsEachNestedKFrequencyTaskExactlyOnce) +{ + constexpr int global_kpoint_count = 5; + constexpr int kpoint_groups = 2; + constexpr int frequency_count = 2; + constexpr int mpi_ranks = kpoint_groups * frequency_count; + std::vector tasks_by_rank(mpi_ranks, 0); + + for (int ik = 0; ik != global_kpoint_count; ++ik) + { + for (int ifrequency = 0; ifrequency != frequency_count; ++ifrequency) + { + const auto assignment = ModuleRI::sternheimer_nested_mpi_assignment(ik, + global_kpoint_count, + ifrequency, + frequency_count, + kpoint_groups, + mpi_ranks, + 0); + EXPECT_EQ(assignment.kpoint_group, + ModuleRI::sternheimer_kpoint_owner_group(ik, + global_kpoint_count, + kpoint_groups)); + EXPECT_EQ(assignment.frequency_slot, ifrequency); + ASSERT_GE(assignment.owner_rank, 0); + ASSERT_LT(assignment.owner_rank, mpi_ranks); + ++tasks_by_rank[static_cast(assignment.owner_rank)]; + } + } + + EXPECT_EQ(tasks_by_rank, (std::vector{3, 3, 2, 2})); +} + +TEST(SternheimerABACUSSTSmoke, WrapsNestedFrequencyRankShiftWithinEachKGroup) +{ + const auto positive = ModuleRI::sternheimer_nested_mpi_assignment(3, 5, 1, 3, 2, 6, 1); + EXPECT_EQ(positive.kpoint_group, 1); + EXPECT_EQ(positive.frequency_slot, 2); + EXPECT_EQ(positive.owner_rank, 5); + + const auto negative = ModuleRI::sternheimer_nested_mpi_assignment(0, 5, 0, 3, 2, 6, -1); + EXPECT_EQ(negative.kpoint_group, 0); + EXPECT_EQ(negative.frequency_slot, 2); + EXPECT_EQ(negative.owner_rank, 2); +} + +TEST(SternheimerABACUSSTSmoke, BuildsNestedChannelMPIReplicaLayout) +{ + const auto base = ModuleRI::sternheimer_nested_mpi_replica_layout(2, 16, 32, 31, false); + EXPECT_EQ(base.response_slots, 32); + EXPECT_EQ(base.replicas_per_slot, 1); + EXPECT_EQ(base.local_response_slot, 31); + EXPECT_EQ(base.local_replica, 0); + + const auto replicated = ModuleRI::sternheimer_nested_mpi_replica_layout(2, 16, 128, 127, true); + EXPECT_EQ(replicated.response_slots, 32); + EXPECT_EQ(replicated.replicas_per_slot, 4); + EXPECT_EQ(replicated.local_response_slot, 31); + EXPECT_EQ(replicated.local_replica, 3); + + EXPECT_THROW(ModuleRI::sternheimer_nested_mpi_replica_layout(2, 16, 64, 0, false), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_nested_mpi_replica_layout(2, 16, 48, 0, true), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, AssignsChannelBatchesOnceAndBalancesAcrossReplicas) +{ + constexpr int occupied_states = 5; + constexpr int batch_count = 126; + constexpr int replica_count = 8; + std::vector tasks_by_replica(replica_count, 0); + for (int occupied = 0; occupied != occupied_states; ++occupied) + { + for (int batch = 0; batch != batch_count; ++batch) + { + const int owner = ModuleRI::sternheimer_channel_batch_replica_owner( + occupied, batch, batch_count, replica_count); + ASSERT_GE(owner, 0); + ASSERT_LT(owner, replica_count); + ++tasks_by_replica[static_cast(owner)]; + } + } + const auto range = std::minmax_element(tasks_by_replica.begin(), tasks_by_replica.end()); + EXPECT_LE(*range.second - *range.first, 1); + EXPECT_THROW(ModuleRI::sternheimer_channel_batch_replica_owner(0, 126, 126, 8), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, RejectsInvalidNestedMPIContracts) +{ + EXPECT_THROW(ModuleRI::sternheimer_nested_mpi_assignment(0, 4, 0, 2, 2, 3), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_nested_mpi_assignment(-1, 4, 0, 2, 2, 4), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_nested_mpi_assignment(0, 4, -1, 2, 2, 4), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_nested_mpi_assignment(0, 4, 2, 2, 2, 4), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_nested_mpi_assignment(0, 2, 0, 2, 3, 6), + std::invalid_argument); +} + +TEST(SternheimerABACUSSTSmoke, PartitionsResponsePairsBySourceKWithoutOverlap) +{ + std::vector records; + for (int ik = 0; ik != 8; ++ik) + { + records.push_back(make_occupied_kpoint(ik, + ik, + 0, + {static_cast(ik) / 8.0, 0.0, 0.0}, + 0.25)); + } + const auto plan = ModuleRI::build_sternheimer_periodic_response_plan(records, 2); + + std::vector seen(plan.kq_pairs.size(), 0); + for (int group = 0; group != 4; ++group) + { + const auto owned = ModuleRI::sternheimer_owned_kq_pair_indices(plan, group, 4); + ASSERT_EQ(owned.size(), 2); + for (const std::size_t pair_index: owned) + { + ASSERT_LT(pair_index, plan.kq_pairs.size()); + ++seen[pair_index]; + EXPECT_EQ(ModuleRI::sternheimer_kpoint_owner_group(plan.kq_pairs[pair_index].source_index, 8, 4), + group); + } + } + EXPECT_EQ(seen, std::vector(plan.kq_pairs.size(), 1)); +} + +} // namespace diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abfs_perturbation_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abfs_perturbation_test.cpp new file mode 100644 index 00000000000..de8b92f5eab --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abfs_perturbation_test.cpp @@ -0,0 +1,497 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" + +#include + +#include +#include +#include + +TEST(SternheimerABFSPerturbation, SamplesSChannelOnFDGrid) +{ + ModuleRI::SternheimerRadialPerturbation radial; + radial.angular_momentum = 0; + radial.radial_index = 0; + radial.label = "toy_s"; + radial.radial_grid = {0.0, 1.0}; + radial.radial_values = {2.0, 4.0}; + + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 2; + grid.ny = 1; + grid.nz = 1; + grid.hx = 1.0; + grid.hy = 1.0; + grid.hz = 1.0; + grid.periodic = false; + + const std::vector> radials_by_type = {{radial}}; + const std::vector atom_types = {0}; + const std::vector> atom_positions = {ModuleBase::Vector3(0.0, 0.0, 0.0)}; + + const std::vector channels + = ModuleRI::sample_sternheimer_abf_grid_channels(radials_by_type, atom_types, atom_positions, grid, 1); + + ASSERT_EQ(channels.size(), 1); + EXPECT_EQ(channels[0].atom_index, 0); + EXPECT_EQ(channels[0].atom_local_index, 0); + EXPECT_EQ(channels[0].angular_momentum, 0); + EXPECT_EQ(channels[0].magnetic_index, 0); + EXPECT_EQ(channels[0].label, "toy_s"); + ASSERT_EQ(channels[0].potential_r.size(), 2); + + constexpr double y00 = 0.28209479177387814347; + EXPECT_NEAR(channels[0].potential_r[0], 2.0 * y00, 1.0e-14); + EXPECT_NEAR(channels[0].potential_r[1], 4.0 * y00, 1.0e-14); + EXPECT_NEAR(channels[0].max_abs, 4.0 * y00, 1.0e-14); +} + +TEST(SternheimerABFSPerturbation, StreamsRawChannelsIntoLinearCombinations) +{ + ModuleRI::SternheimerRadialPerturbation radial0; + radial0.angular_momentum = 0; + radial0.radial_index = 0; + radial0.label = "toy_s0"; + radial0.radial_grid = {0.0, 1.0}; + radial0.radial_values = {2.0, 4.0}; + ModuleRI::SternheimerRadialPerturbation radial1 = radial0; + radial1.radial_index = 1; + radial1.label = "toy_s1"; + radial1.radial_values = {-1.0, 3.0}; + ModuleRI::SternheimerRadialPerturbation radial2 = radial0; + radial2.angular_momentum = 1; + radial2.radial_index = 0; + radial2.label = "toy_p0"; + radial2.radial_values = {0.5, -2.0}; + + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 1025; + grid.ny = 1; + grid.nz = 1; + grid.hx = 0.001; + grid.hy = 1.0; + grid.hz = 1.0; + grid.periodic = false; + + const std::vector> radials_by_type + = {{radial0, radial1, radial2}}; + const std::vector atom_types = {0}; + const std::vector> atom_positions + = {ModuleBase::Vector3(0.0, 0.0, 0.0)}; + const auto dense + = ModuleRI::sample_sternheimer_abf_grid_channels(radials_by_type, atom_types, atom_positions, grid); + auto metadata + = ModuleRI::describe_sternheimer_abf_grid_channels(radials_by_type, atom_types, atom_positions); + + ASSERT_EQ(metadata.size(), dense.size()); + ASSERT_EQ(metadata.size(), 5); + EXPECT_TRUE(metadata[0].potential_r.empty()); + EXPECT_TRUE(metadata[1].potential_r.empty()); + EXPECT_EQ(metadata[0].label, dense[0].label); + EXPECT_EQ(metadata[1].channel_index, dense[1].channel_index); + + // Row-major raw-to-output transform: two s and three p channels form three outputs. + const std::vector transform = { + 1.0, 0.5, -2.0, + 3.0, -1.0, 0.25, + 0.7, -0.2, 0.4, + -0.3, 1.2, 0.8, + 0.1, 0.6, -0.9, + }; + const auto streamed = ModuleRI::sample_sternheimer_abf_grid_channel_transform(radials_by_type, + atom_types, + atom_positions, + grid, + metadata, + transform, + 3); + ASSERT_EQ(streamed.size(), 3); + ASSERT_EQ(dense[0].potential_r.size(), 1025); + for (std::size_t raw = 0; raw != dense.size(); ++raw) + { + EXPECT_NEAR(metadata[raw].max_abs, dense[raw].max_abs, 1.0e-14); + } + for (int output = 0; output != 3; ++output) + { + ASSERT_EQ(streamed[static_cast(output)].size(), dense[0].potential_r.size()); + for (std::size_t ir = 0; ir != dense[0].potential_r.size(); ++ir) + { + double expected = 0.0; + for (std::size_t raw = 0; raw != dense.size(); ++raw) + { + expected += dense[raw].potential_r[ir] + * transform[raw * 3 + static_cast(output)]; + } + EXPECT_NEAR(streamed[static_cast(output)][ir], expected, 1.0e-14); + } + } +} + +TEST(SternheimerABFSPerturbation, ReadsABFSDiagnosticOnlyEnvironmentFlag) +{ + unsetenv("ABACUS_STERNHEIMER_FD_ST_ABFS_DIAG_ONLY"); + EXPECT_FALSE(ModuleRI::sternheimer_abfs_diag_only_enabled()); + + setenv("ABACUS_STERNHEIMER_FD_ST_ABFS_DIAG_ONLY", "1", 1); + EXPECT_TRUE(ModuleRI::sternheimer_abfs_diag_only_enabled()); + + setenv("ABACUS_STERNHEIMER_FD_ST_ABFS_DIAG_ONLY", "false", 1); + EXPECT_FALSE(ModuleRI::sternheimer_abfs_diag_only_enabled()); + + unsetenv("ABACUS_STERNHEIMER_FD_ST_ABFS_DIAG_ONLY"); +} + +TEST(SternheimerABFSPerturbation, RequiresExplicitOptInForLargeGridCoulombDiagnostic) +{ + unsetenv("ABACUS_STERNHEIMER_GRID_COULOMB_DIAG"); + EXPECT_TRUE(ModuleRI::sternheimer_grid_coulomb_diagnostic_enabled(32)); + EXPECT_FALSE(ModuleRI::sternheimer_grid_coulomb_diagnostic_enabled(33)); + + setenv("ABACUS_STERNHEIMER_GRID_COULOMB_DIAG", "1", 1); + EXPECT_TRUE(ModuleRI::sternheimer_grid_coulomb_diagnostic_enabled(244)); + + setenv("ABACUS_STERNHEIMER_GRID_COULOMB_DIAG", "false", 1); + EXPECT_FALSE(ModuleRI::sternheimer_grid_coulomb_diagnostic_enabled(244)); + unsetenv("ABACUS_STERNHEIMER_GRID_COULOMB_DIAG"); +} + +TEST(SternheimerABFSPerturbation, BlochPotentialUsesQPhaseAndGammaMatchesRealSampler) +{ + ModuleRI::SternheimerRadialPerturbation radial; + radial.angular_momentum = 0; + radial.radial_index = 0; + radial.label = "toy_s"; + radial.radial_grid = {0.0, 0.5, 1.0}; + radial.radial_values = {0.0, 1.0, 2.0}; + + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 3; + grid.ny = 1; + grid.nz = 1; + grid.hx = 1.0; + grid.hy = 10.0; + grid.hz = 10.0; + grid.periodic = true; + + const std::vector> radials_by_type = {{radial}}; + const std::vector atom_types = {0}; + const std::vector> atom_positions + = {ModuleBase::Vector3(0.0, 0.0, 0.0)}; + const auto bloch = ModuleRI::sample_sternheimer_abf_bloch_grid_channels( + radials_by_type, atom_types, atom_positions, grid, {0.25, 0.0, 0.0}, 1); + + constexpr double y00 = 0.28209479177387814347; + ASSERT_EQ(bloch.size(), 1); + ASSERT_EQ(bloch[0].potential_r.size(), 3); + EXPECT_NEAR(bloch[0].potential_r[2].real(), 0.0, 1.0e-14); + EXPECT_NEAR(bloch[0].potential_r[2].imag(), 2.0 * y00, 1.0e-14); + + const auto gamma_complex = ModuleRI::sample_sternheimer_abf_bloch_grid_channels( + radials_by_type, atom_types, atom_positions, grid, {0.0, 0.0, 0.0}, 1); + const auto gamma_real + = ModuleRI::sample_sternheimer_abf_grid_channels(radials_by_type, atom_types, atom_positions, grid, 1); + ASSERT_EQ(gamma_complex.size(), gamma_real.size()); + for (std::size_t ir = 0; ir != gamma_real[0].potential_r.size(); ++ir) + { + EXPECT_NEAR(gamma_complex[0].potential_r[ir].real(), gamma_real[0].potential_r[ir], 1.0e-14); + EXPECT_NEAR(gamma_complex[0].potential_r[ir].imag(), 0.0, 1.0e-14); + } +} + +TEST(SternheimerABFSPerturbation, PeriodicPoissonAppliesShiftedFullCoulombKernel) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 4; + grid.ny = 2; + grid.nz = 2; + grid.hx = 0.5 * M_PI; + grid.hy = M_PI; + grid.hz = M_PI; + grid.periodic = true; + + ModuleRI::SternheimerABFBlochGridChannel density; + density.channel_index = 0; + density.atom_index = 0; + density.atom_local_index = 0; + density.angular_momentum = 0; + density.label = "plane_wave"; + const std::size_t grid_size + = static_cast(grid.nx) * static_cast(grid.ny) + * static_cast(grid.nz); + density.potential_r.resize(grid_size); + + const ModuleRI::SternheimerReducedKPoint qpoint{0.5, 0.0, 0.0}; + for (int ix = 0; ix != grid.nx; ++ix) + { + const double x = ix * grid.hx; + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const std::size_t ir + = static_cast((ix * grid.ny + iy) * grid.nz + iz); + density.potential_r[ir] = std::exp(std::complex(0.0, 1.5 * x)); + } + } + } + + const auto potentials + = ModuleRI::solve_sternheimer_abf_periodic_full_coulomb({density}, grid, qpoint, 0.0); + ASSERT_EQ(potentials.size(), 1); + ASSERT_EQ(potentials[0].potential_r.size(), density.potential_r.size()); + const double expected_factor = 4.0 * M_PI / (1.5 * 1.5); + for (std::size_t ir = 0; ir != density.potential_r.size(); ++ir) + { + EXPECT_NEAR(potentials[0].potential_r[ir].real(), + expected_factor * density.potential_r[ir].real(), + 1.0e-12); + EXPECT_NEAR(potentials[0].potential_r[ir].imag(), + expected_factor * density.potential_r[ir].imag(), + 1.0e-12); + } +} + +TEST(SternheimerABFSPerturbation, PeriodicPoissonInPlaceMatchesCopyingInterface) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 4; + grid.ny = 2; + grid.nz = 2; + grid.hx = 0.5 * M_PI; + grid.hy = M_PI; + grid.hz = M_PI; + grid.periodic = true; + + ModuleRI::SternheimerABFBlochGridChannel density; + density.channel_index = 7; + density.atom_index = 2; + density.label = "in_place_plane_wave"; + density.potential_r.resize(static_cast(grid.nx * grid.ny * grid.nz)); + for (int ix = 0; ix != grid.nx; ++ix) + { + const double x = ix * grid.hx; + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const std::size_t ir = static_cast((ix * grid.ny + iy) * grid.nz + iz); + density.potential_r[ir] = std::exp(std::complex(0.0, 1.5 * x)); + } + } + } + + const ModuleRI::SternheimerReducedKPoint qpoint{0.5, 0.0, 0.0}; + const auto expected = ModuleRI::solve_sternheimer_abf_periodic_full_coulomb({density}, grid, qpoint, 0.0); + std::vector in_place{density}; + ModuleRI::solve_sternheimer_abf_periodic_full_coulomb_in_place(in_place, grid, qpoint, 0.0); + + ASSERT_EQ(in_place.size(), expected.size()); + EXPECT_EQ(in_place[0].channel_index, density.channel_index); + EXPECT_EQ(in_place[0].atom_index, density.atom_index); + EXPECT_EQ(in_place[0].label, density.label); + ASSERT_EQ(in_place[0].potential_r.size(), expected[0].potential_r.size()); + for (std::size_t ir = 0; ir != expected[0].potential_r.size(); ++ir) + { + EXPECT_NEAR(in_place[0].potential_r[ir].real(), expected[0].potential_r[ir].real(), 1.0e-12); + EXPECT_NEAR(in_place[0].potential_r[ir].imag(), expected[0].potential_r[ir].imag(), 1.0e-12); + } +} + +TEST(SternheimerABFSPerturbation, NonOrthogonalSamplingUsesCartesianLatticeCombination) +{ + ModuleRI::SternheimerRadialPerturbation radial; + radial.angular_momentum = 0; + radial.radial_index = 0; + radial.label = "linear_s"; + radial.radial_grid = {0.0, 1.0}; + radial.radial_values = {0.0, 1.0}; + + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 4; + grid.ny = 4; + grid.nz = 4; + grid.hx = std::sqrt(2.0) / 4.0; + grid.hy = std::sqrt(2.0) / 4.0; + grid.hz = std::sqrt(2.0) / 4.0; + grid.periodic = false; + grid.lattice_vectors = {{{0.0, 1.0, 1.0}, + {1.0, 0.0, 1.0}, + {1.0, 1.0, 0.0}}}; + + const std::vector> radials_by_type = {{radial}}; + const std::vector atom_types = {0}; + const std::vector> atom_positions + = {ModuleBase::Vector3(0.0, 0.0, 0.0)}; + const auto channels + = ModuleRI::sample_sternheimer_abf_grid_channels(radials_by_type, atom_types, atom_positions, grid, 1); + + ASSERT_EQ(channels.size(), 1); + const std::size_t ir = static_cast((1 * grid.ny + 1) * grid.nz); + constexpr double y00 = 0.28209479177387814347; + EXPECT_NEAR(channels[0].potential_r[ir], std::sqrt(0.375) * y00, 1.0e-14); +} + +TEST(SternheimerABFSPerturbation, NonOrthogonalPeriodicPoissonUsesReciprocalMetric) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 4; + grid.ny = 4; + grid.nz = 4; + grid.hx = std::sqrt(2.0) / 4.0; + grid.hy = std::sqrt(2.0) / 4.0; + grid.hz = std::sqrt(2.0) / 4.0; + grid.periodic = true; + grid.lattice_vectors = {{{0.0, 1.0, 1.0}, + {1.0, 0.0, 1.0}, + {1.0, 1.0, 0.0}}}; + + ModuleRI::SternheimerABFBlochGridChannel density; + density.channel_index = 0; + density.atom_index = 0; + density.atom_local_index = 0; + density.angular_momentum = 0; + density.label = "skew_plane_wave"; + density.potential_r.resize(static_cast(grid.nx * grid.ny * grid.nz)); + + const ModuleRI::SternheimerReducedKPoint qpoint{0.25, 0.0, 0.0}; + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const double reduced_phase + = 1.25 * static_cast(ix) / grid.nx + + static_cast(iy) / grid.ny; + const std::size_t ir = static_cast((ix * grid.ny + iy) * grid.nz + iz); + density.potential_r[ir] + = std::exp(std::complex(0.0, 2.0 * M_PI * reduced_phase)); + } + } + } + + const auto potentials + = ModuleRI::solve_sternheimer_abf_periodic_full_coulomb({density}, grid, qpoint, 0.0); + ASSERT_EQ(potentials.size(), 1); + const double wavevector_squared = 4.0 * M_PI * M_PI * 1.296875; + const double expected_factor = 4.0 * M_PI / wavevector_squared; + for (std::size_t ir = 0; ir != density.potential_r.size(); ++ir) + { + EXPECT_NEAR(potentials[0].potential_r[ir].real(), + expected_factor * density.potential_r[ir].real(), + 1.0e-12); + EXPECT_NEAR(potentials[0].potential_r[ir].imag(), + expected_factor * density.potential_r[ir].imag(), + 1.0e-12); + } +} + +TEST(SternheimerABFSPerturbation, PeriodicPoissonAtGammaPreservesNonzeroFourierModes) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 4; + grid.ny = 1; + grid.nz = 1; + grid.hx = 1.0; + grid.hy = 1.0; + grid.hz = 1.0; + grid.periodic = true; + + ModuleRI::SternheimerABFBlochGridChannel density; + const std::size_t grid_size + = static_cast(grid.nx) * static_cast(grid.ny) + * static_cast(grid.nz); + density.potential_r.resize(grid_size); + for (int ix = 0; ix != grid.nx; ++ix) + { + density.potential_r[static_cast(ix)] + = {std::cos(2.0 * M_PI * static_cast(ix) / grid.nx), 0.0}; + } + + const auto potentials = ModuleRI::solve_sternheimer_abf_periodic_full_coulomb( + {density}, grid, {0.0, 0.0, 0.0}, 0.0); + + ASSERT_EQ(potentials.size(), 1); + const double wavevector_squared = M_PI * M_PI / 4.0; + const double expected_factor = 4.0 * M_PI / wavevector_squared; + for (std::size_t ir = 0; ir != density.potential_r.size(); ++ir) + { + EXPECT_NEAR(potentials[0].potential_r[ir].real(), + expected_factor * density.potential_r[ir].real(), + 1.0e-12); + EXPECT_NEAR(potentials[0].potential_r[ir].imag(), 0.0, 1.0e-12); + } +} + +TEST(SternheimerABFSPerturbation, PeriodicPoissonAtGammaUsesSuppliedMassiddaZeroMode) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 2; + grid.ny = 2; + grid.nz = 2; + grid.hx = 1.0; + grid.hy = 1.0; + grid.hz = 1.0; + grid.periodic = true; + + ModuleRI::SternheimerABFBlochGridChannel density; + const std::size_t grid_size + = static_cast(grid.nx) * static_cast(grid.ny) + * static_cast(grid.nz); + density.potential_r.assign(grid_size, {1.0, 0.0}); + + constexpr double gamma_inverse_k2 = 0.75; + const auto potentials = ModuleRI::solve_sternheimer_abf_periodic_full_coulomb( + {density}, grid, {0.0, 0.0, 0.0}, gamma_inverse_k2); + + ASSERT_EQ(potentials.size(), 1); + const double expected = 4.0 * M_PI * gamma_inverse_k2; + for (const std::complex& value: potentials[0].potential_r) + { + EXPECT_NEAR(value.real(), expected, 1.0e-12); + EXPECT_NEAR(value.imag(), 0.0, 1.0e-12); + } +} + +TEST(SternheimerABFSPerturbation, PeriodicPoissonRejectsInvalidGammaFactors) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 2; + grid.ny = 2; + grid.nz = 2; + grid.hx = 1.0; + grid.hy = 1.0; + grid.hz = 1.0; + grid.periodic = true; + + ModuleRI::SternheimerABFBlochGridChannel density; + density.potential_r.assign(8, {1.0, 0.0}); + + EXPECT_THROW(ModuleRI::solve_sternheimer_abf_periodic_full_coulomb( + {density}, grid, {0.0, 0.0, 0.0}, -1.0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::solve_sternheimer_abf_periodic_full_coulomb( + {density}, grid, {0.5, 0.0, 0.0}, 0.75), + std::invalid_argument); +} + +TEST(SternheimerABFSPerturbation, CoulombProjectionMismatchIsDiagnosticOnly) +{ + ModuleRI::SternheimerABFBlochGridChannel density0; + density0.potential_r = {{1.0, 0.0}, {0.0, 0.0}}; + ModuleRI::SternheimerABFBlochGridChannel density1; + density1.potential_r = {{1.0, 0.0}, {1.0, 0.0}}; + ModuleRI::SternheimerABFBlochGridChannel potential0 = density0; + ModuleRI::SternheimerABFBlochGridChannel potential1 = density1; + potential0.potential_r.assign(2, {0.0, 0.0}); + potential1.potential_r.assign(2, {0.0, 0.0}); + const std::vector> target{ + {2.0, 0.0}, {0.5, 0.0}, {0.5, 0.0}, {1.0, 0.0}}; + + const auto result = ModuleRI::compare_sternheimer_periodic_coulomb_projection( + {density0, density1}, {potential0, potential1}, target, 1.0); + + EXPECT_NEAR(result.relative_error, 1.0, 1.0e-14); + EXPECT_DOUBLE_EQ(potential0.potential_r[0].real(), 0.0); + EXPECT_DOUBLE_EQ(potential1.potential_r[1].real(), 0.0); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abfs_strict2d_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abfs_strict2d_test.cpp new file mode 100644 index 00000000000..58926e811cd --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_abfs_strict2d_test.cpp @@ -0,0 +1,612 @@ +#include + +#if __has_include("source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.h") +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_strict2d.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +using Complex = std::complex; +using Grid = ModuleRI::SternheimerFDHamiltonian::Grid; +using Channel = ModuleRI::SternheimerABFBlochGridChannel; +using QPoint = ModuleRI::SternheimerReducedKPoint; +constexpr double two_pi = 6.283185307179586476925286766559; + +Grid skew_grid(const int nx = 4, const int ny = 3, const int nz = 5) +{ + Grid grid; + grid.nx = nx; + grid.ny = ny; + grid.nz = nz; + grid.lattice_vectors = {{{4.2, 0.8, 0.0}, {1.1, 3.7, 0.0}, {0.0, 0.0, 6.5}}}; + grid.hx = std::hypot(4.2, 0.8) / nx; + grid.hy = std::hypot(1.1, 3.7) / ny; + grid.hz = 6.5 / nz; + return grid; +} + +int index(const Grid& grid, const int x, const int y, const int z) +{ + return (x * grid.ny + y) * grid.nz + z; +} + +// Independent Cartesian 2x2 inverse, not the production grid-dual helper. +double wave_number(const Grid& grid, const double gx, const double gy) +{ + const auto& a = grid.lattice_vectors[0]; + const auto& b = grid.lattice_vectors[1]; + const double determinant = a[0] * b[1] - a[1] * b[0]; + return two_pi * std::hypot((b[1] * gx - a[1] * gy) / determinant, + (a[0] * gy - b[0] * gx) / determinant); +} + +Channel density(const Grid& grid, const int seed) +{ + Channel channel; + channel.channel_index = seed; + channel.atom_index = 7; + channel.label = "complex_fine_grid_density"; + channel.max_abs = -1.0; + channel.potential_r.resize(grid.nx * grid.ny * grid.nz); + for (std::size_t i = 0; i < channel.potential_r.size(); ++i) + { + const double t = static_cast(i + 1); + channel.potential_r[i] = Complex(std::sin((0.31 + seed * 0.17) * t), + std::cos((0.47 + seed * 0.11) * t)); + } + return channel; +} + +// Gauss-Legendre quadrature on [0,1], independent of the production operator. +std::vector> projection_quadrature() +{ + constexpr int order = 64; + std::vector> result; + for (int j = 0; j < order; ++j) + { + double x = std::cos(two_pi / 2.0 * (j + 0.75) / (order + 0.5)); + double derivative = 0.0; + for (int iteration = 0; iteration < 30; ++iteration) + { + double previous = 1.0, current = x; + for (int n = 2; n <= order; ++n) + { + const double next = ((2 * n - 1) * x * current - (n - 1) * previous) / n; + previous = current; + current = next; + } + derivative = order * (x * current - previous) / (x * x - 1.0); + const double step = current / derivative; + x -= step; + if (std::abs(step) < 1.0e-15) break; + } + result.emplace_back((x + 1.0) / 2.0, 1.0 / ((1.0 - x * x) * derivative * derivative)); + } + return result; +} + +// Independently integrate the continuous Green function against exp(ik_n*z') +// by splitting the source integral at z, then numerically project in target z. +// No ODE boundary amplitudes, production Fourier factors or sampled cusp sum. +std::vector continuous_green_matrix(const int nz, const double length, const double Q) +{ + std::vector matrix(nz * nz, Complex{}); + for (const auto& node: projection_quadrature()) + { + const double z = length * node.first; + for (int n = 0; n < nz; ++n) + { + const int sn = n <= (nz - 1) / 2 ? n : n - nz; + const double kn = two_pi * sn / length; + const Complex oscillation = std::exp(Complex(0.0, kn * z)); + const Complex green = two_pi / Q + * ((oscillation - std::exp(-Q * z)) / Complex(Q, kn) + + (oscillation - std::exp(-Q * (length - z))) / Complex(Q, -kn)); + for (int m = 0; m < nz; ++m) + { + const int sm = m <= (nz - 1) / 2 ? m : m - nz; + matrix[m * nz + n] += node.second * std::exp(Complex(0.0, -two_pi * sm * node.first)) * green; + } + } + } + return matrix; +} + +// Explicit planar Fourier projection of FULL Bloch fields plus continuous-z +// Green integration and Fourier projection, without FFTW or grid dual helpers. +std::vector direct_potential(const Channel& rho, const Grid& grid, const QPoint& q) +{ + std::vector result(rho.potential_r.size(), Complex{}); + const double length = std::abs(grid.lattice_vectors[2][2]); + for (int mx = -grid.nx / 2; mx <= (grid.nx - 1) / 2; ++mx) + { + for (int my = -grid.ny / 2; my <= (grid.ny - 1) / 2; ++my) + { + const double Q = wave_number(grid, mx + q[0], my + q[1]); + std::vector coefficients(grid.nz, Complex{}); + for (int z = 0; z < grid.nz; ++z) + { + for (int x = 0; x < grid.nx; ++x) + { + for (int y = 0; y < grid.ny; ++y) + { + const double angle = two_pi * ((mx + q[0]) * x / grid.nx + + (my + q[1]) * y / grid.ny); + coefficients[z] += rho.potential_r[index(grid, x, y, z)] + * std::exp(Complex(0.0, -angle)) / double(grid.nx * grid.ny); + } + } + } + std::vector rho_modes(grid.nz, Complex{}), potential_modes(grid.nz, Complex{}); + for (int n = 0; n < grid.nz; ++n) + { + for (int z = 0; z < grid.nz; ++z) + { + rho_modes[n] += coefficients[z] * std::exp(Complex(0.0, -two_pi * n * z / grid.nz)) + / double(grid.nz); + } + } + const auto green = continuous_green_matrix(grid.nz, length, Q); + for (int m = 0; m < grid.nz; ++m) + for (int n = 0; n < grid.nz; ++n) + potential_modes[m] += green[m * grid.nz + n] * rho_modes[n]; + for (int z = 0; z < grid.nz; ++z) + { + Complex potential{}; + for (int m = 0; m < grid.nz; ++m) + potential += potential_modes[m] * std::exp(Complex(0.0, two_pi * m * z / grid.nz)); + for (int x = 0; x < grid.nx; ++x) + { + for (int y = 0; y < grid.ny; ++y) + { + const double angle = two_pi * ((mx + q[0]) * x / grid.nx + + (my + q[1]) * y / grid.ny); + result[index(grid, x, y, z)] += potential * std::exp(Complex(0.0, angle)); + } + } + } + } + } + return result; +} + +Complex inner_product(const Channel& a, const Channel& b, const double dv) +{ + Complex result{}; + for (std::size_t i = 0; i < a.potential_r.size(); ++i) + { + result += std::conj(a.potential_r[i]) * b.potential_r[i] * dv; + } + return result; +} +} // namespace + +TEST(SternheimerABFSStrict2D, MatchesContinuousGreenGalerkinOnSkewOddEvenGrids) +{ + for (const Grid grid: {skew_grid(), skew_grid(3, 4, 1), skew_grid(1, 1, 3), skew_grid(3, 2, 4)}) + { + // Includes negative q, a zone edge, and an unfolded reciprocal shift. + for (const QPoint q: {QPoint{0.0, 0.25, 0.0}, QPoint{0.5, -0.23, 0.0}, QPoint{1.17, -1.21, 0.0}}) + { + std::vector channels{density(grid, 0), density(grid, 1)}; + const auto original = channels; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(channels, grid, q); + for (std::size_t c = 0; c < channels.size(); ++c) + { + const auto expected = direct_potential(original[c], grid, q); + double max_abs = 0.0; + for (std::size_t i = 0; i < expected.size(); ++i) + { + EXPECT_NEAR(std::abs(channels[c].potential_r[i] - expected[i]), 0.0, + 3.0e-12 * std::max(1.0, std::abs(expected[i]))); + max_abs = std::max(max_abs, std::abs(expected[i])); + } + EXPECT_NEAR(channels[c].max_abs, max_abs, 3.0e-12 * std::max(1.0, max_abs)); + EXPECT_EQ(channels[c].channel_index, original[c].channel_index); + EXPECT_EQ(channels[c].atom_index, original[c].atom_index); + EXPECT_EQ(channels[c].label, original[c].label); + } + } + } +} + +TEST(SternheimerABFSStrict2D, IsHermitianPositiveAndComplexLinear) +{ + const Grid grid = skew_grid(); + const QPoint q{0.13, -0.21, 0.0}; + const Complex alpha(0.7, -0.3), beta(-0.4, 0.8); + std::vector rho{density(grid, 0), density(grid, 1), density(grid, 2)}; + for (std::size_t i = 0; i < rho[0].potential_r.size(); ++i) + { + rho[2].potential_r[i] = alpha * rho[0].potential_r[i] + beta * rho[1].potential_r[i]; + } + auto phi = rho; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, q); + const Complex ab = inner_product(rho[0], phi[1], 1.0); + const Complex ba = inner_product(rho[1], phi[0], 1.0); + EXPECT_NEAR(std::abs(ab - std::conj(ba)), 0.0, 2.0e-11); + for (std::size_t c = 0; c < rho.size(); ++c) + { + const Complex norm = inner_product(rho[c], phi[c], 1.0); + EXPECT_GT(norm.real(), 0.0); + EXPECT_NEAR(norm.imag(), 0.0, 2.0e-11); + } + for (std::size_t i = 0; i < rho[0].potential_r.size(); ++i) + { + EXPECT_NEAR(std::abs(phi[2].potential_r[i] - alpha * phi[0].potential_r[i] + - beta * phi[1].potential_r[i]), 0.0, 2.0e-12); + } +} + +TEST(SternheimerABFSStrict2D, ImplicitOrthogonalCellMatchesExplicitCell) +{ + Grid implicit_grid = skew_grid(); + implicit_grid.lattice_vectors = {}; + Grid explicit_grid = implicit_grid; + explicit_grid.lattice_vectors = {{{implicit_grid.nx * implicit_grid.hx, 0.0, 0.0}, + {0.0, implicit_grid.ny * implicit_grid.hy, 0.0}, + {0.0, 0.0, implicit_grid.nz * implicit_grid.hz}}}; + const QPoint q{0.0, 0.25, 0.0}; + const Channel rho = density(implicit_grid, 0); + const auto expected = direct_potential(rho, explicit_grid, q); + std::vector phi{rho}; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, implicit_grid, q); + for (std::size_t i = 0; i < expected.size(); ++i) + { + EXPECT_NEAR(std::abs(phi[0].potential_r[i] - expected[i]), 0.0, 3.0e-12); + } + std::vector empty; + EXPECT_NO_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(empty, implicit_grid, q)); + EXPECT_TRUE(empty.empty()); + EXPECT_TRUE(ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(empty, empty, implicit_grid, {}).empty()); +} + +TEST(SternheimerABFSStrict2D, GalerkinConstantModeMatchesContinuousGreenAverage) +{ + // RED against 40f3967ca: rectangle self interaction overestimates this + // exact continuous constant-mode integral even on the single-z grid. + for (const int nz: {1, 3, 4, 7}) + { + const Grid grid = skew_grid(1, 1, nz); + const QPoint q{0.12, -0.16, 0.0}; + const double Q = wave_number(grid, q[0], q[1]); + const double length = 6.5; + const double periodic = 2.0 * two_pi / (Q * Q); + const double expected = periodic * (1.0 + std::expm1(-Q * length) / (Q * length)); + Channel rho; + rho.potential_r.assign(nz, Complex(1.0, 0.0)); + std::vector phi{rho}; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, q); + const Complex average = inner_product(rho, phi[0], 1.0 / nz); + EXPECT_NEAR(average.real(), expected, 3.0e-12); + EXPECT_NEAR(average.imag(), 0.0, 3.0e-12); + EXPECT_LT(average.real(), periodic); // No periodic-z images. + } +} + +TEST(SternheimerABFSStrict2D, ProjectsBoundaryExponentialsRatherThanSamplingThem) +{ + const Grid grid = skew_grid(1, 1, 3); + const QPoint q{0.12, -0.16, 0.0}; + const double Q = wave_number(grid, q[0], q[1]); + Channel rho; + rho.potential_r.assign(grid.nz, Complex(1.0, 0.0)); + const auto reference = direct_potential(rho, grid, q); + std::vector phi{rho}; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, q); + for (int z = 0; z < grid.nz; ++z) + EXPECT_NEAR(std::abs(phi[0].potential_r[z] - reference[z]), 0.0, 3.0e-12); + const double unprojected_at_zero = two_pi / (Q * Q) * (-std::expm1(-Q * 6.5)); + EXPECT_GT(std::abs(phi[0].potential_r[0] - unprojected_at_zero), 1.0e-2); +} + +TEST(SternheimerABFSStrict2D, RetainsComplexNegativeZNyquistMode) +{ + const Grid grid = skew_grid(1, 1, 4); + const QPoint q{0.12, -0.16, 0.0}; + Channel rho; + for (int z = 0; z < grid.nz; ++z) + rho.potential_r.push_back(Complex(0.4, 1.1) * (z % 2 == 0 ? 1.0 : -1.0)); + const auto reference = direct_potential(rho, grid, q); + std::vector phi{rho}; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, q); + for (int z = 0; z < grid.nz; ++z) + EXPECT_NEAR(std::abs(phi[0].potential_r[z] - reference[z]), 0.0, 3.0e-12); + const Complex energy = inner_product(rho, phi[0], 1.0); + EXPECT_GT(energy.real(), 0.0); + EXPECT_NEAR(energy.imag(), 0.0, 3.0e-12); +} + +TEST(SternheimerABFSStrict2D, StableSmallQConstantMode) +{ + const Grid grid = skew_grid(1, 1, 1); + const QPoint q{1.0e-9, 0.0, 0.0}; + const double Q = wave_number(grid, q[0], q[1]); + const double length = 6.5; + const double x = Q * length; + const double expected = two_pi * length / Q * (1.0 - x / 3.0 + x * x / 12.0); + Channel rho; + rho.potential_r = {Complex(1.0, 0.0)}; + std::vector phi{rho}; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, q); + EXPECT_NEAR(phi[0].potential_r[0].real() / expected, 1.0, 2.0e-14); + EXPECT_EQ(phi[0].potential_r[0].imag(), 0.0); +} + +TEST(SternheimerABFSStrict2D, GammaFinitePartMatchesConstantDensityAnalyticLimit) +{ + const Grid grid = skew_grid(1, 1, 1); + const double length = 6.5; + Channel rho; + rho.potential_r = {Complex(1.0, 0.0)}; + std::vector phi{rho}; + + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_finite_part_in_place( + phi, grid, QPoint{0.0, 0.0, 0.0}); + + EXPECT_NEAR(phi[0].potential_r[0].real(), -two_pi * length * length / 3.0, + 2.0e-12); + EXPECT_EQ(phi[0].potential_r[0].imag(), 0.0); +} + +TEST(SternheimerABFSStrict2D, GammaFinitePartIsSmallQLimitAfterRemovingSingularConstant) +{ + const Grid grid = skew_grid(1, 1, 4); + const QPoint small_q{1.0e-6, 0.0, 0.0}; + const double Q = wave_number(grid, small_q[0], small_q[1]); + const double length = 6.5; + Channel rho; + rho.potential_r = {{0.8, 0.2}, {-0.1, 0.4}, {0.5, -0.3}, {0.2, 0.1}}; + const Complex average = std::accumulate(rho.potential_r.begin(), rho.potential_r.end(), + Complex{}) / static_cast(grid.nz); + + std::vector gamma{rho}; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_finite_part_in_place( + gamma, grid, QPoint{0.0, 0.0, 0.0}); + std::vector finite_q{rho}; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(finite_q, grid, small_q); + + const Complex singular = two_pi * length * average / Q; + double reference_scale = 1.0; + double max_error = 0.0; + for (int iz = 0; iz < grid.nz; ++iz) + { + reference_scale = std::max(reference_scale, std::abs(gamma[0].potential_r[iz])); + max_error = std::max(max_error, + std::abs(finite_q[0].potential_r[iz] - singular + - gamma[0].potential_r[iz])); + } + EXPECT_LT(max_error / reference_scale, 2.0e-5); +} + +TEST(SternheimerABFSStrict2D, GammaFinitePartRejectsNonGammaInputBeforeMutation) +{ + const Grid grid = skew_grid(); + const Channel original = density(grid, 0); + std::vector channels{original}; + + for (const QPoint q : {QPoint{0.0, 0.25, 0.0}, QPoint{1.0, -1.0, 0.0}, + QPoint{1.0e-15, 0.0, 0.0}}) + { + EXPECT_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_finite_part_in_place( + channels, grid, q), + std::invalid_argument); + EXPECT_EQ(channels[0].potential_r, original.potential_r); + } +} + +TEST(SternheimerABFSStrict2D, SelectedIntegralsUseCellVolumeAndRequestedOrder) +{ + const Grid grid = skew_grid(); + const QPoint q{0.0, 0.25, 0.0}; + const std::vector rho{density(grid, 0), density(grid, 1)}; + auto phi = rho; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, q); + const std::vector> pairs{{1, 0}, {0, 0}, {0, 1}, {1, 0}}; + const auto selected = ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(rho, phi, grid, pairs); + const double dv = (4.2 * 3.7 - 0.8 * 1.1) * 6.5 / (grid.nx * grid.ny * grid.nz); + ASSERT_EQ(selected.size(), pairs.size()); + for (std::size_t i = 0; i < pairs.size(); ++i) + { + EXPECT_NEAR(std::abs(selected[i] - inner_product(rho[pairs[i].first], phi[pairs[i].second], dv)), + 0.0, 2.0e-11); + } + EXPECT_NEAR(std::abs(selected[0] - std::conj(selected[2])), 0.0, 2.0e-11); + EXPECT_THROW(ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(rho, phi, grid, {{2, 0}}), + std::invalid_argument); + auto sparse_phi = phi; + sparse_phi[1].potential_r.clear(); + EXPECT_NO_THROW(ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(rho, sparse_phi, grid, {{1, 0}})); + EXPECT_THROW(ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(rho, sparse_phi, grid, {{0, 1}}), + std::invalid_argument); +} + +TEST(SternheimerABFSStrict2D, RejectsGammaQzAndUnsupportedCellsBeforeMutation) +{ + const Grid grid = skew_grid(); + const Channel original = density(grid, 0); + std::vector channels{original}; + for (const QPoint q: {QPoint{0.0, 0.0, 0.0}, QPoint{1.0, -1.0, 0.0}, QPoint{0.1, 0.2, 0.01}, + QPoint{0.1, 0.2, 1.0}, QPoint{0.1, 0.2, 1.0e-15}, + QPoint{std::numeric_limits::quiet_NaN(), 0.2, 0.0}}) + { + EXPECT_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(channels, grid, q), + std::invalid_argument); + EXPECT_EQ(channels[0].potential_r, original.potential_r); + } + const QPoint q{0.1, 0.2, 0.0}; + for (int defect = 0; defect < 7; ++defect) + { + Grid invalid = grid; + if (defect == 0) invalid.lattice_vectors[2][0] = 0.1; + if (defect == 1) invalid.lattice_vectors[0][2] = 0.1; + if (defect == 2) invalid.lattice_vectors[1] = invalid.lattice_vectors[0]; + if (defect == 3) invalid.periodic = false; + if (defect == 4) invalid.nz = 0; + if (defect == 5) invalid.lattice_vectors[2][2] = 0.0; + if (defect == 6) invalid.lattice_vectors[0][0] = std::numeric_limits::infinity(); + EXPECT_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(channels, invalid, q), + std::invalid_argument); + EXPECT_EQ(channels[0].potential_r, original.potential_r); + } + channels.push_back(original); + channels[1].potential_r.pop_back(); + EXPECT_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(channels, grid, q), std::invalid_argument); + EXPECT_EQ(channels[0].potential_r, original.potential_r); + channels[1] = original; + channels[1].potential_r[0] = Complex(0.0, std::numeric_limits::quiet_NaN()); + EXPECT_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(channels, grid, q), std::invalid_argument); + EXPECT_EQ(channels[0].potential_r, original.potential_r); + EXPECT_THROW(ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(channels, channels, grid, {{0, 1}}), + std::invalid_argument); +} + +TEST(SternheimerABFSStrict2D, RejectsFiniteDensityOverflowInForwardFFT) +{ + Grid grid; + grid.nx = grid.ny = 1; + grid.nz = 3; + grid.hx = grid.hy = grid.hz = 1.0; + Channel rho; + rho.potential_r.assign(3, Complex(0.75 * std::numeric_limits::max(), 0.0)); + rho.max_abs = 17.0; + std::vector channels{rho}; + try + { + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(channels, grid, QPoint{0.001, 0.0, 0.0}); + FAIL() << "Finite density must not silently overflow the z Fourier transform."; + } + catch (const std::overflow_error& error) + { + EXPECT_NE(std::string(error.what()).find("forward FFT"), std::string::npos); + } + EXPECT_EQ(channels[0].potential_r, rho.potential_r); + EXPECT_EQ(channels[0].max_abs, rho.max_abs); +} + +TEST(SternheimerABFSStrict2D, RejectsFiniteModeOverflowInInverseFFT) +{ + Grid grid; + grid.nx = 2; + grid.ny = grid.nz = 1; + grid.hx = std::sqrt(two_pi) / 4.0; + grid.hy = 1.0; + grid.hz = 4.0; + Channel rho; + // q=1/2 gives equal projected factors for both planar modes. Their + // separate coefficients are finite, but their inverse-FFT sum overflows. + rho.potential_r = {Complex(0.9 * std::numeric_limits::max(), 0.0), Complex{}}; + rho.max_abs = 19.0; + std::vector channels{rho}; + try + { + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(channels, grid, QPoint{0.5, 0.0, 0.0}); + FAIL() << "Finite Fourier coefficients must not silently overflow the inverse FFT."; + } + catch (const std::overflow_error& error) + { + EXPECT_NE(std::string(error.what()).find("inverse FFT"), std::string::npos); + } + EXPECT_EQ(channels[0].potential_r, rho.potential_r); + EXPECT_EQ(channels[0].max_abs, rho.max_abs); +} + +TEST(SternheimerABFSStrict2D, RejectsFiniteFieldOverflowInSelectedIntegrals) +{ + for (int stage = 0; stage < 3; ++stage) + { + Grid grid; + grid.nx = grid.ny = 1; + grid.nz = stage == 1 ? 2 : 1; + grid.hx = grid.hy = 1.0; + grid.hz = stage == 2 ? 4.0 : 1.0; + Channel rho, phi; + rho.potential_r.assign(grid.nz, Complex(0.75 * std::numeric_limits::max(), 0.0)); + phi.potential_r.assign(grid.nz, Complex(stage == 0 ? 2.0 : 1.0, 0.0)); + // Independently overflow the product, the running sum, and dV scaling. + EXPECT_THROW(ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals({rho}, {phi}, grid, {{0, 0}}), + std::overflow_error); + } +} + +TEST(SternheimerABFSStrict2D, AcceptsLargeFiniteRepresentableDensitiesAndIntegrals) +{ + const Grid grid = skew_grid(); + const QPoint q{0.0, 0.25, 0.0}; + const double scale = 1.0e140; + const std::vector rho{density(grid, 0)}; + auto large_rho = rho; + for (auto& value: large_rho[0].potential_r) value *= scale; + auto phi = rho; + auto large_phi = large_rho; + ASSERT_NO_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, q)); + ASSERT_NO_THROW(ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(large_phi, grid, q)); + for (std::size_t i = 0; i < phi[0].potential_r.size(); ++i) + { + EXPECT_NEAR(std::abs(large_phi[0].potential_r[i] / scale - phi[0].potential_r[i]), 0.0, 3.0e-12); + } + EXPECT_TRUE(std::isfinite(large_phi[0].max_abs)); + const auto baseline = ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(rho, phi, grid, {{0, 0}}); + const auto large = ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(large_rho, large_phi, grid, {{0, 0}}); + ASSERT_EQ(large.size(), 1u); + EXPECT_TRUE(std::isfinite(large[0].real())); + EXPECT_TRUE(std::isfinite(large[0].imag())); + EXPECT_NEAR(std::abs(large[0] / (scale * scale) - baseline[0]), 0.0, 3.0e-11); +} + +#ifdef STERNHEIMER_FULL_COULOMB_PANEL +TEST(SternheimerABFSStrict2D, FullPanelMatchesComplexSelectedIntegrals) +{ + const Grid grid = skew_grid(); + const std::vector rho{density(grid, 0), density(grid, 1)}; + auto phi = rho; + ModuleRI::solve_sternheimer_abf_strict2d_coulomb_in_place(phi, grid, QPoint{0.25, 0.0, 0.0}); + std::vector r, p; + for (const auto& c : rho) r.insert(r.end(), c.potential_r.begin(), c.potential_r.end()); + p = phi[1].potential_r; + const auto lattice = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + const double dv = std::abs(lattice[0][0]*lattice[1][1]-lattice[0][1]*lattice[1][0]) + *std::abs(lattice[2][2])/grid.size(); + const auto full = ModuleRI::sternheimer_abf_coulomb_panel(r, p, grid.size(), 2, 1, dv); + const auto selected = ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(rho, phi, grid, {{0,1},{1,1}}); + ASSERT_EQ(full.size(), selected.size()); + for (std::size_t i = 0; i < full.size(); ++i) EXPECT_NEAR(std::abs(full[i]-selected[i]), 0.0, 1e-11); + p.insert(p.end(), phi[0].potential_r.begin(), phi[0].potential_r.end()); + const auto both = ModuleRI::sternheimer_abf_coulomb_panel(r, p, grid.size(), 2, 2, dv); + const auto other = ModuleRI::sternheimer_abf_strict2d_selected_coulomb_integrals(rho, phi, grid, {{0,0},{1,0}}); + ASSERT_EQ(both.size(), 4u); + for (std::size_t i = 0; i < 2; ++i) + { + EXPECT_NEAR(std::abs(both[i]-selected[i]), 0.0, 1e-11); + EXPECT_NEAR(std::abs(both[i+2]-other[i]), 0.0, 1e-11); + } +} + +TEST(SternheimerABFSStrict2D, FullPanelRejectsInvalidInputs) +{ + using ModuleRI::sternheimer_abf_coulomb_panel; + EXPECT_THROW(sternheimer_abf_coulomb_panel({Complex(1)}, {Complex(1)}, 0, 1, 1, 1), std::invalid_argument); + EXPECT_THROW(sternheimer_abf_coulomb_panel({Complex(1)}, {Complex(1)}, 2, 1, 1, 1), std::invalid_argument); + EXPECT_THROW(sternheimer_abf_coulomb_panel({Complex(1)}, {Complex(1)}, 1, 1, 1, -1), std::invalid_argument); + EXPECT_THROW(sternheimer_abf_coulomb_panel({Complex(std::numeric_limits::infinity())}, + {Complex(1)}, 1, 1, 1, 1), std::invalid_argument); +} +#else +TEST(SternheimerABFSStrict2D, RequiresFullCoulombPanel) +{ + FAIL() << "Full density-potential Coulomb matrix panel is not implemented."; +} +#endif + +#else +TEST(SternheimerABFSStrict2D, RequiresStrict2DCoulombImplementation) +{ + FAIL() << "Missing sternheimer_abfs_strict2d.h: discrete nonzero-q strict2D Coulomb is not implemented."; +} +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_channel_resources_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_channel_resources_test.cpp new file mode 100644 index 00000000000..8893e343b59 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_channel_resources_test.cpp @@ -0,0 +1,659 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" + +#include +#include +#include +#include + +TEST(SternheimerChannelResources, EstimatesOneHundredTwentyComplexGridVectors) +{ + EXPECT_EQ(ModuleRI::estimate_sternheimer_channel_worker_bytes(1000), + 120ULL * 1000ULL * sizeof(std::complex)); +} + +TEST(SternheimerChannelResources, GroupsChannelsIntoStableMicroBatches) +{ + const auto batches = ModuleRI::make_sternheimer_channel_batches(10, 4); + ASSERT_EQ(batches.size(), 3U); + EXPECT_EQ(batches[0].begin, 0); + EXPECT_EQ(batches[0].size, 4); + EXPECT_EQ(batches[1].begin, 4); + EXPECT_EQ(batches[1].size, 4); + EXPECT_EQ(batches[2].begin, 8); + EXPECT_EQ(batches[2].size, 2); + + const auto scalar = ModuleRI::make_sternheimer_channel_batches(3, 1); + ASSERT_EQ(scalar.size(), 3U); + EXPECT_EQ(scalar[0].begin, 0); + EXPECT_EQ(scalar[1].begin, 1); + EXPECT_EQ(scalar[2].begin, 2); + EXPECT_THROW(ModuleRI::make_sternheimer_channel_batches(-1, 4), std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_channel_batches(4, 0), std::invalid_argument); +} + +TEST(SternheimerChannelResources, OwnedChannelBatchesKeepRequestedWidthOwnership) +{ + const auto first = ModuleRI::make_sternheimer_owned_channel_batches(10, 4, 2, 2, 0, 0); + ASSERT_EQ(first.size(), 3U); + EXPECT_EQ(first[0].begin, 0); + EXPECT_EQ(first[0].size, 2); + EXPECT_EQ(first[1].begin, 2); + EXPECT_EQ(first[1].size, 2); + EXPECT_EQ(first[2].begin, 8); + EXPECT_EQ(first[2].size, 2); + const auto second = ModuleRI::make_sternheimer_owned_channel_batches(10, 4, 3, 2, 1, 0); + ASSERT_EQ(second.size(), 2U); + EXPECT_EQ(second[0].begin, 4); + EXPECT_EQ(second[0].size, 3); + EXPECT_EQ(second[1].begin, 7); + EXPECT_EQ(second[1].size, 1); +} + +TEST(SternheimerChannelResources, OwnedChannelBatchesMixedWidthsCoverEveryChannelExactlyOnce) +{ + for (const int num_channels: {1, 2, 3, 7, 10, 17, 31}) + { + for (const int ownership_width: {1, 2, 4, 7}) + { + for (const int replicas: {1, 2, 3, 5}) + { + for (const int occupied_band: {0, 1, 3, std::numeric_limits::max()}) + { + SCOPED_TRACE(std::to_string(num_channels) + ":" + std::to_string(ownership_width) + + ":" + std::to_string(replicas) + ":" + std::to_string(occupied_band)); + const int ownership_batch_count = 1 + (num_channels - 1) / ownership_width; + std::vector visits(static_cast(num_channels), 0); + for (int replica = 0; replica != replicas; ++replica) + { + const int worker_width = 1 + replica % 3; + const auto batches = ModuleRI::make_sternheimer_owned_channel_batches( + num_channels, ownership_width, worker_width, replicas, replica, occupied_band); + int previous_end = 0; + for (const auto& batch: batches) + { + ASSERT_GE(batch.begin, previous_end); + ASSERT_LT(batch.begin, num_channels); + ASSERT_GT(batch.size, 0); + ASSERT_LE(batch.size, worker_width); + ASSERT_LE(batch.size, num_channels - batch.begin); + EXPECT_EQ(batch.begin / ownership_width, + (batch.begin + batch.size - 1) / ownership_width); + for (int channel = batch.begin; channel != batch.begin + batch.size; ++channel) + { + const int owner = ModuleRI::sternheimer_channel_batch_replica_owner( + occupied_band, channel / ownership_width, ownership_batch_count, replicas); + EXPECT_EQ(owner, replica); + ++visits[static_cast(channel)]; + } + previous_end = batch.begin + batch.size; + } + } + for (const int count: visits) + { + EXPECT_EQ(count, 1); + } + } + } + } + } +} + +TEST(SternheimerChannelResources, OwnedChannelBatchesEqualWidthsPreserveLegacyBandRotation) +{ + for (const int width: {1, 2, 4}) + { + const auto global_batches = ModuleRI::make_sternheimer_channel_batches(11, width); + for (int occupied_band = 0; occupied_band != 7; ++occupied_band) + { + for (int replica = 0; replica != 3; ++replica) + { + std::vector expected; + for (std::size_t chunk = 0; chunk != global_batches.size(); ++chunk) + { + if (ModuleRI::sternheimer_channel_batch_replica_owner( + occupied_band, static_cast(chunk), static_cast(global_batches.size()), 3) + == replica) + { + expected.push_back(global_batches[chunk]); + } + } + const auto actual = ModuleRI::make_sternheimer_owned_channel_batches( + 11, width, width, 3, replica, occupied_band); + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t batch = 0; batch != expected.size(); ++batch) + { + EXPECT_EQ(actual[batch].begin, expected[batch].begin); + EXPECT_EQ(actual[batch].size, expected[batch].size); + } + } + } + } +} + +TEST(SternheimerChannelResources, OwnedChannelBatchesAllowEmptyChannelsAndIdleReplicas) +{ + EXPECT_TRUE(ModuleRI::make_sternheimer_owned_channel_batches(0, 4, 2, 3, 1, 0).empty()); + EXPECT_TRUE(ModuleRI::make_sternheimer_owned_channel_batches(3, 4, 2, 8, 7, 0).empty()); + EXPECT_TRUE(ModuleRI::make_sternheimer_owned_channel_batches(3, 4, 2, 8, 0, 7).empty()); + const auto rotated = ModuleRI::make_sternheimer_owned_channel_batches(3, 4, 8, 8, 7, 7); + ASSERT_EQ(rotated.size(), 1U); + EXPECT_EQ(rotated[0].begin, 0); + EXPECT_EQ(rotated[0].size, 3); +} + +TEST(SternheimerChannelResources, OwnedChannelBatchesRejectInvalidInputs) +{ + EXPECT_THROW(ModuleRI::make_sternheimer_owned_channel_batches(-1, 2, 1, 2, 0, 0), std::invalid_argument); + for (const int invalid: {0, -1}) + { + EXPECT_THROW(ModuleRI::make_sternheimer_owned_channel_batches(4, invalid, 1, 2, 0, 0), std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_owned_channel_batches(4, 2, invalid, 2, 0, 0), std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_owned_channel_batches(4, 2, 1, invalid, 0, 0), std::invalid_argument); + } + for (const int invalid: {-1, 2}) + { + EXPECT_THROW(ModuleRI::make_sternheimer_owned_channel_batches(4, 2, 1, 2, invalid, 0), std::invalid_argument); + } + EXPECT_THROW(ModuleRI::make_sternheimer_owned_channel_batches(4, 2, 1, 2, 0, -1), std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_owned_channel_batches(0, 0, 1, 2, 0, 0), std::invalid_argument); +} + +TEST(SternheimerChannelResources, OwnedChannelBatchesAvoidIntegerOverflow) +{ + constexpr int maximum = std::numeric_limits::max(); + const auto wide = ModuleRI::make_sternheimer_owned_channel_batches(maximum, maximum, maximum, + maximum, maximum - 1, maximum - 1); + ASSERT_EQ(wide.size(), 1U); + EXPECT_EQ(wide[0].begin, 0); + EXPECT_EQ(wide[0].size, maximum); + const auto tail = ModuleRI::make_sternheimer_owned_channel_batches(maximum, maximum - 1, maximum, 2, 1, 0); + ASSERT_EQ(tail.size(), 1U); + EXPECT_EQ(tail[0].begin, maximum - 1); + EXPECT_EQ(tail[0].size, 1); + const auto sparse = ModuleRI::make_sternheimer_owned_channel_batches(maximum, 1, 1, + maximum, maximum - 1, maximum); + ASSERT_EQ(sparse.size(), 1U); + EXPECT_EQ(sparse[0].begin, maximum - 1); + EXPECT_EQ(sparse[0].size, 1); +} + +TEST(SternheimerChannelResources, BatchWorkerPlanUsesEveryAvailableOuterThread) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 100000000, + 0, + 1, + "test"}; + const auto scalar = ModuleRI::plan_sternheimer_owned_channel_workers(100, 100, 30, 100, 0, memory, 1); + const auto batch = ModuleRI::plan_sternheimer_owned_channel_workers(100, 100, 30, 100, 0, memory, 4); + + EXPECT_EQ(batch.channel_batch_width, 4); + EXPECT_EQ(batch.batch_tasks, 25); + EXPECT_EQ(batch.memory_per_worker_bytes, 4 * scalar.memory_per_worker_bytes); + EXPECT_EQ(scalar.automatic_workers, 30); + EXPECT_EQ(batch.automatic_workers, 25); + EXPECT_EQ(batch.effective_workers, 25); + EXPECT_LE(static_cast(batch.automatic_workers) * batch.memory_per_worker_bytes, + batch.increment_bytes_per_rank); +} + +TEST(SternheimerChannelResources, PlansFromNodeAggregateMemory) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 100000, + 32000, + 2, + "cgroup_v2"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, memory); + EXPECT_EQ(plan.automatic_workers, 11); + EXPECT_EQ(plan.effective_workers, 11); +} + +TEST(SternheimerChannelResources, PartialOuterTeamFallsBackToNestedGridParallelism) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 100000, + 0, + 1, + "proc_meminfo"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(32, 30, 1, 4, memory); + EXPECT_EQ(plan.automatic_workers, 30); + EXPECT_EQ(plan.effective_workers, 1); +} + +TEST(SternheimerChannelResources, BatchColumnsDoNotCountAsOuterWorkerThreads) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 100000000, + 0, + 1, + "proc_meminfo"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(8, 30, 100, 0, memory, 4); + EXPECT_EQ(plan.automatic_workers, 2); + EXPECT_EQ(plan.effective_workers, 1); +} + +TEST(SternheimerChannelResources, DownshiftsRequestedBatchWidthToDetectedMemoryBudget) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 100000, + 50000, + 1, + "cgroup_v1"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, memory, 16); + + EXPECT_EQ(plan.channel_batch_width, 13); + EXPECT_EQ(plan.batch_tasks, 4); + EXPECT_EQ(plan.memory_per_worker_bytes, 13 * 120 * sizeof(std::complex)); + EXPECT_LE(plan.memory_per_worker_bytes, plan.increment_bytes_per_rank); + EXPECT_EQ(plan.automatic_workers, 1); + EXPECT_EQ(plan.effective_workers, 1); +} + +TEST(SternheimerChannelResources, KeepsOuterParallelismWhenMostThreadsCanWork) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 56320, + 0, + 1, + "proc_meminfo"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, memory); + EXPECT_EQ(plan.automatic_workers, 22); + EXPECT_EQ(plan.effective_workers, 22); +} + +TEST(SternheimerChannelResources, ClampsToChannelsAndOpenMPThreads) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 100000, + 0, + 1, + "proc_meminfo"}; + EXPECT_EQ(ModuleRI::plan_sternheimer_channel_workers(3, 30, 1, 0, memory).effective_workers, 1); + EXPECT_EQ(ModuleRI::plan_sternheimer_channel_workers(40, 5, 1, 0, memory).effective_workers, 5); +} + +TEST(SternheimerChannelResources, DividesPerRankLimitBeforeSubtractingRss) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::per_rank, + 100000, + 10000, + 2, + "slurm+proc_status"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, memory); + EXPECT_EQ(plan.automatic_workers, 14); +} + +TEST(SternheimerChannelResources, DividesAvailableMemoryAcrossLocalRanks) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 100000, + 0, + 2, + "proc_meminfo"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, memory); + EXPECT_EQ(plan.automatic_workers, 19); +} + +TEST(SternheimerChannelResources, ZeroCapUsesAutomaticCount) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 100000, + 0, + 1, + "proc_meminfo"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(32, 30, 1, 0, memory); + EXPECT_EQ(plan.effective_workers, plan.automatic_workers); +} + +TEST(SternheimerChannelResources, FormatsWorkerDecisionDiagnostic) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 100000, + 32000, + 2, + "cgroup_v2"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 4, memory); + EXPECT_EQ(ModuleRI::format_sternheimer_channel_worker_diagnostic(memory, plan, 1, 4), + "resource_source=cgroup_v2 accounting_mode=node_aggregate " + "node_memory_limit_bytes=100000 memory_current_bytes=32000 " + "memory_target_bytes=75000 local_mpi_ranks=2 grid_size=1 " + "memory_per_worker_bytes=1920 channel_batch_width=1 batch_tasks=40 " + "automatic_workers=11 user_cap=4 effective_workers=1"); +} + +TEST(SternheimerChannelResources, RejectsDetectedBudgetBelowOneWorker) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 1000, + 0, + 1, + "cgroup_v2"}; + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(4, 4, 1, 0, memory), std::runtime_error); +} + +TEST(SternheimerChannelResources, BaselineRejectionReportsSnapshotAndPreservesSeventyFivePercentTarget) +{ + for (const auto mode: {ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + ModuleRI::SternheimerMemoryAccountingMode::per_rank}) + { + const std::uint64_t target + = mode == ModuleRI::SternheimerMemoryAccountingMode::node_aggregate ? 75000 : 37500; + for (const std::uint64_t current: {target, target + 1}) + { + SCOPED_TRACE(std::to_string(current)); + const ModuleRI::SternheimerMemorySnapshot memory{mode, 100000, current, 2, "test_limit"}; + try + { + (void)ModuleRI::plan_sternheimer_channel_workers(4, 4, 1, 1, memory); + FAIL() << "A one-worker cap must not bypass the 75 percent target."; + } + catch (const std::runtime_error& error) + { + const std::string message = error.what(); + EXPECT_NE(message.find("75 percent target"), std::string::npos); + EXPECT_NE(message.find("memory_current_bytes=" + std::to_string(current)), std::string::npos); + EXPECT_NE(message.find("memory_target_bytes=" + std::to_string(target)), std::string::npos); + EXPECT_NE(message.find("node_memory_limit_bytes=100000"), std::string::npos); + EXPECT_NE(message.find("resource_source=test_limit"), std::string::npos); + EXPECT_NE(message.find("accounting_mode=" + ModuleRI::sternheimer_memory_accounting_mode_name(mode)), + std::string::npos); + EXPECT_NE(message.find("local_mpi_ranks=2"), std::string::npos); + } + } + } +} + +TEST(SternheimerChannelResources, InsufficientWorkerHeadroomReportsCurrentTargetAndLimit) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 100000, + 74999, + 1, + "cgroup_v2"}; + try + { + (void)ModuleRI::plan_sternheimer_channel_workers(4, 4, 1, 0, memory); + FAIL() << "One byte of headroom must not admit a worker."; + } + catch (const std::runtime_error& error) + { + const std::string message = error.what(); + EXPECT_NE(message.find("75 percent target"), std::string::npos); + EXPECT_NE(message.find("memory_current_bytes=74999"), std::string::npos); + EXPECT_NE(message.find("memory_target_bytes=75000"), std::string::npos); + EXPECT_NE(message.find("node_memory_limit_bytes=100000"), std::string::npos); + } +} + +TEST(SternheimerChannelResources, ExactWorkerHeadroomRemainsAdmissible) +{ + const auto worker_bytes = ModuleRI::estimate_sternheimer_channel_worker_bytes(1); + ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 100000, + 75000 - worker_bytes, + 1, + "cgroup_v2"}; + const auto plan = ModuleRI::plan_sternheimer_channel_workers(4, 4, 1, 1, memory); + EXPECT_EQ(plan.target_bytes, 75000U); + EXPECT_EQ(plan.increment_bytes_per_rank, worker_bytes); + EXPECT_EQ(plan.effective_workers, 1); + ++memory.current_bytes; + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(4, 4, 1, 1, memory), std::runtime_error); +} + +TEST(SternheimerChannelResources, BestEffortHeapTrimPreservesLiveDataAndDoesNotOverrideBudget) +{ + static_assert(noexcept(ModuleRI::trim_sternheimer_process_heap()), "Heap trimming must be best effort."); + const std::complex value(3.25, -0.75); + const std::vector> live_values(1024, value); + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 100000, + 75000, + 1, + "cgroup_v2"}; + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(4, 4, 1, 1, memory), std::runtime_error); + for (int attempt = 0; attempt != 2; ++attempt) + { + const auto status = ModuleRI::trim_sternheimer_process_heap(); +#if defined(__GLIBC__) + EXPECT_TRUE(status == ModuleRI::SternheimerHeapTrimStatus::no_pages_released + || status == ModuleRI::SternheimerHeapTrimStatus::pages_released); +#else + EXPECT_EQ(status, ModuleRI::SternheimerHeapTrimStatus::unsupported); +#endif + for (const auto& element: live_values) + { + EXPECT_EQ(element, value); + } + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(4, 4, 1, 1, memory), std::runtime_error); + } +} + +TEST(SternheimerChannelResources, ReservesSharedMemoryAcrossNodeAggregateRanks) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 100000, + 32000, + 2, + "cgroup_v2"}; + const auto adjusted = ModuleRI::reserve_sternheimer_shared_memory(memory, 10000); + EXPECT_EQ(adjusted.current_bytes, 52000U); + EXPECT_EQ(adjusted.limit_bytes, memory.limit_bytes); + EXPECT_EQ(adjusted.mode, memory.mode); + EXPECT_EQ(adjusted.local_mpi_ranks, memory.local_mpi_ranks); + EXPECT_EQ(adjusted.source, memory.source); + EXPECT_EQ(memory.current_bytes, 32000U); + EXPECT_EQ(ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, adjusted).automatic_workers, 5); +} + +TEST(SternheimerChannelResources, ReservesSharedMemoryOnceForPerRankAccounting) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::per_rank, + 100000, + 10000, + 2, + "slurm+proc_status"}; + const auto adjusted = ModuleRI::reserve_sternheimer_shared_memory(memory, 10000); + EXPECT_EQ(adjusted.current_bytes, 20000U); + EXPECT_EQ(adjusted.limit_bytes, 100000U); + EXPECT_EQ(ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, adjusted).automatic_workers, 9); +} + +TEST(SternheimerChannelResources, ReservesSharedMemoryByReducingAvailableBytes) +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 100000, + 0, + 2, + "proc_meminfo"}; + const auto adjusted = ModuleRI::reserve_sternheimer_shared_memory(memory, 10000); + EXPECT_EQ(adjusted.current_bytes, 0U); + EXPECT_EQ(adjusted.limit_bytes, 80000U); + EXPECT_EQ(ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, adjusted).automatic_workers, 15); + EXPECT_THROW(ModuleRI::reserve_sternheimer_shared_memory(memory, 50001), std::runtime_error); + const auto exhausted = ModuleRI::reserve_sternheimer_shared_memory(memory, 50000); + EXPECT_EQ(exhausted.limit_bytes, 0U); + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(40, 30, 1, 0, exhausted), std::invalid_argument); +} + +TEST(SternheimerChannelResources, SharedReservationLeavesUnknownFallbackUnchanged) +{ + ModuleRI::SternheimerMemorySnapshot memory; + memory.local_mpi_ranks = 3; + const auto adjusted + = ModuleRI::reserve_sternheimer_shared_memory(memory, std::numeric_limits::max()); + EXPECT_EQ(adjusted.mode, ModuleRI::SternheimerMemoryAccountingMode::fallback_one); + EXPECT_EQ(adjusted.limit_bytes, 0U); + EXPECT_EQ(adjusted.current_bytes, 0U); + EXPECT_EQ(adjusted.local_mpi_ranks, 3); + EXPECT_EQ(adjusted.source, "unavailable"); +} + +TEST(SternheimerChannelResources, ZeroSharedReservationPreservesEveryAccountingMode) +{ + for (const auto mode: {ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + ModuleRI::SternheimerMemoryAccountingMode::per_rank, + ModuleRI::SternheimerMemoryAccountingMode::available, + ModuleRI::SternheimerMemoryAccountingMode::fallback_one}) + { + const ModuleRI::SternheimerMemorySnapshot memory{mode, 100000, 10000, 2, "test"}; + const auto adjusted = ModuleRI::reserve_sternheimer_shared_memory(memory, 0); + EXPECT_EQ(adjusted.mode, memory.mode); + EXPECT_EQ(adjusted.limit_bytes, memory.limit_bytes); + EXPECT_EQ(adjusted.current_bytes, memory.current_bytes); + EXPECT_EQ(adjusted.local_mpi_ranks, memory.local_mpi_ranks); + EXPECT_EQ(adjusted.source, memory.source); + } +} + +TEST(SternheimerChannelResources, SharedReservationChecksArithmeticOverflowAndRankCount) +{ + const auto maximum = std::numeric_limits::max(); + for (const auto mode: {ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + ModuleRI::SternheimerMemoryAccountingMode::per_rank}) + { + ModuleRI::SternheimerMemorySnapshot memory{mode, maximum, maximum, 2, "test"}; + EXPECT_THROW(ModuleRI::reserve_sternheimer_shared_memory(memory, 1), std::overflow_error); + memory.current_bytes = 0; + if (mode == ModuleRI::SternheimerMemoryAccountingMode::node_aggregate) + { + EXPECT_THROW(ModuleRI::reserve_sternheimer_shared_memory(memory, maximum), std::overflow_error); + } + else + { + EXPECT_EQ(ModuleRI::reserve_sternheimer_shared_memory(memory, maximum).current_bytes, maximum); + } + memory.local_mpi_ranks = 0; + EXPECT_THROW(ModuleRI::reserve_sternheimer_shared_memory(memory, 1), std::invalid_argument); + memory.local_mpi_ranks = -1; + EXPECT_THROW(ModuleRI::reserve_sternheimer_shared_memory(memory, 1), std::invalid_argument); + } + const ModuleRI::SternheimerMemorySnapshot available{ModuleRI::SternheimerMemoryAccountingMode::available, + maximum, + 0, + 2, + "test"}; + EXPECT_THROW(ModuleRI::reserve_sternheimer_shared_memory(available, maximum), std::overflow_error); +} + +TEST(SternheimerChannelResources, SharedReservationRejectsPackedProjectorsBeforeAllocation) +{ + constexpr std::uint64_t gib = 1ULL << 30; + constexpr std::uint64_t shared_bytes = (2ULL * 68 + 676) * 72 * 72 * 135 * sizeof(std::complex); + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::node_aggregate, + 108 * gib, + 74 * gib, + 1, + "cgroup_v2"}; + EXPECT_NO_THROW(ModuleRI::plan_sternheimer_channel_workers(3897, 30, 72 * 72 * 135, 1, memory, 2)); + const auto adjusted = ModuleRI::reserve_sternheimer_shared_memory(memory, shared_bytes); + EXPECT_EQ(adjusted.current_bytes, memory.current_bytes + shared_bytes); + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(3897, 30, 72 * 72 * 135, 1, adjusted, 2), + std::runtime_error); +} + +TEST(SternheimerChannelResources, UnavailableResourcesFallBackToOneWorker) +{ + const ModuleRI::SternheimerMemorySnapshot memory{}; + EXPECT_EQ(ModuleRI::plan_sternheimer_channel_workers(32, 30, 1, 0, memory).effective_workers, 1); + EXPECT_EQ(ModuleRI::plan_sternheimer_channel_workers(32, 30, 1, 8, memory).effective_workers, 1); +} + +TEST(SternheimerChannelResources, RejectsInvalidInputsAndOverflow) +{ + const ModuleRI::SternheimerMemorySnapshot memory{}; + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(-1, 1, 1, 0, memory), std::invalid_argument); + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(1, 0, 1, 0, memory), std::invalid_argument); + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(1, 1, 1, -1, memory), std::invalid_argument); + EXPECT_THROW(ModuleRI::plan_sternheimer_channel_workers(1, 1, 0, 0, memory), std::invalid_argument); + EXPECT_THROW(ModuleRI::estimate_sternheimer_channel_worker_bytes(std::numeric_limits::max()), + std::overflow_error); +} + +TEST(SternheimerChannelResources, ParsesCgroupSlurmAndProcMemoryUnits) +{ + using ModuleRI::detail::parse_sternheimer_kib_field; + using ModuleRI::detail::parse_sternheimer_memory_bytes; + using ModuleRI::detail::parse_sternheimer_slurm_mem_per_node; + + EXPECT_EQ(parse_sternheimer_memory_bytes("4096"), 4096U); + EXPECT_EQ(parse_sternheimer_memory_bytes("110610M"), 110610ULL * 1024ULL * 1024ULL); + EXPECT_EQ(parse_sternheimer_slurm_mem_per_node("110610"), 110610ULL * 1024ULL * 1024ULL); + EXPECT_FALSE(parse_sternheimer_memory_bytes("max").has_value()); + EXPECT_FALSE(parse_sternheimer_memory_bytes("12MB").has_value()); + EXPECT_EQ(parse_sternheimer_kib_field("MemTotal: 4096 kB\nMemAvailable: 2048 kB\n", "MemAvailable"), + 2048ULL * 1024ULL); + EXPECT_EQ(parse_sternheimer_kib_field("VmPeak:\t2048 kB\nVmRSS:\t1024 kB\n", "VmRSS"), 1024ULL * 1024ULL); + EXPECT_FALSE(parse_sternheimer_kib_field("VmRSS: invalid kB\n", "VmRSS").has_value()); +} + +TEST(SternheimerChannelResources, ParsesCgroupMembershipPaths) +{ + EXPECT_EQ(ModuleRI::detail::parse_sternheimer_cgroup_v2_path("0::/slurm/job_7/step_0\n"), "/slurm/job_7/step_0"); + EXPECT_EQ( + ModuleRI::detail::parse_sternheimer_cgroup_v1_memory_path("4:cpu:/slurm/job_7\n5:memory:/slurm/job_7/step_0\n"), + "/slurm/job_7/step_0"); + EXPECT_FALSE(ModuleRI::detail::parse_sternheimer_cgroup_v2_path("5:memory:/job\n").has_value()); +} + +TEST(SternheimerChannelResources, SelectsSmallestEnforcedLimitWithCgroupUsage) +{ + ModuleRI::detail::SternheimerMemoryCandidates values; + values.cgroup_limit_bytes = 100000; + values.cgroup_current_bytes = 20000; + values.slurm_limit_bytes = 90000; + values.cgroup_source = "cgroup_v2"; + const auto result = ModuleRI::detail::select_sternheimer_memory_snapshot(values, 2); + EXPECT_EQ(result.mode, ModuleRI::SternheimerMemoryAccountingMode::node_aggregate); + EXPECT_EQ(result.limit_bytes, 90000U); + EXPECT_EQ(result.current_bytes, 20000U); + EXPECT_EQ(result.local_mpi_ranks, 2); + EXPECT_EQ(result.source, "cgroup_v2+slurm"); +} + +TEST(SternheimerChannelResources, UsesPerRankRssWithSlurmLimit) +{ + ModuleRI::detail::SternheimerMemoryCandidates values; + values.slurm_limit_bytes = 100000; + values.process_rss_bytes = 10000; + const auto result = ModuleRI::detail::select_sternheimer_memory_snapshot(values, 2); + EXPECT_EQ(result.mode, ModuleRI::SternheimerMemoryAccountingMode::per_rank); + EXPECT_EQ(result.limit_bytes, 100000U); + EXPECT_EQ(result.current_bytes, 10000U); + EXPECT_EQ(result.source, "slurm+proc_status"); +} + +TEST(SternheimerChannelResources, UsesAvailableMemoryWithoutSubtractingRssAgain) +{ + ModuleRI::detail::SternheimerMemoryCandidates values; + values.mem_available_bytes = 80000; + values.process_rss_bytes = 10000; + const auto result = ModuleRI::detail::select_sternheimer_memory_snapshot(values, 1); + EXPECT_EQ(result.mode, ModuleRI::SternheimerMemoryAccountingMode::available); + EXPECT_EQ(result.limit_bytes, 80000U); + EXPECT_EQ(result.current_bytes, 0U); + EXPECT_EQ(result.source, "proc_meminfo"); +} + +TEST(SternheimerChannelResources, TreatsV1PhysicalMemorySentinelAsUnlimited) +{ + ModuleRI::detail::SternheimerMemoryCandidates values; + values.cgroup_limit_bytes = 0x7ffffffffffff000ULL; + values.physical_memory_bytes = 128ULL << 30; + values.mem_available_bytes = 64ULL << 30; + values.cgroup_source = "cgroup_v1"; + const auto result = ModuleRI::detail::select_sternheimer_memory_snapshot(values, 1); + EXPECT_EQ(result.mode, ModuleRI::SternheimerMemoryAccountingMode::available); + EXPECT_EQ(result.limit_bytes, 64ULL << 30); +} + +TEST(SternheimerChannelResources, FallsBackWhenNoMemorySourceIsTrustworthy) +{ + const ModuleRI::detail::SternheimerMemoryCandidates values{}; + const auto result = ModuleRI::detail::select_sternheimer_memory_snapshot(values, 3); + EXPECT_EQ(result.mode, ModuleRI::SternheimerMemoryAccountingMode::fallback_one); + EXPECT_EQ(result.local_mpi_ranks, 3); + EXPECT_EQ(result.source, "unavailable"); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_coulomb_tail_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_coulomb_tail_test.cpp new file mode 100644 index 00000000000..1141a38e66c --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_coulomb_tail_test.cpp @@ -0,0 +1,174 @@ +#include "source_basis/module_ao/orb_atomic_lm.h" +#include "source_lcao/module_ri/conv_coulomb_pot_k.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" + +#include +#include +#include + +namespace +{ +auto sample_tail(const int l, const bool enabled, const bool periodic = false) +{ + ModuleRI::SternheimerRadialPerturbation radial; + radial.angular_momentum = l; + radial.radial_grid = {0.0, 0.5, 1.0}; + radial.radial_values = {0.0, 2.0, 3.0}; + radial.continue_coulomb_tail = enabled; + radial.coulomb_tail_coefficient = 3.0; + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 3; + grid.ny = grid.nz = 1; + grid.hx = grid.hy = grid.hz = 1.0; + grid.periodic = periodic; + return ModuleRI::sample_sternheimer_abf_grid_channels({{radial}}, + {0}, + {ModuleBase::Vector3(0.0, 0.0, 0.0)}, + grid); +} +} // namespace + +TEST(SternheimerCoulombTail, PreservesInnerSamplesAndExtendsMultipoles) +{ + for (const int l: {0, 1, 2}) + { + const auto old = sample_tail(l, false); + const auto extended = sample_tail(l, true); + ASSERT_EQ(old.size(), extended.size()); + double endpoint_norm = 0.0; + for (std::size_t m = 0; m < old.size(); ++m) + { + EXPECT_EQ(extended[m].potential_r[0], old[m].potential_r[0]); + EXPECT_EQ(extended[m].potential_r[1], old[m].potential_r[1]); + EXPECT_EQ(old[m].potential_r[2], 0.0); + EXPECT_NEAR(extended[m].potential_r[2], old[m].potential_r[1] / std::pow(2.0, l + 1), 1.e-14); + endpoint_norm += std::abs(old[m].potential_r[1]); + } + EXPECT_GT(endpoint_norm, 0.1); + } +} + +TEST(SternheimerCoulombTail, RejectsPeriodicImageSum) +{ + EXPECT_THROW(sample_tail(0, true, true), std::invalid_argument); + EXPECT_NO_THROW(sample_tail(0, false, true)); +} + +TEST(SternheimerCoulombTail, ZeroMomentChargeDoesNotAcquireAnExteriorPotential) +{ + // P_l=r^l (1-r^2)^2 (1-a*r^2) has exactly zero l-th moment on [0,1]. + for (const int l: {0, 1, 2}) + { + const auto integral = [l](const int extra) { + return 1.0 / (2 * l + 3 + extra) - 2.0 / (2 * l + 5 + extra) + 1.0 / (2 * l + 7 + extra); + }; + constexpr int nr = 1001; + std::vector r(nr), rab(nr, 1.0 / (nr - 1)), p(nr); + const double a = integral(0) / integral(2); + for (int i = 0; i < nr; ++i) + { + r[i] = i * rab[i]; + p[i] = std::pow(r[i], l) * std::pow(1 - r[i] * r[i], 2) * (1 - a * r[i] * r[i]); + } + Numerical_Orbital_Lm charge; + charge.set_orbital_info("X", + 0, + l, + 0, + nr, + rab.data(), + r.data(), + Numerical_Orbital_Lm::Psi_Type::Psi, + p.data(), + 2001, + 0.05, + 0.001, + false, + true, + false); + const auto potential = Conv_Coulomb_Pot_K::cal_orbs_ccp( + charge, + {{Conv_Coulomb_Pot_K::Coulomb_Type::Fock, {{{"alpha", "1"}, {"singularity_correction", "limits"}}}}}, + 1.0); + std::vector>> orbitals(1); + orbitals[0].resize(l + 1); + orbitals[0][l].push_back(potential); + auto radials = ModuleRI::make_sternheimer_radial_perturbations_from_orbitals(orbitals); + ModuleRI::set_sternheimer_coulomb_tail_from_charge(radials[0][0], charge); + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 2; + grid.ny = grid.nz = 1; + grid.hx = 2.0; + grid.hy = grid.hz = 1.0; + grid.periodic = false; + const auto channels = ModuleRI::sample_sternheimer_abf_grid_channels(radials, + {0}, + {ModuleBase::Vector3(0, 0, 0)}, + grid); + for (const auto& channel: channels) + { + EXPECT_NEAR(channel.potential_r[1], 0.0, 1.e-11) << "l=" << l; + } + } +} + +TEST(SternheimerCoulombTail, ChargeMomentHasAnalyticNormalizationAndRejectsTruncation) +{ + for (const int l: {0, 1, 2, 3}) + { + constexpr int nr = 1001; + std::vector r(nr), rab(nr, 0.001), p(nr); + for (int i = 0; i < nr; ++i) + { + r[i] = i * 0.001; + p[i] = std::pow(r[i], l) * std::pow(1 - r[i] * r[i], 2); + } + Numerical_Orbital_Lm charge; + charge.set_orbital_info("X", + 0, + l, + 0, + nr, + rab.data(), + r.data(), + Numerical_Orbital_Lm::Psi_Type::Psi, + p.data(), + 101, + 0.05, + 0.001, + false, + true, + false); + ModuleRI::SternheimerRadialPerturbation potential; + potential.type_index = 0; + potential.angular_momentum = l; + potential.radial_grid = {0.0, 0.5, 1.0}; + potential.radial_values = {7.0, 8.0, 9.0}; + const auto original_values = potential.radial_values; + ModuleRI::set_sternheimer_coulomb_tail_from_charge(potential, charge); + const double moment = 1.0 / (2 * l + 3) - 2.0 / (2 * l + 5) + 1.0 / (2 * l + 7); + EXPECT_NEAR(potential.coulomb_tail_coefficient, 4 * std::acos(-1.0) * moment / (2 * l + 1), 1.e-11); + EXPECT_EQ(potential.radial_values, original_values); + // The charge, not an arbitrary last potential value, sets the tail. + potential.radial_values.back() = 1.e4; + ModuleRI::set_sternheimer_coulomb_tail_from_charge(potential, charge); + EXPECT_NEAR(potential.coulomb_tail_coefficient, 4 * std::acos(-1.0) * moment / (2 * l + 1), 1.e-11); + potential.radial_grid.back() = 0.9; + EXPECT_THROW(ModuleRI::set_sternheimer_coulomb_tail_from_charge(potential, charge), std::invalid_argument); + } +} + +TEST(SternheimerCoulombTail, RejectsUnspecifiedMoment) +{ + ModuleRI::SternheimerRadialPerturbation radial; + radial.radial_grid = {0, 0.5, 1}; + radial.radial_values = {0, 1, 2}; + radial.continue_coulomb_tail = true; + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = grid.ny = grid.nz = 1; + grid.hx = grid.hy = grid.hz = 1; + grid.periodic = false; + EXPECT_THROW( + ModuleRI::sample_sternheimer_abf_grid_channels({{radial}}, {0}, {ModuleBase::Vector3(0, 0, 0)}, grid), + std::invalid_argument); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_coulomb_whitening_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_coulomb_whitening_test.cpp new file mode 100644 index 00000000000..26b77cae5d5 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_coulomb_whitening_test.cpp @@ -0,0 +1,155 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_coulomb_whitening.h" + +#include + +#include +#include +#include + +namespace +{ + +std::vector gram(const std::vector& columns, const int rows, const int count) +{ + std::vector result(static_cast(count * count), 0.0); + for (int left = 0; left != count; ++left) + { + for (int right = 0; right != count; ++right) + { + for (int row = 0; row != rows; ++row) + { + result[static_cast(left * count + right)] + += columns[static_cast(row * count + left)] + * columns[static_cast(row * count + right)]; + } + } + } + return result; +} + +std::vector right_multiply(const std::vector& left, + const int rows, + const int inner, + const std::vector& right, + const int columns) +{ + std::vector result(static_cast(rows * columns), 0.0); + for (int row = 0; row != rows; ++row) + { + for (int column = 0; column != columns; ++column) + { + for (int index = 0; index != inner; ++index) + { + result[static_cast(row * columns + column)] + += left[static_cast(row * inner + index)] + * right[static_cast(index * columns + column)]; + } + } + } + return result; +} + +std::vector row_projector(const std::vector& columns, const int rows, const int count) +{ + std::vector result(static_cast(rows * rows), 0.0); + for (int left = 0; left != rows; ++left) + { + for (int right = 0; right != rows; ++right) + { + for (int column = 0; column != count; ++column) + { + result[static_cast(left * rows + right)] + += columns[static_cast(left * count + column)] + * columns[static_cast(right * count + column)]; + } + } + } + return result; +} + +std::vector left_transpose_multiply(const std::vector& left, + const int rows, + const int inner, + const std::vector& right, + const int columns) +{ + std::vector result(static_cast(inner * columns), 0.0); + for (int left_column = 0; left_column != inner; ++left_column) + { + for (int right_column = 0; right_column != columns; ++right_column) + { + for (int row = 0; row != rows; ++row) + { + result[static_cast(left_column * columns + right_column)] + += left[static_cast(row * inner + left_column)] + * right[static_cast(row * columns + right_column)]; + } + } + } + return result; +} + +} // namespace + +TEST(SternheimerCoulombWhitening, ProducesIdentityInTheRetainedCoulombMetric) +{ + const std::vector metric = {4.0, 1.0, 1.0, 2.0}; + const ModuleRI::SternheimerCoulombWhitening whitening + = ModuleRI::make_sternheimer_coulomb_whitening(metric, 2, 1.0e-12); + + EXPECT_EQ(whitening.raw_dimension, 2); + EXPECT_EQ(whitening.retained_rank, 2); + EXPECT_LT(whitening.max_orthonormality_error, 1.0e-12); + + const std::vector metric_times_w + = right_multiply(metric, 2, 2, whitening.transform, whitening.retained_rank); + const std::vector identity = left_transpose_multiply(whitening.transform, + 2, + whitening.retained_rank, + metric_times_w, + whitening.retained_rank); + EXPECT_NEAR(identity[0], 1.0, 1.0e-12); + EXPECT_NEAR(identity[1], 0.0, 1.0e-12); + EXPECT_NEAR(identity[2], 0.0, 1.0e-12); + EXPECT_NEAR(identity[3], 1.0, 1.0e-12); +} + +TEST(SternheimerCoulombWhitening, IsInvariantToInvertibleRawAuxiliaryChanges) +{ + const std::vector raw_potentials = { + 1.0, 0.0, + 0.5, 1.0, + 0.0, 2.0, + }; + const std::vector change = {2.0, 0.5, -0.25, 1.5}; + const std::vector changed_potentials = right_multiply(raw_potentials, 3, 2, change, 2); + const std::vector raw_metric = gram(raw_potentials, 3, 2); + const std::vector changed_metric = gram(changed_potentials, 3, 2); + + const auto raw_whitening = ModuleRI::make_sternheimer_coulomb_whitening(raw_metric, 2, 1.0e-12); + const auto changed_whitening + = ModuleRI::make_sternheimer_coulomb_whitening(changed_metric, 2, 1.0e-12); + const auto raw_orthonormal + = ModuleRI::apply_sternheimer_channel_transform(raw_potentials, 3, 2, raw_whitening); + const auto changed_orthonormal + = ModuleRI::apply_sternheimer_channel_transform(changed_potentials, 3, 2, changed_whitening); + + const auto raw_projector = row_projector(raw_orthonormal, 3, raw_whitening.retained_rank); + const auto changed_projector = row_projector(changed_orthonormal, 3, changed_whitening.retained_rank); + ASSERT_EQ(raw_projector.size(), changed_projector.size()); + for (std::size_t index = 0; index != raw_projector.size(); ++index) + { + EXPECT_NEAR(raw_projector[index], changed_projector[index], 1.0e-12); + } +} + +TEST(SternheimerCoulombWhitening, TruncatesNearNullAndRejectsMateriallyNegativeDirections) +{ + const auto truncated + = ModuleRI::make_sternheimer_coulomb_whitening({4.0, 0.0, 0.0, 1.0e-14}, 2, 1.0e-12); + EXPECT_EQ(truncated.retained_rank, 1); + EXPECT_EQ(truncated.discarded_rank, 1); + + EXPECT_THROW(ModuleRI::make_sternheimer_coulomb_whitening({1.0, 0.0, 0.0, -1.0e-3}, 2, 1.0e-12), + std::runtime_error); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_delta_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_delta_test.cpp new file mode 100644 index 00000000000..bc36fb6f5ec --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_delta_test.cpp @@ -0,0 +1,2223 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_delta.h" + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Complex = std::complex; +using Vector = ModuleRI::SternheimerFDHamiltonian::Vector; + +Vector unit_vector(const int size, const int index) +{ + Vector vec(size, Complex(0.0, 0.0)); + vec[index] = Complex(1.0, 0.0); + return vec; +} + +void expect_vector_near(const Vector& actual, const Vector& expected, const double tolerance) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t ir = 0; ir != actual.size(); ++ir) + { + EXPECT_NEAR(actual[ir].real(), expected[ir].real(), tolerance); + EXPECT_NEAR(actual[ir].imag(), expected[ir].imag(), tolerance); + } +} + +std::vector column_major_matrix_vector_product(const std::vector& matrix, + const std::vector& vector) +{ + const std::size_t size = vector.size(); + std::vector result(size, Complex(0.0, 0.0)); + for (std::size_t column = 0; column != size; ++column) + { + for (std::size_t row = 0; row != size; ++row) + { + result[row] += matrix[row + size * column] * vector[column]; + } + } + return result; +} + +using GridFunction = ModuleRI::SternheimerDeltaGridFunction; +using GridMatrices = ModuleRI::SternheimerDeltaGridMatrices; +using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; +using NonlocalProjector = ModuleRI::SternheimerFDNonlocalProjector; + +const std::array grid_matrix_components{ + {&GridMatrices::overlap, &GridMatrices::kinetic, &GridMatrices::local_potential, + &GridMatrices::nonlocal, &GridMatrices::hamiltonian}}; + +void expect_grid_matrices_near(const GridMatrices& actual, + const GridMatrices& expected, + const double tolerance) +{ + EXPECT_EQ(actual.row_count, expected.row_count); + EXPECT_EQ(actual.column_count, expected.column_count); + for (const auto component: grid_matrix_components) + { + expect_vector_near(actual.*component, expected.*component, tolerance); + } +} + +struct WeakCrossFixture +{ + Hamiltonian::Grid grid; + double volume_element; + std::vector potential; + NonlocalProjector::ProjectorBlock block; + + explicit WeakCrossFixture(const int nx = 5, const int ny = 3, const int nz = 2) + : grid{nx, ny, nz, 2.0 / nx, 1.7 / ny, 2.3 / nz, true}, + volume_element(2.0 * 1.7 * 2.3 / grid.size()) + { + grid.kpoint = {{0.17, -0.23, 0.11}}; + grid.lattice_vectors = {{{2.0, 0.0, 0.0}, {0.4, 1.7, 0.0}, {0.2, 0.3, 2.3}}}; + block.projectors.resize(2); + block.d_matrix = {{Complex(1.7, 0.0), Complex(0.3, 0.2)}, + {Complex(0.3, -0.2), Complex(-0.4, 0.0)}}; + for (int ir = 0; ir != grid.size(); ++ir) + { + potential.push_back(0.3 * std::cos(0.31 * ir) - 0.2); + block.projectors[0].emplace_back(0.1 * std::sin(0.2 * ir), 0.2 * std::cos(0.17 * ir)); + block.projectors[1].emplace_back(0.2 * std::cos(0.3 * ir), -0.1 * std::sin(0.27 * ir)); + } + } + + Hamiltonian hamiltonian(const bool include_nonlocal = true, const double prefactor = 1.0) const + { + std::shared_ptr projector; + if (include_nonlocal) + { + projector = std::make_shared( + grid.size(), volume_element, std::vector{block}); + } + return Hamiltonian(grid, potential, prefactor, projector, 8); + } + + std::vector functions(const int count, const double offset) const + { + const double two_pi = 2.0 * std::acos(-1.0); + std::vector result(count); + for (int band = 0; band != count; ++band) + { + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + Complex value(0.0, 0.0); + std::array gradient{}; + // Analytic Cartesian derivatives of two Bloch plane waves in the oblique cell. + for (int mode = 0; mode != 2; ++mode) + { + const std::array reduced{ + {grid.kpoint[0] + mode, grid.kpoint[1] + (band % 2), + grid.kpoint[2] - mode}}; + const double qx = two_pi * reduced[0] / 2.0; + const double qy = (two_pi * reduced[1] - 0.4 * qx) / 1.7; + const double qz = (two_pi * reduced[2] - 0.2 * qx - 0.3 * qy) / 2.3; + const double phase = two_pi * (reduced[0] * ix / grid.nx + + reduced[1] * iy / grid.ny + + reduced[2] * iz / grid.nz); + const Complex term = Complex(0.4 + offset + 0.13 * band + 0.2 * mode, + 0.3 * offset - 0.11 * band + 0.1 * mode) + * std::exp(Complex(0.0, phase)); + value += term; + gradient[0] += Complex(0.0, qx) * term; + gradient[1] += Complex(0.0, qy) * term; + gradient[2] += Complex(0.0, qz) * term; + } + result[band].values.push_back(value); + for (int direction = 0; direction != 3; ++direction) + { + result[band].gradients[direction].push_back(gradient[direction]); + } + } + } + } + } + return result; + } +}; + +GridMatrices scalar_cross_matrices(const Hamiltonian& hamiltonian, + const std::vector& left, + const std::vector& right, + const double volume_element) +{ + GridMatrices result; + result.row_count = left.size(); + result.column_count = right.size(); + for (const auto component: grid_matrix_components) + { + (result.*component).assign(left.size() * right.size(), Complex(0.0, 0.0)); + } + for (std::size_t column = 0; column != right.size(); ++column) + { + for (std::size_t row = 0; row != left.size(); ++row) + { + const std::size_t index = row + left.size() * column; + for (std::size_t ir = 0; ir != left[row].values.size(); ++ir) + { + const Complex product = volume_element * std::conj(left[row].values[ir]) + * right[column].values[ir]; + result.overlap[index] += product; + result.local_potential[index] += product * hamiltonian.local_potential()[ir]; + for (int direction = 0; direction != 3; ++direction) + { + result.kinetic[index] += volume_element * hamiltonian.kinetic_prefactor() + * std::conj(left[row].gradients[direction][ir]) + * right[column].gradients[direction][ir]; + } + } + // Independent beta-D-beta quadrature: no projector apply or BLAS assembly helpers. + if (hamiltonian.nonlocal_projector() != nullptr) + { + for (const auto& block: hamiltonian.nonlocal_projector()->blocks()) + { + for (std::size_t p = 0; p != block.projectors.size(); ++p) + { + for (std::size_t q = 0; q != block.projectors.size(); ++q) + { + Complex left_beta(0.0, 0.0), beta_right(0.0, 0.0); + for (std::size_t ir = 0; ir != left[row].values.size(); ++ir) + { + left_beta += volume_element * std::conj(left[row].values[ir]) + * block.projectors[p][ir]; + beta_right += volume_element * std::conj(block.projectors[q][ir]) + * right[column].values[ir]; + } + result.nonlocal[index] += left_beta * block.d_matrix[p][q] * beta_right; + } + } + } + } + result.hamiltonian[index] = result.kinetic[index] + result.local_potential[index] + + result.nonlocal[index]; + } + } + return result; +} + +} // namespace + +TEST(SternheimerDelta, PostprocessReconstructsStandardSolutionWithResidualCoupling) +{ + constexpr double volume_element = 1.0; + constexpr double eps_i = -0.25; + constexpr double eps_a = 0.75; + constexpr double omega = 0.4; + + const Vector occupied = unit_vector(4, 0); + const Vector eta = unit_vector(4, 1); + const Vector residual = {Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.30, -0.10), Complex(-0.20, 0.40)}; + const Vector out = {Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.20, 0.30), Complex(-0.10, 0.25)}; + const Complex expected_coefficient(0.70, -0.20); + + Vector standard_delta = out; + for (std::size_t ir = 0; ir != standard_delta.size(); ++ir) + { + standard_delta[ir] += expected_coefficient * eta[ir]; + } + + const Complex residual_overlap = ModuleRI::sternheimer_fd_grid_dot(residual, out, volume_element); + const Complex denominator(eps_i - eps_a, -omega); + const Complex perturbation_matrix_element = expected_coefficient * denominator - residual_overlap; + + ModuleRI::SternheimerDeltaPostprocessInput input; + input.occupied_wavefunctions = {occupied}; + input.virtual_states = {{eta, residual, eps_a}}; + input.perturbation_matrix_elements = {perturbation_matrix_element}; + input.occupied_eigenvalue = eps_i; + input.omega = omega; + input.volume_element = volume_element; + + const auto result = ModuleRI::postprocess_delta_sternheimer_solution(standard_delta, input); + + ASSERT_EQ(result.coefficients.size(), 1); + ASSERT_EQ(result.sos_coefficients.size(), 1); + ASSERT_EQ(result.pulay_coefficients.size(), 1); + const Complex expected_sos_coefficient = perturbation_matrix_element / denominator; + const Complex expected_pulay_coefficient = residual_overlap / denominator; + EXPECT_NEAR(result.sos_coefficients[0].real(), expected_sos_coefficient.real(), 1.0e-14); + EXPECT_NEAR(result.sos_coefficients[0].imag(), expected_sos_coefficient.imag(), 1.0e-14); + EXPECT_NEAR(result.pulay_coefficients[0].real(), expected_pulay_coefficient.real(), 1.0e-14); + EXPECT_NEAR(result.pulay_coefficients[0].imag(), expected_pulay_coefficient.imag(), 1.0e-14); + EXPECT_NEAR(result.coefficients[0].real(), expected_coefficient.real(), 1.0e-14); + EXPECT_NEAR(result.coefficients[0].imag(), expected_coefficient.imag(), 1.0e-14); + expect_vector_near(result.out_wavefunction, out, 1.0e-14); + Vector expected_in_sos(4, Complex(0.0, 0.0)); + Vector expected_in_pulay(4, Complex(0.0, 0.0)); + for (std::size_t ir = 0; ir != eta.size(); ++ir) + { + expected_in_sos[ir] = expected_sos_coefficient * eta[ir]; + expected_in_pulay[ir] = expected_pulay_coefficient * eta[ir]; + } + expect_vector_near(result.in_sos_wavefunction, expected_in_sos, 1.0e-14); + expect_vector_near(result.in_pulay_wavefunction, expected_in_pulay, 1.0e-14); + expect_vector_near(result.reconstructed_wavefunction, standard_delta, 1.0e-14); + EXPECT_NEAR(result.reconstruction_error, 0.0, 1.0e-14); + + EXPECT_NEAR(std::abs(ModuleRI::sternheimer_fd_grid_dot(occupied, result.out_wavefunction, volume_element)), + 0.0, + 1.0e-14); + EXPECT_NEAR(std::abs(ModuleRI::sternheimer_fd_grid_dot(eta, result.out_wavefunction, volume_element)), + 0.0, + 1.0e-14); +} + +TEST(SternheimerDelta, PulayOperatorTermsCloseToStoredResidualContribution) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + constexpr double volume_element = 0.25; + Hamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + const std::vector full_local{0.2, -0.1, 0.4, -0.3}; + const std::vector fixed_local{0.1, -0.2, 0.15, -0.05}; + Hamiltonian hamiltonian(grid, full_local, 1.0); + const Vector occupied(4, Complex(1.0, 0.0)); + const Vector eta{Complex(1.0, 0.0), Complex(0.0, 1.0), + Complex(-1.0, 0.0), Complex(0.0, -1.0)}; + const Vector out{Complex(1.0, 0.0), Complex(-1.0, 0.0), + Complex(1.0, 0.0), Complex(-1.0, 0.0)}; + const double eta_norm = ModuleRI::sternheimer_fd_grid_norm(eta, volume_element); + const double occupied_norm = ModuleRI::sternheimer_fd_grid_norm(occupied, volume_element); + Vector occupied_normalized = occupied; + Vector eta_normalized = eta; + for (Complex& value: occupied_normalized) + { + value /= occupied_norm; + } + for (Complex& value: eta_normalized) + { + value /= eta_norm; + } + + constexpr double virtual_eigenvalue = 0.75; + Vector residual; + hamiltonian.apply(eta_normalized, residual); + for (std::size_t ir = 0; ir != residual.size(); ++ir) + { + residual[ir] -= virtual_eigenvalue * eta_normalized[ir]; + } + const auto dot = [volume_element](const Vector& lhs, const Vector& rhs) { + return ModuleRI::sternheimer_fd_grid_dot(lhs, rhs, volume_element); + }; + ModuleRI::SternheimerRPA::project_out_subspace( + {occupied_normalized, eta_normalized}, dot, residual); + + const std::vector states + = {{eta_normalized, residual, virtual_eigenvalue}}; + const auto components = ModuleRI::decompose_delta_sternheimer_pulay_operator_terms( + hamiltonian, + fixed_local, + {occupied_normalized}, + states, + out, + -0.25, + 0.4, + volume_element); + + for (std::size_t ir = 0; ir != components.total.size(); ++ir) + { + const Complex sum = components.kinetic[ir] + components.fixed_local[ir] + + components.hxc_local[ir] + components.nonlocal[ir] + + components.eigenvalue[ir]; + EXPECT_NEAR(sum.real(), components.total[ir].real(), 1.0e-13); + EXPECT_NEAR(sum.imag(), components.total[ir].imag(), 1.0e-13); + } +} + +TEST(SternheimerDelta, BuildsDirectSOSWavefunctionFromExplicitVirtualStates) +{ + constexpr double eps_i = -0.5; + constexpr double omega = 0.25; + const Vector first = unit_vector(3, 1); + const Vector second = unit_vector(3, 2); + const std::vector virtual_states + = {{first, {}, 0.5}, {second, {}, 1.5}}; + const std::vector matrix_elements + = {Complex(0.4, -0.2), Complex(-0.3, 0.5)}; + + const Vector response = ModuleRI::build_delta_sternheimer_sos_wavefunction( + virtual_states, matrix_elements, eps_i, omega); + + const Complex first_coefficient = matrix_elements[0] / Complex(eps_i - 0.5, -omega); + const Complex second_coefficient = matrix_elements[1] / Complex(eps_i - 1.5, -omega); + const Vector expected = {Complex(0.0, 0.0), first_coefficient, second_coefficient}; + expect_vector_near(response, expected, 1.0e-14); + + EXPECT_THROW(ModuleRI::build_delta_sternheimer_sos_wavefunction( + virtual_states, {matrix_elements[0]}, eps_i, omega), + std::invalid_argument); +} + +TEST(SternheimerDelta, AccumulateResponseFromDecomposedWavefunction) +{ + constexpr double volume_element = 1.0; + const Vector eta = unit_vector(4, 1); + const Vector residual(4, Complex(0.0, 0.0)); + const Vector out = {Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.4, -0.1), Complex(-0.3, 0.2)}; + const Complex coefficient(-0.6, 0.5); + + ModuleRI::SternheimerDeltaPostprocessResult result; + result.out_wavefunction = out; + result.coefficients = {coefficient}; + result.reconstructed_wavefunction = out; + for (std::size_t ir = 0; ir != result.reconstructed_wavefunction.size(); ++ir) + { + result.reconstructed_wavefunction[ir] += coefficient * eta[ir]; + } + + const Vector probe = {Complex(0.0, 0.0), Complex(1.5, -0.2), Complex(-0.4, 0.1), Complex(0.7, 0.3)}; + const std::vector virtual_states = {{eta, residual, 0.75}}; + + const Complex decomposed + = ModuleRI::accumulate_delta_sternheimer_response(probe, result, virtual_states, volume_element); + const Complex direct = ModuleRI::sternheimer_fd_grid_dot(probe, result.reconstructed_wavefunction, volume_element); + + EXPECT_NEAR(decomposed.real(), direct.real(), 1.0e-14); + EXPECT_NEAR(decomposed.imag(), direct.imag(), 1.0e-14); +} + +TEST(SternheimerDelta, SolvesGeneralMetricSubspaceEquationByComponents) +{ + const std::vector overlap = {Complex(1.0, 0.0), Complex(0.2, -0.1), Complex(0.2, 0.1), Complex(1.3, 0.0)}; + const std::vector hamiltonian + = {Complex(0.8, 0.0), Complex(0.15, -0.05), Complex(0.15, 0.05), Complex(1.4, 0.0)}; + const Complex shift(-0.25, -0.4); + + std::vector shifted_operator(4, Complex(0.0, 0.0)); + for (std::size_t index = 0; index != shifted_operator.size(); ++index) + { + shifted_operator[index] = hamiltonian[index] - shift * overlap[index]; + } + + const std::vector expected_sos = {Complex(0.3, -0.2), Complex(-0.1, 0.4)}; + const std::vector expected_pulay = {Complex(-0.05, 0.15), Complex(0.2, -0.1)}; + const std::vector rhs = column_major_matrix_vector_product(shifted_operator, expected_sos); + std::vector hamiltonian_out = column_major_matrix_vector_product(shifted_operator, expected_pulay); + for (Complex& value: hamiltonian_out) + { + value = -value; + } + + const auto result + = ModuleRI::solve_delta_sternheimer_subspace_coefficients(hamiltonian, overlap, rhs, hamiltonian_out, shift); + + ASSERT_EQ(result.sos.size(), 2); + ASSERT_EQ(result.pulay.size(), 2); + ASSERT_EQ(result.total.size(), 2); + for (std::size_t index = 0; index != 2; ++index) + { + EXPECT_NEAR(result.sos[index].real(), expected_sos[index].real(), 1.0e-13); + EXPECT_NEAR(result.sos[index].imag(), expected_sos[index].imag(), 1.0e-13); + EXPECT_NEAR(result.pulay[index].real(), expected_pulay[index].real(), 1.0e-13); + EXPECT_NEAR(result.pulay[index].imag(), expected_pulay[index].imag(), 1.0e-13); + EXPECT_NEAR(result.total[index].real(), (expected_sos[index] + expected_pulay[index]).real(), 1.0e-13); + EXPECT_NEAR(result.total[index].imag(), (expected_sos[index] + expected_pulay[index]).imag(), 1.0e-13); + } +} + +TEST(SternheimerDelta, AssemblesReferenceGridHamiltonianWithAnalyticGradientsAndNonlocalProjector) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{2, 1, 1, 1.0, 1.0, 1.0, false}; + constexpr double volume_element = 0.5; + using Projector = ModuleRI::SternheimerFDNonlocalProjector; + Projector::ProjectorBlock block; + block.projectors = {{Complex(1.0, 0.0), Complex(1.0, 0.0)}}; + block.d_matrix = {{Complex(3.0, 0.0)}}; + const auto nonlocal = std::make_shared(grid.size(), volume_element, + std::vector{block}); + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {2.0, -1.0}, 0.5, nonlocal); + + ModuleRI::SternheimerDeltaGridFunction basis; + basis.values = {Complex(1.0, 0.0), Complex(0.0, 1.0)}; + basis.gradients[0] = {Complex(2.0, 0.0), Complex(0.0, 0.0)}; + basis.gradients[1] = {Complex(0.0, 0.0), Complex(1.0, 0.0)}; + basis.gradients[2] = {Complex(0.0, 0.0), Complex(0.0, 0.0)}; + + const auto matrices + = ModuleRI::assemble_delta_sternheimer_grid_matrices(hamiltonian, {basis}, volume_element); + + ASSERT_EQ(matrices.overlap.size(), 1); + ASSERT_EQ(matrices.hamiltonian.size(), 1); + EXPECT_NEAR(matrices.overlap[0].real(), 1.0, 1.0e-14); + EXPECT_NEAR(matrices.overlap[0].imag(), 0.0, 1.0e-14); + // T=1.25, Vloc=0.5, and Vnl=1.5 in the same grid metric. + EXPECT_NEAR(matrices.kinetic[0].real(), 1.25, 1.0e-14); + EXPECT_NEAR(matrices.local_potential[0].real(), 0.5, 1.0e-14); + EXPECT_NEAR(matrices.nonlocal[0].real(), 1.5, 1.0e-14); + EXPECT_NEAR(matrices.hamiltonian[0].real(), 3.25, 1.0e-14); + EXPECT_NEAR(matrices.hamiltonian[0].imag(), 0.0, 1.0e-14); + EXPECT_NEAR((matrices.kinetic[0] + matrices.local_potential[0] + matrices.nonlocal[0] + - matrices.hamiltonian[0]) + .real(), + 0.0, + 1.0e-14); +} + +TEST(SternheimerDelta, CombinesLCAOCoefficientsWithAOValuesAndAnalyticGradients) +{ + ModuleRI::SternheimerDeltaGridFunction first; + first.values = {Complex(1.0, 0.0), Complex(0.0, 1.0)}; + first.gradients[0] = {Complex(2.0, 0.0), Complex(0.0, 0.0)}; + first.gradients[1] = {Complex(0.0, 1.0), Complex(1.0, 0.0)}; + first.gradients[2] = {Complex(0.0, 0.0), Complex(0.0, -1.0)}; + + ModuleRI::SternheimerDeltaGridFunction second; + second.values = {Complex(0.0, -1.0), Complex(3.0, 0.0)}; + second.gradients[0] = {Complex(1.0, 1.0), Complex(0.0, 2.0)}; + second.gradients[1] = {Complex(-1.0, 0.0), Complex(0.0, 0.0)}; + second.gradients[2] = {Complex(2.0, 0.0), Complex(1.0, 1.0)}; + + const Complex first_coefficient(1.0, 0.5); + const Complex second_coefficient(-0.25, 2.0); + const auto state = ModuleRI::linear_combination_delta_sternheimer_grid_functions( + {first, second}, {first_coefficient, second_coefficient}); + + auto combine = [first_coefficient, second_coefficient](const Vector& first_values, + const Vector& second_values) { + Vector expected(first_values.size(), Complex(0.0, 0.0)); + for (std::size_t ir = 0; ir != expected.size(); ++ir) + { + expected[ir] = first_coefficient * first_values[ir] + + second_coefficient * second_values[ir]; + } + return expected; + }; + expect_vector_near(state.values, combine(first.values, second.values), 1.0e-14); + for (int direction = 0; direction != 3; ++direction) + { + expect_vector_near(state.gradients[static_cast(direction)], + combine(first.gradients[static_cast(direction)], + second.gradients[static_cast(direction)]), + 1.0e-14); + } + + EXPECT_THROW(ModuleRI::linear_combination_delta_sternheimer_grid_functions({first, second}, + {first_coefficient}), + std::invalid_argument); +} + +TEST(SternheimerDelta, AccumulatesBlochImagePhaseIntoValuesAndEveryGradient) +{ + std::vector functions(1); + functions[0].values.assign(2, Complex(0.0, 0.0)); + for (Vector& gradient : functions[0].gradients) + { + gradient.assign(2, Complex(0.0, 0.0)); + } + + const std::vector first_values{2.0, -1.0}; + const std::array, 3> first_gradients{{ + {3.0, 4.0}, + {-2.0, 1.0}, + {0.5, -0.25}, + }}; + ModuleRI::accumulate_delta_sternheimer_bloch_samples(first_values, + first_gradients, + 2, + 1, + 0, + 0, + {0.25, 0.0, 0.0}, + {0, 0, 0}, + functions); + + const std::vector second_values{5.0, 7.0}; + const std::array, 3> second_gradients{{ + {1.0, -3.0}, + {2.5, 6.0}, + {-4.0, 8.0}, + }}; + ModuleRI::accumulate_delta_sternheimer_bloch_samples(second_values, + second_gradients, + 2, + 1, + 0, + 0, + {0.25, 0.0, 0.0}, + {1, 0, 0}, + functions); + + const Complex imaginary_phase(0.0, 1.0); + expect_vector_near(functions[0].values, + {Complex(2.0, 0.0) + imaginary_phase * 5.0, + Complex(-1.0, 0.0) + imaginary_phase * 7.0}, + 1.0e-14); + for (std::size_t direction = 0; direction != functions[0].gradients.size(); ++direction) + { + const Vector expected{ + Complex(first_gradients[direction][0], 0.0) + + imaginary_phase * second_gradients[direction][0], + Complex(first_gradients[direction][1], 0.0) + + imaginary_phase * second_gradients[direction][1], + }; + expect_vector_near(functions[0].gradients[direction], expected, 1.0e-14); + } + + std::vector gamma_functions(1); + gamma_functions[0].values.assign(2, Complex(0.0, 0.0)); + for (Vector& gradient : gamma_functions[0].gradients) + { + gradient.assign(2, Complex(0.0, 0.0)); + } + ModuleRI::accumulate_delta_sternheimer_bloch_samples(first_values, + first_gradients, + 2, + 1, + 0, + 0, + {0.0, 0.0, 0.0}, + {2, -1, 4}, + gamma_functions); + ModuleRI::accumulate_delta_sternheimer_bloch_samples(second_values, + second_gradients, + 2, + 1, + 0, + 0, + {0.0, 0.0, 0.0}, + {-3, 5, 1}, + gamma_functions); + expect_vector_near(gamma_functions[0].values, {Complex(7.0, 0.0), Complex(6.0, 0.0)}, 1.0e-14); + for (std::size_t direction = 0; direction != gamma_functions[0].gradients.size(); ++direction) + { + const Vector expected{ + Complex(first_gradients[direction][0] + second_gradients[direction][0], 0.0), + Complex(first_gradients[direction][1] + second_gradients[direction][1], 0.0), + }; + expect_vector_near(gamma_functions[0].gradients[direction], expected, 1.0e-14); + } +} + +TEST(SternheimerDelta, AccumulatesLCAOStatesWithoutMaterializingAOGridFunctions) +{ + std::vector states(2); + for (auto& state: states) + { + state.values.assign(3, Complex(0.0, 0.0)); + for (auto& gradient: state.gradients) + { + gradient.assign(3, Complex(0.0, 0.0)); + } + } + const std::vector values{1.0, 3.0, 2.0, 4.0}; + const std::array, 3> gradients{{ + {10.0, 30.0, 20.0, 40.0}, + {100.0, 300.0, 200.0, 400.0}, + {1000.0, 3000.0, 2000.0, 4000.0}, + }}; + const std::vector> coefficients{ + {Complex(1.0, 0.0), Complex(0.0, 0.0)}, + {Complex(0.0, 0.0), Complex(0.0, 2.0)}, + }; + + ModuleRI::accumulate_delta_sternheimer_lcao_state_samples(values, + gradients, + 2, + 2, + 1, + 0, + coefficients, + {0.5, 0.0, 0.0}, + {1, 0, 0}, + states); + + EXPECT_NEAR(std::abs(states[0].values[0]), 0.0, 1.0e-14); + EXPECT_NEAR(states[0].values[1].real(), -1.0, 1.0e-14); + EXPECT_NEAR(states[0].values[2].real(), -2.0, 1.0e-14); + EXPECT_NEAR(states[1].values[1].imag(), -6.0, 1.0e-14); + EXPECT_NEAR(states[1].values[2].imag(), -8.0, 1.0e-14); + EXPECT_NEAR(states[0].gradients[0][1].real(), -10.0, 1.0e-14); + EXPECT_NEAR(states[1].gradients[2][2].imag(), -8000.0, 1.0e-12); +} + +TEST(SternheimerDelta, OrthonormalizesOccupiedProjectorValuesAndGradientsTogether) +{ + constexpr double volume_element = 1.0; + + ModuleRI::SternheimerDeltaGridFunction first; + first.values = {Complex(2.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; + first.gradients[0] = {Complex(4.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; + first.gradients[1] = {Complex(0.0, 2.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; + first.gradients[2] = {Complex(-2.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; + + ModuleRI::SternheimerDeltaGridFunction second; + second.values = {Complex(3.0, 0.0), Complex(4.0, 0.0), Complex(0.0, 0.0)}; + second.gradients[0] = {Complex(11.0, 0.0), Complex(20.0, 0.0), Complex(0.0, 0.0)}; + second.gradients[1] = {Complex(0.0, 7.0), Complex(0.0, 12.0), Complex(0.0, 0.0)}; + second.gradients[2] = {Complex(-5.0, 0.0), Complex(8.0, 0.0), Complex(0.0, 0.0)}; + + const auto projector = ModuleRI::orthonormalize_delta_sternheimer_grid_functions( + {first, second}, volume_element, 1.0e-12); + + ASSERT_EQ(projector.size(), 2); + expect_vector_near(projector[0].values, + {Complex(1.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}, + 1.0e-14); + expect_vector_near(projector[1].values, + {Complex(0.0, 0.0), Complex(1.0, 0.0), Complex(0.0, 0.0)}, + 1.0e-14); + expect_vector_near(projector[1].gradients[0], + {Complex(1.25, 0.0), Complex(5.0, 0.0), Complex(0.0, 0.0)}, + 1.0e-14); + expect_vector_near(projector[1].gradients[1], + {Complex(0.0, 1.0), Complex(0.0, 3.0), Complex(0.0, 0.0)}, + 1.0e-14); + expect_vector_near(projector[1].gradients[2], + {Complex(-0.5, 0.0), Complex(2.0, 0.0), Complex(0.0, 0.0)}, + 1.0e-14); + + const Complex overlap00 + = ModuleRI::sternheimer_fd_grid_dot(projector[0].values, projector[0].values, volume_element); + const Complex overlap01 + = ModuleRI::sternheimer_fd_grid_dot(projector[0].values, projector[1].values, volume_element); + const Complex overlap11 + = ModuleRI::sternheimer_fd_grid_dot(projector[1].values, projector[1].values, volume_element); + EXPECT_NEAR(overlap00.real(), 1.0, 1.0e-14); + EXPECT_NEAR(std::abs(overlap01), 0.0, 1.0e-14); + EXPECT_NEAR(overlap11.real(), 1.0, 1.0e-14); +} + +TEST(SternheimerDelta, ReferenceSubspaceTransformsValuesAndGradientsTogether) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{2, 1, 1, 1.0, 1.0, 1.0, false}; + constexpr double volume_element = 1.0; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {0.0, 0.0}, 0.5); + + ModuleRI::SternheimerDeltaGridFunction occupied; + occupied.values = {Complex(1.0, 0.0), Complex(0.0, 0.0)}; + occupied.gradients[0] = {Complex(1.0, 0.0), Complex(0.0, 0.0)}; + occupied.gradients[1] = Vector(2, Complex(0.0, 0.0)); + occupied.gradients[2] = Vector(2, Complex(0.0, 0.0)); + + ModuleRI::SternheimerDeltaGridFunction candidate; + candidate.values = {Complex(1.0, 0.0), Complex(1.0, 0.0)}; + candidate.gradients[0] = {Complex(2.0, 0.0), Complex(3.0, 0.0)}; + candidate.gradients[1] = Vector(2, Complex(0.0, 0.0)); + candidate.gradients[2] = Vector(2, Complex(0.0, 0.0)); + + ModuleRI::SternheimerDeltaSubspaceOptions options; + options.max_virtual_states = 1; + const auto subspace = ModuleRI::build_reference_delta_sternheimer_subspace( + hamiltonian, {occupied}, {candidate}, volume_element, options); + + ASSERT_EQ(subspace.virtual_states.size(), 1); + ASSERT_EQ(subspace.grid_functions.size(), 1); + expect_vector_near(subspace.grid_functions[0].values, + {Complex(0.0, 0.0), Complex(1.0, 0.0)}, + 1.0e-14); + expect_vector_near(subspace.grid_functions[0].gradients[0], + {Complex(1.0, 0.0), Complex(3.0, 0.0)}, + 1.0e-14); + EXPECT_NEAR(subspace.virtual_states[0].eigenvalue, 5.0, 1.0e-13); + Vector h_virtual; + hamiltonian.apply(subspace.virtual_states[0].orbital, h_virtual); + const double grid_diagonal = std::real(ModuleRI::sternheimer_fd_grid_dot( + subspace.virtual_states[0].orbital, h_virtual, volume_element)); + const double expected_difference = std::abs(grid_diagonal - subspace.virtual_states[0].eigenvalue); + EXPECT_NEAR(subspace.full_grid_hamiltonian_max_abs_difference, expected_difference, 1.0e-13); + EXPECT_NEAR(subspace.full_grid_hamiltonian_relative_difference, + expected_difference / std::abs(subspace.virtual_states[0].eigenvalue), + 1.0e-13); +} + +TEST(SternheimerDelta, ProductionOptionsDropGridDiagnosticsWithoutChangingVirtualState) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{2, 1, 1, 1.0, 1.0, 1.0, false}; + constexpr double volume_element = 1.0; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {0.0, 0.0}, 0.5); + + ModuleRI::SternheimerDeltaGridFunction occupied; + occupied.values = {Complex(1.0, 0.0), Complex(0.0, 0.0)}; + occupied.gradients[0] = occupied.values; + occupied.gradients[1] = Vector(2, Complex(0.0, 0.0)); + occupied.gradients[2] = Vector(2, Complex(0.0, 0.0)); + + ModuleRI::SternheimerDeltaGridFunction candidate; + candidate.values = {Complex(0.0, 0.0), Complex(1.0, 0.0)}; + candidate.gradients[0] = {Complex(0.0, 0.0), Complex(3.0, 0.0)}; + candidate.gradients[1] = Vector(2, Complex(0.0, 0.0)); + candidate.gradients[2] = Vector(2, Complex(0.0, 0.0)); + + ModuleRI::SternheimerDeltaSubspaceOptions reference_options; + reference_options.max_virtual_states = 1; + const auto reference = ModuleRI::build_reference_delta_sternheimer_subspace( + hamiltonian, {occupied}, {candidate}, volume_element, reference_options); + + ModuleRI::SternheimerDeltaSubspaceOptions production_options = reference_options; + production_options.retain_grid_functions = false; + production_options.evaluate_full_grid_difference = false; + const auto production = ModuleRI::build_reference_delta_sternheimer_subspace( + hamiltonian, {occupied}, {candidate}, volume_element, production_options); + + ASSERT_EQ(production.virtual_states.size(), 1); + EXPECT_TRUE(production.grid_functions.empty()); + expect_vector_near(production.virtual_states[0].orbital, + reference.virtual_states[0].orbital, + 1.0e-14); + expect_vector_near(production.virtual_states[0].residual, + reference.virtual_states[0].residual, + 1.0e-14); + EXPECT_NEAR(production.virtual_states[0].eigenvalue, + reference.virtual_states[0].eigenvalue, + 1.0e-14); + EXPECT_EQ(production.full_grid_hamiltonian_relative_difference, -1.0); + EXPECT_EQ(production.full_grid_hamiltonian_max_abs_difference, -1.0); +} + +TEST(SternheimerDelta, ReferenceSubspacePivotsPastNearDependentCandidates) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{4, 1, 1, 1.0, 1.0, 1.0, false}; + constexpr double volume_element = 1.0; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {0.0, 1.0, 2.0, 3.0}, 0.0); + + auto grid_function = [](const Vector& values) { + ModuleRI::SternheimerDeltaGridFunction function; + function.values = values; + for (Vector& gradient: function.gradients) + { + gradient.assign(values.size(), Complex(0.0, 0.0)); + } + return function; + }; + + const auto occupied = grid_function(unit_vector(4, 0)); + Vector nearly_occupied = unit_vector(4, 0); + nearly_occupied[1] = Complex(1.0e-8, 0.0); + const std::vector candidates{ + grid_function(nearly_occupied), + grid_function(unit_vector(4, 2)), + grid_function(unit_vector(4, 3)), + }; + + ModuleRI::SternheimerDeltaSubspaceOptions options; + options.max_virtual_states = 2; + options.norm_tolerance = 1.0e-10; + const auto subspace = ModuleRI::build_reference_delta_sternheimer_subspace( + hamiltonian, {occupied}, candidates, volume_element, options); + + ASSERT_EQ(subspace.virtual_states.size(), 2); + EXPECT_EQ(subspace.accepted_candidates, 2); + EXPECT_EQ(subspace.discarded_candidates, 1); + EXPECT_NEAR(subspace.virtual_states[0].eigenvalue, 2.0, 1.0e-13); + EXPECT_NEAR(subspace.virtual_states[1].eigenvalue, 3.0, 1.0e-13); +} + +TEST(SternheimerDelta, FastReferenceMatricesMatchScalarComponents) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Projector = ModuleRI::SternheimerFDNonlocalProjector; + Hamiltonian::Grid grid{5, 1, 1, 1.0, 1.0, 1.0, false}; + Projector::ProjectorBlock block; + block.projectors = {{Complex(0.3, 0.1), Complex(-0.1, 0.4), Complex(0.2, -0.3), + Complex(0.1, 0.2), Complex(-0.5, 0.1)}}; + block.d_matrix = {{Complex(1.7, 0.0)}}; + auto projector = std::make_shared(5, 0.7, std::vector{block}); + Hamiltonian hamiltonian(grid, {0.1, -0.2, 0.3, 0.4, -0.5}, 1.0, projector); + std::vector basis(3); + for (std::size_t i = 0; i < basis.size(); ++i) + { + for (int j = 0; j < 5; ++j) + { + const Complex z(std::sin(double((i + 1) * (j + 1))), std::cos(double(i + j))); + basis[i].values.push_back(z); + for (int d = 0; d < 3; ++d) + { + basis[i].gradients[d].push_back(z * Complex(0.2 * (d + 1), 0.1 * j)); + } + } + } + const auto scalar = ModuleRI::assemble_delta_sternheimer_grid_matrices(hamiltonian, basis, 0.7); + const auto fast = ModuleRI::assemble_delta_sternheimer_grid_matrices_fast(hamiltonian, basis, 0.7); + for (const auto pair : {std::make_pair(&scalar.overlap, &fast.overlap), + std::make_pair(&scalar.kinetic, &fast.kinetic), + std::make_pair(&scalar.local_potential, &fast.local_potential), + std::make_pair(&scalar.nonlocal, &fast.nonlocal), + std::make_pair(&scalar.hamiltonian, &fast.hamiltonian)}) + { + expect_vector_near(*pair.first, *pair.second, 1.0e-12); + } +} + +TEST(SternheimerDelta, CrossMatricesMatchIndependentObliqueComplexQuadrature) +{ + const WeakCrossFixture fixture; + const auto hamiltonian = fixture.hamiltonian(); + const auto left = fixture.functions(2, 0.1); + const auto right = fixture.functions(3, -0.2); + const auto actual = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, left, right, fixture.volume_element); + expect_grid_matrices_near(actual, scalar_cross_matrices( + hamiltonian, left, right, fixture.volume_element), 2.0e-12); + EXPECT_GT(std::abs(actual.overlap[0].imag()), 1.0e-3); + EXPECT_GT(std::abs(actual.nonlocal[0]), 1.0e-5); +} + +TEST(SternheimerDelta, CrossMatricesReverseToAdjointWithoutSquareCleanup) +{ + const WeakCrossFixture fixture; + const auto hamiltonian = fixture.hamiltonian(); + const auto left = fixture.functions(2, 0.1); + for (const int columns: {2, 3}) + { + const auto right = fixture.functions(columns, -0.2); + const auto lr = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, left, right, fixture.volume_element); + const auto rl = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, right, left, fixture.volume_element); + for (const auto component: grid_matrix_components) + { + for (std::size_t row = 0; row != left.size(); ++row) + { + for (std::size_t column = 0; column != right.size(); ++column) + { + EXPECT_NEAR(std::abs((lr.*component)[row + left.size() * column] + - std::conj((rl.*component)[column + right.size() * row])), + 0.0, 2.0e-12); + } + } + } + EXPECT_GT(std::abs(lr.overlap[0].imag()), 1.0e-3); + } +} + +TEST(SternheimerDelta, CrossMatricesSameBasisMatchBothSquareAssemblies) +{ + const WeakCrossFixture fixture; + const auto hamiltonian = fixture.hamiltonian(); + const auto basis = fixture.functions(3, 0.1); + const auto cross = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, basis, basis, fixture.volume_element); + expect_grid_matrices_near(cross, ModuleRI::assemble_delta_sternheimer_grid_matrices_fast( + hamiltonian, basis, fixture.volume_element), 2.0e-12); + expect_grid_matrices_near(cross, ModuleRI::assemble_delta_sternheimer_grid_matrices( + hamiltonian, basis, fixture.volume_element), 2.0e-12); +} + +TEST(SternheimerDelta, CrossMatricesPreserveUnequalComplexNormalization) +{ + const WeakCrossFixture fixture; + const auto hamiltonian = fixture.hamiltonian(); + auto left = fixture.functions(2, 0.1); + auto right = fixture.functions(3, -0.2); + auto expected = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, left, right, fixture.volume_element); + const std::array left_scale{{Complex(2.0, -0.4), Complex(0.1, 0.2)}}; + const std::array right_scale{{Complex(0.3, 0.7), Complex(-1.1, 0.2), Complex(3.0, -0.1)}}; + for (std::size_t row = 0; row != left.size(); ++row) + { + for (int component = -1; component != 3; ++component) + { + Vector& values = component < 0 ? left[row].values : left[row].gradients[component]; + for (Complex& value: values) + { + value *= left_scale[row]; + } + } + } + for (std::size_t column = 0; column != right.size(); ++column) + { + for (int component = -1; component != 3; ++component) + { + Vector& values = component < 0 ? right[column].values : right[column].gradients[component]; + for (Complex& value: values) + { + value *= right_scale[column]; + } + } + for (std::size_t row = 0; row != left.size(); ++row) + { + for (const auto component: grid_matrix_components) + { + (expected.*component)[row + left.size() * column] + *= std::conj(left_scale[row]) * right_scale[column]; + } + } + } + expect_grid_matrices_near(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, left, right, fixture.volume_element), expected, 3.0e-12); +} + +TEST(SternheimerDelta, CrossMatricesUseAnalyticGradientsAndHamiltonianRyPrefactor) +{ + const WeakCrossFixture fixture; + auto left = fixture.functions(2, 0.1); + const auto right = fixture.functions(3, -0.2); + const auto ry = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + fixture.hamiltonian(false, 1.0), left, right, fixture.volume_element); + const auto hartree = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + fixture.hamiltonian(false, 0.5), left, right, fixture.volume_element); + for (std::size_t i = 0; i != ry.kinetic.size(); ++i) + { + EXPECT_NEAR(std::abs(ry.kinetic[i] - 2.0 * hartree.kinetic[i]), 0.0, 1.0e-12); + EXPECT_EQ(ry.nonlocal[i], Complex(0.0, 0.0)); + } + for (auto& function: left) + { + for (auto& gradient: function.gradients) + { + std::fill(gradient.begin(), gradient.end(), Complex(0.0, 0.0)); + } + } + const auto zero = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + fixture.hamiltonian(false), left, right, fixture.volume_element); + expect_vector_near(zero.overlap, ry.overlap, 0.0); + for (const Complex value: zero.kinetic) + { + EXPECT_EQ(value, Complex(0.0, 0.0)); + } +} + +TEST(SternheimerDelta, CrossMatricesHandleGridAndNonlocalColumnBlockRemainders) +{ + const WeakCrossFixture fixture(4103, 1, 1); + const auto hamiltonian = fixture.hamiltonian(); + const auto left = fixture.functions(2, 0.1); + const auto right = fixture.functions(65, -0.2); + expect_grid_matrices_near(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, left, right, fixture.volume_element), scalar_cross_matrices( + hamiltonian, left, right, fixture.volume_element), 2.0e-9); +} + +TEST(SternheimerDelta, CrossMatricesKeepExplicitEmptyDimensions) +{ + const WeakCrossFixture fixture; + const auto hamiltonian = fixture.hamiltonian(); + const auto basis = fixture.functions(2, 0.1); + const auto left_empty = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, {}, basis, fixture.volume_element); + const auto right_empty = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, basis, {}, fixture.volume_element); + const auto both_empty = ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, {}, {}, fixture.volume_element); + EXPECT_EQ(left_empty.row_count, 0u); + EXPECT_EQ(left_empty.column_count, 2u); + EXPECT_EQ(right_empty.row_count, 2u); + EXPECT_EQ(right_empty.column_count, 0u); + EXPECT_EQ(both_empty.row_count, 0u); + EXPECT_EQ(both_empty.column_count, 0u); + for (const auto component: grid_matrix_components) + { + EXPECT_TRUE((left_empty.*component).empty()); + EXPECT_TRUE((right_empty.*component).empty()); + EXPECT_TRUE((both_empty.*component).empty()); + } +} + +TEST(SternheimerDelta, CrossMatricesRejectEitherSideValueAndGradientDimensions) +{ + const WeakCrossFixture fixture; + const auto hamiltonian = fixture.hamiltonian(); + const auto valid = fixture.functions(2, 0.1); + for (int component = -1; component != 3; ++component) + { + auto invalid = valid; + (component < 0 ? invalid[1].values : invalid[1].gradients[component]).pop_back(); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, invalid, valid, fixture.volume_element), std::invalid_argument); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, valid, invalid, fixture.volume_element), std::invalid_argument); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, {}, invalid, fixture.volume_element), std::invalid_argument); + } +} + +TEST(SternheimerDelta, CrossMatricesRejectNonfiniteSamplesOnEitherSide) +{ + const WeakCrossFixture fixture; + const auto hamiltonian = fixture.hamiltonian(); + const auto valid = fixture.functions(2, 0.1); + for (const double bad: {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity()}) + { + for (int component = -1; component != 3; ++component) + { + auto invalid = valid; + (component < 0 ? invalid[1].values : invalid[1].gradients[component])[0] = Complex(0.0, bad); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, invalid, valid, fixture.volume_element), std::invalid_argument); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + hamiltonian, valid, invalid, fixture.volume_element), std::invalid_argument); + } + } +} + +TEST(SternheimerDelta, CrossMatricesRejectInvalidWeightsAndHamiltonianData) +{ + const WeakCrossFixture fixture; + const auto valid = fixture.functions(1, 0.1); + const double nan = std::numeric_limits::quiet_NaN(); + const double inf = std::numeric_limits::infinity(); + for (const double bad: {0.0, -1.0, nan, inf}) + { + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + fixture.hamiltonian(), valid, valid, bad), std::invalid_argument); + } + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + fixture.hamiltonian(), valid, valid, 2.0 * fixture.volume_element), std::invalid_argument); + for (const double bad: {nan, inf}) + { + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + fixture.hamiltonian(false, bad), valid, valid, fixture.volume_element), std::invalid_argument); + auto modified = fixture; + modified.potential[0] = bad; + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + modified.hamiltonian(), valid, valid, fixture.volume_element), std::invalid_argument); + modified = fixture; + modified.block.projectors[0][0] = Complex(bad, 0.0); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + modified.hamiltonian(), valid, valid, fixture.volume_element), std::invalid_argument); + modified = fixture; + modified.block.d_matrix[0][1] = Complex(0.0, bad); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + modified.hamiltonian(), valid, valid, fixture.volume_element), std::invalid_argument); + } +} + +TEST(SternheimerDelta, CrossMatricesRejectFiniteArithmeticOverflow) +{ + Hamiltonian::Grid grid{1, 1, 1, 1.0, 1.0, 1.0, false}; + GridFunction huge; + huge.values = {Complex(1.0e200, 0.0)}; + for (auto& gradient: huge.gradients) + { + gradient = {Complex(0.0, 0.0)}; + } + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + Hamiltonian(grid, {0.0}, 0.0), {huge}, {huge}, 1.0), std::overflow_error); + huge.values[0] = Complex(1.0, 0.0); + huge.gradients[1][0] = Complex(1.0e200, 0.0); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + Hamiltonian(grid, {0.0}, 1.0), {huge}, {huge}, 1.0), std::overflow_error); + huge.gradients[1][0] = Complex(0.0, 0.0); + huge.values[0] = Complex(2.0, 0.0); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + Hamiltonian(grid, {1.0e308}, 0.0), {huge}, {huge}, 1.0), std::overflow_error); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + Hamiltonian(grid, {0.0}, std::numeric_limits::max()), + {huge}, {huge}, 2.0), std::overflow_error); + NonlocalProjector::ProjectorBlock block; + block.projectors = {{Complex(1.0e150, 0.0)}}; + block.d_matrix = {{Complex(1.0e100, 0.0)}}; + const auto projector = std::make_shared( + 1, 1.0, std::vector{block}); + EXPECT_THROW(ModuleRI::assemble_delta_sternheimer_cross_matrices_fast( + Hamiltonian(grid, {0.0}, 0.0, projector), {huge}, {huge}, 1.0), std::overflow_error); +} + +TEST(SternheimerDelta, DefaultFullReferenceSubspaceUsesBlockPathAndMatchesLegacy) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 1.0, 1.0, 1.0, false}; + constexpr double volume_element = 1.0; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {0.0, 0.7, 1.9, 3.2, 5.1}, 0.4); + + auto grid_function = [](const Vector& values, const double gradient_scale) { + ModuleRI::SternheimerDeltaGridFunction function; + function.values = values; + for (std::size_t direction = 0; direction != function.gradients.size(); ++direction) + { + function.gradients[direction].resize(values.size()); + for (std::size_t ir = 0; ir != values.size(); ++ir) + { + const double factor = gradient_scale * static_cast((direction + 1) * (ir + 1)); + function.gradients[direction][ir] = factor * values[ir]; + } + } + return function; + }; + + const auto occupied = grid_function(unit_vector(5, 0), 0.03); + Vector candidate0 = unit_vector(5, 1); + candidate0[0] = Complex(0.2, -0.1); + Vector candidate1 = unit_vector(5, 2); + candidate1[1] = Complex(0.3, 0.2); + Vector candidate2 = unit_vector(5, 4); + candidate2[2] = Complex(-0.25, 0.15); + candidate2[3] = Complex(0.4, -0.1); + const std::vector candidates{ + grid_function(candidate0, 0.05), + grid_function(candidate1, 0.07), + grid_function(candidate2, 0.09), + }; + + ModuleRI::SternheimerDeltaSubspaceOptions block_options; + // Zero is the production spelling for the complete virtual space. + block_options.max_virtual_states = 0; + block_options.evaluate_full_grid_difference = false; + ModuleRI::SternheimerDeltaSubspaceOptions legacy_options = block_options; + legacy_options.use_block_generalized_eigensolver = false; + + const auto block = ModuleRI::build_reference_delta_sternheimer_subspace( + hamiltonian, {occupied}, candidates, volume_element, block_options); + const auto legacy = ModuleRI::build_reference_delta_sternheimer_subspace( + hamiltonian, {occupied}, candidates, volume_element, legacy_options); + + EXPECT_TRUE(block.used_block_generalized_eigensolver); + EXPECT_FALSE(legacy.used_block_generalized_eigensolver); + ASSERT_EQ(block.virtual_states.size(), legacy.virtual_states.size()); + EXPECT_EQ(block.accepted_candidates, legacy.accepted_candidates); + EXPECT_EQ(block.discarded_candidates, legacy.discarded_candidates); + for (std::size_t istate = 0; istate != block.virtual_states.size(); ++istate) + { + EXPECT_NEAR(block.virtual_states[istate].eigenvalue, + legacy.virtual_states[istate].eigenvalue, + 1.0e-11); + const Complex overlap = ModuleRI::sternheimer_fd_grid_dot( + legacy.virtual_states[istate].orbital, + block.virtual_states[istate].orbital, + volume_element); + EXPECT_NEAR(std::abs(overlap), 1.0, 1.0e-11); + const Complex residual_overlap = ModuleRI::sternheimer_fd_grid_dot( + legacy.virtual_states[istate].residual, + block.virtual_states[istate].residual, + volume_element); + const double legacy_residual_norm = ModuleRI::sternheimer_fd_grid_norm( + legacy.virtual_states[istate].residual, volume_element); + const double block_residual_norm = ModuleRI::sternheimer_fd_grid_norm( + block.virtual_states[istate].residual, volume_element); + EXPECT_NEAR(block_residual_norm, legacy_residual_norm, 1.0e-11); + EXPECT_NEAR(std::abs(residual_overlap), + block_residual_norm * legacy_residual_norm, + 1.0e-11); + } +} + +TEST(SternheimerDelta, CapsRequestedVirtualStatesAtTheLCAOComplementDimension) +{ + EXPECT_EQ(ModuleRI::sternheimer_delta_virtual_state_limit(0, 176, 16), 160); + EXPECT_EQ(ModuleRI::sternheimer_delta_virtual_state_limit(40, 176, 16), 40); + EXPECT_EQ(ModuleRI::sternheimer_delta_virtual_state_limit(200, 176, 16), 160); + EXPECT_EQ(ModuleRI::sternheimer_delta_virtual_state_limit(0, 16, 16), 0); + EXPECT_THROW(ModuleRI::sternheimer_delta_virtual_state_limit(-1, 176, 16), std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_delta_virtual_state_limit(0, 15, 16), std::invalid_argument); +} + +TEST(SternheimerDelta, BuildsPeriodicCenteredGradientForOccupiedGridState) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + Vector values(static_cast(grid.size()), Complex(0.0, 0.0)); + for (int ix = 0; ix != grid.nx; ++ix) + { + const double phase = 0.5 * std::acos(-1.0) * static_cast(ix); + values[static_cast(ix)] = std::exp(Complex(0.0, phase)); + } + + const auto function = ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients(values, grid); + + expect_vector_near(function.values, values, 1.0e-14); + for (int ix = 0; ix != grid.nx; ++ix) + { + const Complex expected = Complex(0.0, 2.0) * values[static_cast(ix)]; + EXPECT_NEAR(function.gradients[0][static_cast(ix)].real(), expected.real(), 1.0e-14); + EXPECT_NEAR(function.gradients[0][static_cast(ix)].imag(), expected.imag(), 1.0e-14); + EXPECT_NEAR(std::abs(function.gradients[1][static_cast(ix)]), 0.0, 1.0e-14); + EXPECT_NEAR(std::abs(function.gradients[2][static_cast(ix)]), 0.0, 1.0e-14); + } +} + +TEST(SternheimerDelta, EnumeratesEveryPeriodicOrbitalImageInsideTheCutoff) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{12, 12, 12, 1.0, 1.0, 1.0, true}; + const auto centered_wide + = ModuleRI::enumerate_delta_sternheimer_periodic_images(grid, {6.0, 6.0, 6.0}, 8.0); + EXPECT_EQ(centered_wide.size(), 27); + + const auto centered_local + = ModuleRI::enumerate_delta_sternheimer_periodic_images(grid, {6.0, 6.0, 6.0}, 5.0); + ASSERT_EQ(centered_local.size(), 1); + EXPECT_EQ(centered_local[0], (std::array{0, 0, 0})); + + const auto boundary_crossing + = ModuleRI::enumerate_delta_sternheimer_periodic_images(grid, {1.0, 6.0, 6.0}, 2.0); + ASSERT_EQ(boundary_crossing.size(), 2); + EXPECT_NE(std::find(boundary_crossing.begin(), boundary_crossing.end(), std::array{0, 0, 0}), + boundary_crossing.end()); + EXPECT_NE(std::find(boundary_crossing.begin(), boundary_crossing.end(), std::array{1, 0, 0}), + boundary_crossing.end()); + + grid.periodic = false; + const auto nonperiodic + = ModuleRI::enumerate_delta_sternheimer_periodic_images(grid, {6.0, 6.0, 6.0}, 8.0); + ASSERT_EQ(nonperiodic.size(), 1); + EXPECT_EQ(nonperiodic[0], (std::array{0, 0, 0})); +} + +TEST(SternheimerDelta, EnumeratesPeriodicImagesUsingNonorthogonalDualVectors) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 4; + grid.ny = 4; + grid.nz = 4; + grid.hx = std::sqrt(2.0) / 4.0; + grid.hy = std::sqrt(2.0) / 4.0; + grid.hz = std::sqrt(2.0) / 4.0; + grid.periodic = true; + grid.lattice_vectors = {{{0.0, 1.0, 1.0}, + {1.0, 0.0, 1.0}, + {1.0, 1.0, 0.0}}}; + + const auto images + = ModuleRI::enumerate_delta_sternheimer_periodic_images(grid, {0.5, 0.5, 0.5}, 0.9); + + EXPECT_EQ(images.size(), 27); + EXPECT_NE(std::find(images.begin(), images.end(), std::array{-1, 0, 0}), images.end()); +} + +TEST(SternheimerDelta, BuildsOrthogonalVirtualSubspaceWithHamiltonianResiduals) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + constexpr double volume_element = 0.5; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, std::vector(grid.size(), 0.25)); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 4, volume_element); + + Vector candidate = states.wavefunctions[2]; + for (std::size_t ir = 0; ir != candidate.size(); ++ir) + { + candidate[ir] += Complex(0.3, 0.0) * states.wavefunctions[3][ir]; + } + + ModuleRI::SternheimerDeltaSubspaceOptions options; + options.max_virtual_states = 1; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace(hamiltonian, + {states.wavefunctions[0]}, + {candidate}, + volume_element, + options); + + ASSERT_EQ(subspace.virtual_states.size(), 1); + EXPECT_EQ(subspace.accepted_candidates, 1); + EXPECT_EQ(subspace.discarded_candidates, 0); + EXPECT_NEAR(ModuleRI::sternheimer_fd_grid_norm(subspace.virtual_states[0].orbital, volume_element), + 1.0, + 1.0e-12); + EXPECT_NEAR(std::abs(ModuleRI::sternheimer_fd_grid_dot(states.wavefunctions[0], + subspace.virtual_states[0].orbital, + volume_element)), + 0.0, + 1.0e-12); + + Vector h_eta; + hamiltonian.apply(subspace.virtual_states[0].orbital, h_eta); + for (std::size_t ir = 0; ir != h_eta.size(); ++ir) + { + h_eta[ir] -= subspace.virtual_states[0].eigenvalue * subspace.virtual_states[0].orbital[ir]; + } + ModuleRI::SternheimerRPA::project_out_subspace({states.wavefunctions[0], subspace.virtual_states[0].orbital}, + [volume_element](const Vector& lhs, const Vector& rhs) { + return ModuleRI::sternheimer_fd_grid_dot(lhs, + rhs, + volume_element); + }, + h_eta); + expect_vector_near(subspace.virtual_states[0].residual, h_eta, 1.0e-12); +} + +TEST(SternheimerDelta, ParsesDiagnosticABlockModes) +{ + using Mode = ModuleRI::SternheimerDeltaABlockMode; + + EXPECT_EQ(ModuleRI::parse_sternheimer_delta_a_block_mode("reference_value_gradient"), + Mode::ReferenceValueGradient); + EXPECT_EQ(ModuleRI::parse_sternheimer_delta_a_block_mode("grid"), Mode::FullGrid); + EXPECT_STREQ(ModuleRI::sternheimer_delta_a_block_mode_name(Mode::ReferenceValueGradient), + "reference_value_gradient"); + EXPECT_STREQ(ModuleRI::sternheimer_delta_a_block_mode_name(Mode::FullGrid), "grid"); + EXPECT_THROW(ModuleRI::parse_sternheimer_delta_a_block_mode("mixed"), std::invalid_argument); +} + +TEST(SternheimerDelta, FullGridABlockModeMatchesStandardForGeneralProjectedSubspace) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + constexpr double volume_element = 0.6; + const std::vector potential = {0.10, -0.05, 0.20, -0.10, 0.05}; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, potential); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, volume_element); + + Vector candidate = states.wavefunctions[2]; + for (std::size_t ir = 0; ir != candidate.size(); ++ir) + { + candidate[ir] += Complex(0.25, -0.10) * states.wavefunctions[3][ir]; + } + const auto occupied_function + = ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients(states.wavefunctions[0], grid); + const auto candidate_function + = ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients(candidate, grid); + ModuleRI::SternheimerDeltaSubspaceOptions subspace_options; + subspace_options.max_virtual_states = 1; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace_by_mode( + hamiltonian, + {occupied_function}, + {candidate_function}, + volume_element, + subspace_options, + ModuleRI::SternheimerDeltaABlockMode::FullGrid); + ASSERT_EQ(subspace.virtual_states.size(), 1); + + const std::vector perturbation = {0.7, -0.3, 0.2, 0.5, -0.4}; + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(perturbation, + states.wavefunctions[0], + rhs); + const auto matrix_elements = ModuleRI::delta_sternheimer_perturbation_matrix_elements( + subspace.virtual_states, perturbation, states.wavefunctions[0], volume_element); + ModuleRI::SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = 80; + solver_options.residual_tol = 1.0e-12; + constexpr double omega = 0.45; + const auto standard = ModuleRI::solve_sternheimer_fd_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + omega, + volume_element, + solver_options); + const auto delta = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + matrix_elements, + omega, + volume_element, + solver_options); + + ASSERT_TRUE(standard.solver.converged); + ASSERT_TRUE(delta.solver.converged); + EXPECT_LT(delta.residual_norm, 1.0e-9); + expect_vector_near(delta.response.reconstructed_wavefunction, standard.delta_wavefunction, 1.0e-8); +} + +TEST(SternheimerDelta, FullGridABlockRetainsComplexHermitianCouplingsAtTwistedKPoint) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + grid.kpoint = {0.19, 0.0, 0.0}; + constexpr double volume_element = 0.6; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {0.10, -0.05, 0.20, -0.10, 0.05}); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, volume_element); + + const double inverse_sqrt_two = 1.0 / std::sqrt(2.0); + Vector first_candidate(states.wavefunctions[1].size(), Complex(0.0, 0.0)); + Vector second_candidate(states.wavefunctions[1].size(), Complex(0.0, 0.0)); + for (std::size_t ir = 0; ir != first_candidate.size(); ++ir) + { + first_candidate[ir] + = inverse_sqrt_two * (states.wavefunctions[1][ir] + Complex(0.0, 1.0) * states.wavefunctions[2][ir]); + second_candidate[ir] + = inverse_sqrt_two * (Complex(0.0, 1.0) * states.wavefunctions[1][ir] + states.wavefunctions[2][ir]); + } + const auto occupied_function + = ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients(states.wavefunctions[0], grid); + const std::vector candidate_functions{ + ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients(first_candidate, grid), + ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients(second_candidate, grid), + }; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace_by_mode( + hamiltonian, + {occupied_function}, + candidate_functions, + volume_element, + ModuleRI::SternheimerDeltaSubspaceOptions(), + ModuleRI::SternheimerDeltaABlockMode::FullGrid); + ASSERT_EQ(subspace.virtual_states.size(), 2); + EXPECT_LT(subspace.full_grid_hamiltonian_relative_difference, 1.0e-10); + + const Vector perturbation = {Complex(0.70, 0.11), Complex(-0.30, 0.05), Complex(0.21, -0.08), + Complex(0.48, 0.03), Complex(-0.37, 0.09)}; + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(perturbation, + states.wavefunctions[0], + rhs); + const auto matrix_elements = ModuleRI::delta_sternheimer_perturbation_matrix_elements( + subspace.virtual_states, perturbation, states.wavefunctions[0], volume_element); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 80; + options.residual_tol = 1.0e-12; + constexpr double omega = 0.45; + const auto standard = ModuleRI::solve_sternheimer_fd_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + omega, + volume_element, + options); + const auto delta = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + matrix_elements, + omega, + volume_element, + options); + + ASSERT_TRUE(standard.solver.converged); + ASSERT_TRUE(delta.solver.converged); + expect_vector_near(delta.response.reconstructed_wavefunction, standard.delta_wavefunction, 1.0e-9); +} + +TEST(SternheimerDelta, EmptyABlockReducesToStandardSternheimer) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + constexpr double volume_element = 0.6; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {0.10, -0.05, 0.20, -0.10, 0.05}); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, volume_element); + const std::vector perturbation = {0.7, -0.3, 0.2, 0.5, -0.4}; + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(perturbation, + states.wavefunctions[0], + rhs); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 80; + options.residual_tol = 1.0e-12; + constexpr double omega = 0.45; + const auto standard = ModuleRI::solve_sternheimer_fd_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + omega, + volume_element, + options); + const auto delta = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + {}, + {}, + omega, + volume_element, + options); + + ASSERT_TRUE(standard.solver.converged); + ASSERT_TRUE(delta.solver.converged); + EXPECT_TRUE(delta.response.coefficients.empty()); + expect_vector_near(delta.response.reconstructed_wavefunction, standard.delta_wavefunction, 1.0e-10); +} + +TEST(SternheimerDelta, CompleteFullGridABlockReducesToSOSWithZeroOutGridResponse) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + grid.kpoint = {0.19, 0.0, 0.0}; + constexpr double volume_element = 0.6; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, {0.10, -0.05, 0.20, -0.10, 0.05}); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, volume_element); + const auto occupied_function + = ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients(states.wavefunctions[0], grid); + std::vector candidate_functions; + for (int state_index = 1; state_index != 5; ++state_index) + { + candidate_functions.push_back(ModuleRI::make_delta_sternheimer_grid_function_with_fd_gradients( + states.wavefunctions[static_cast(state_index)], grid)); + } + ModuleRI::SternheimerDeltaSubspaceOptions subspace_options; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace_by_mode( + hamiltonian, + {occupied_function}, + candidate_functions, + volume_element, + subspace_options, + ModuleRI::SternheimerDeltaABlockMode::FullGrid); + ASSERT_EQ(subspace.virtual_states.size(), 4); + + const Vector perturbation = {Complex(0.70, 0.11), Complex(-0.30, 0.05), Complex(0.21, -0.08), + Complex(0.48, 0.03), Complex(-0.37, 0.09)}; + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(perturbation, + states.wavefunctions[0], + rhs); + const auto matrix_elements = ModuleRI::delta_sternheimer_perturbation_matrix_elements( + subspace.virtual_states, perturbation, states.wavefunctions[0], volume_element); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 80; + options.residual_tol = 1.0e-12; + constexpr double omega = 0.45; + const auto standard = ModuleRI::solve_sternheimer_fd_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + omega, + volume_element, + options); + const auto delta = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + matrix_elements, + omega, + volume_element, + options); + const Vector sos = ModuleRI::build_sternheimer_fd_complete_sos_response(states, + 1, + 0, + rhs, + omega, + volume_element); + + ASSERT_TRUE(standard.solver.converged); + ASSERT_TRUE(delta.solver.converged); + EXPECT_LT(delta.response.out_norm, 1.0e-10); + expect_vector_near(delta.response.reconstructed_wavefunction, standard.delta_wavefunction, 1.0e-9); + expect_vector_near(delta.response.reconstructed_wavefunction, sos, 1.0e-9); +} + +TEST(SternheimerDelta, StrictProjectedSolverMatchesStandardSternheimerResponse) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + constexpr double volume_element = 0.6; + const std::vector potential = {0.10, -0.05, 0.20, -0.10, 0.05}; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, potential); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, volume_element); + + Vector candidate = states.wavefunctions[2]; + for (std::size_t ir = 0; ir != candidate.size(); ++ir) + { + candidate[ir] += Complex(0.25, -0.10) * states.wavefunctions[3][ir]; + } + ModuleRI::SternheimerDeltaSubspaceOptions subspace_options; + subspace_options.max_virtual_states = 1; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace(hamiltonian, + {states.wavefunctions[0]}, + {candidate}, + volume_element, + subspace_options); + ASSERT_EQ(subspace.virtual_states.size(), 1); + + const std::vector perturbation = {0.7, -0.3, 0.2, 0.5, -0.4}; + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(perturbation, states.wavefunctions[0], rhs); + + ModuleRI::SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = 80; + solver_options.residual_tol = 1.0e-12; + constexpr double omega = 0.45; + + const auto standard = ModuleRI::solve_sternheimer_fd_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + omega, + volume_element, + solver_options); + ASSERT_TRUE(standard.solver.converged); + + const std::vector perturbation_matrix_elements + = ModuleRI::delta_sternheimer_perturbation_matrix_elements(subspace.virtual_states, + perturbation, + states.wavefunctions[0], + volume_element); + const auto delta = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + solver_options); + + ASSERT_TRUE(delta.solver.converged); + EXPECT_LT(delta.residual_norm, 1.0e-9); + ASSERT_EQ(delta.response.sos_coefficients.size(), subspace.virtual_states.size()); + ASSERT_EQ(delta.response.pulay_coefficients.size(), subspace.virtual_states.size()); + ASSERT_EQ(delta.response.coefficients.size(), subspace.virtual_states.size()); + for (std::size_t ia = 0; ia != subspace.virtual_states.size(); ++ia) + { + const Complex component_sum = delta.response.sos_coefficients[ia] + delta.response.pulay_coefficients[ia]; + EXPECT_NEAR(delta.response.coefficients[ia].real(), component_sum.real(), 1.0e-12); + EXPECT_NEAR(delta.response.coefficients[ia].imag(), component_sum.imag(), 1.0e-12); + } + Vector reconstructed_from_components = delta.response.out_wavefunction; + for (std::size_t ir = 0; ir != reconstructed_from_components.size(); ++ir) + { + reconstructed_from_components[ir] + += delta.response.in_sos_wavefunction[ir] + delta.response.in_pulay_wavefunction[ir]; + } + expect_vector_near(delta.response.reconstructed_wavefunction, reconstructed_from_components, 1.0e-12); + expect_vector_near(delta.response.reconstructed_wavefunction, standard.delta_wavefunction, 1.0e-8); +} + +TEST(SternheimerDelta, SharedFixedSubspaceMatchesCompatibilitySolver) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + constexpr double volume_element = 0.6; + const std::vector potential = {0.10, -0.05, 0.20, -0.10, 0.05}; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, potential); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, volume_element); + + Vector candidate = states.wavefunctions[2]; + for (std::size_t ir = 0; ir != candidate.size(); ++ir) + { + candidate[ir] += Complex(0.25, -0.10) * states.wavefunctions[3][ir]; + } + ModuleRI::SternheimerDeltaSubspaceOptions subspace_options; + subspace_options.max_virtual_states = 1; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace(hamiltonian, + {states.wavefunctions[0]}, + {candidate}, + volume_element, + subspace_options); + ASSERT_EQ(subspace.virtual_states.size(), 1); + + const std::vector perturbation = {0.7, -0.3, 0.2, 0.5, -0.4}; + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(perturbation, states.wavefunctions[0], rhs); + const std::vector perturbation_matrix_elements + = ModuleRI::delta_sternheimer_perturbation_matrix_elements(subspace.virtual_states, + perturbation, + states.wavefunctions[0], + volume_element); + ModuleRI::SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = 80; + solver_options.residual_tol = 1.0e-12; + constexpr double omega = 0.45; + + const auto compatibility = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + solver_options); + const ModuleRI::SternheimerDeltaFixedSubspace fixed_subspace + = ModuleRI::build_delta_sternheimer_fixed_subspace({states.wavefunctions[0]}, + subspace.virtual_states); + const auto shared = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + fixed_subspace, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + solver_options); + + EXPECT_EQ(fixed_subspace.functions.size(), 2U); + EXPECT_EQ(shared.solver.converged, compatibility.solver.converged); + EXPECT_EQ(shared.solver.iterations, compatibility.solver.iterations); + EXPECT_NEAR(shared.solver.relative_residual, compatibility.solver.relative_residual, 1.0e-15); + EXPECT_NEAR(shared.residual_norm, compatibility.residual_norm, 1.0e-15); + expect_vector_near(shared.response.out_wavefunction, compatibility.response.out_wavefunction, 1.0e-14); + expect_vector_near(shared.response.in_sos_wavefunction, compatibility.response.in_sos_wavefunction, 1.0e-14); + expect_vector_near(shared.response.in_pulay_wavefunction, compatibility.response.in_pulay_wavefunction, 1.0e-14); + expect_vector_near(shared.response.reconstructed_wavefunction, + compatibility.response.reconstructed_wavefunction, + 1.0e-14); +} + +TEST(SternheimerDelta, BatchLinearResponseMatchesIndependentFD8Channels) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Matrix = Hamiltonian::Matrix; + Hamiltonian::Grid grid{9, 1, 1, 0.55, 1.0, 1.0, true}; + constexpr double volume_element = 0.55; + std::vector local_potential(static_cast(grid.size())); + for (int ir = 0; ir != grid.size(); ++ir) + { + local_potential[static_cast(ir)] = 0.08 * (ir % 4) - 0.03 * (ir % 7); + } + const Hamiltonian hamiltonian(grid, local_potential, 1.0, nullptr, 8); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense( + hamiltonian, grid.size(), volume_element); + + Vector candidate = states.wavefunctions[2]; + for (std::size_t ir = 0; ir != candidate.size(); ++ir) + { + candidate[ir] += Complex(0.2, -0.15) * states.wavefunctions[3][ir]; + } + ModuleRI::SternheimerDeltaSubspaceOptions subspace_options; + subspace_options.max_virtual_states = 1; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace(hamiltonian, + {states.wavefunctions[0]}, + {candidate}, + volume_element, + subspace_options); + ASSERT_EQ(subspace.virtual_states.size(), 1U); + const auto fixed_subspace = ModuleRI::build_delta_sternheimer_fixed_subspace( + {states.wavefunctions[0]}, subspace.virtual_states); + + const std::vector> perturbations + = {{0.7, -0.3, 0.2, 0.5, -0.4, 0.1, 0.6, -0.2, 0.35}, + {-0.2, 0.4, 0.8, -0.1, 0.3, -0.6, 0.25, 0.15, -0.45}}; + Matrix rhs(perturbations.size()); + std::vector> perturbation_matrix_elements(perturbations.size()); + for (std::size_t column = 0; column != perturbations.size(); ++column) + { + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation( + perturbations[column], states.wavefunctions[0], rhs[column]); + perturbation_matrix_elements[column] + = ModuleRI::delta_sternheimer_perturbation_matrix_elements(subspace.virtual_states, + perturbations[column], + states.wavefunctions[0], + volume_element); + } + + ModuleRI::SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = 120; + solver_options.residual_tol = 1.0e-11; + constexpr double omega = 0.45; + std::vector scalar; + for (std::size_t column = 0; column != rhs.size(); ++column) + { + scalar.push_back(ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + fixed_subspace, + states.eigenvalues[0], + rhs[column], + subspace.virtual_states, + perturbation_matrix_elements[column], + omega, + volume_element, + solver_options)); + } + + const auto batch = ModuleRI::solve_delta_sternheimer_linear_response_batch(hamiltonian, + fixed_subspace, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + solver_options); + + ASSERT_EQ(batch.size(), scalar.size()); + for (std::size_t column = 0; column != scalar.size(); ++column) + { + EXPECT_EQ(batch[column].solver.converged, scalar[column].solver.converged); + EXPECT_EQ(batch[column].solver.iterations, scalar[column].solver.iterations); + EXPECT_NEAR(batch[column].solver.absolute_residual, + scalar[column].solver.absolute_residual, + 1.0e-12); + EXPECT_NEAR(batch[column].solver.relative_residual, + scalar[column].solver.relative_residual, + 1.0e-12); + EXPECT_NEAR(batch[column].residual_norm, scalar[column].residual_norm, 1.0e-11); + expect_vector_near(batch[column].response.out_wavefunction, + scalar[column].response.out_wavefunction, + 1.0e-11); + expect_vector_near(batch[column].response.in_sos_wavefunction, + scalar[column].response.in_sos_wavefunction, + 1.0e-11); + expect_vector_near(batch[column].response.in_pulay_wavefunction, + scalar[column].response.in_pulay_wavefunction, + 1.0e-11); + expect_vector_near(batch[column].response.reconstructed_wavefunction, + scalar[column].response.reconstructed_wavefunction, + 1.0e-11); + ASSERT_EQ(batch[column].response.coefficients.size(), scalar[column].response.coefficients.size()); + for (std::size_t ia = 0; ia != scalar[column].response.coefficients.size(); ++ia) + { + EXPECT_NEAR(batch[column].response.coefficients[ia].real(), + scalar[column].response.coefficients[ia].real(), + 1.0e-11); + EXPECT_NEAR(batch[column].response.coefficients[ia].imag(), + scalar[column].response.coefficients[ia].imag(), + 1.0e-11); + } + } +} + +TEST(SternheimerDelta, StrictIndependentDeltaHamiltonianMatchesExplicitHybridBlockEquation) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + constexpr double volume_element = 0.6; + const std::vector potential = {0.10, -0.05, 0.20, -0.10, 0.05}; + ModuleRI::SternheimerFDHamiltonian hamiltonian(grid, potential); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, volume_element); + + const Vector& occupied = states.wavefunctions[0]; + constexpr double mixing_angle = 0.41; + constexpr double mixing_phase = 0.37; + const double cosine = std::cos(mixing_angle); + const double sine = std::sin(mixing_angle); + const Complex phase = std::exp(Complex(0.0, mixing_phase)); + + Vector eta(grid.size(), Complex(0.0, 0.0)); + Vector out_direction(grid.size(), Complex(0.0, 0.0)); + for (std::size_t ir = 0; ir != eta.size(); ++ir) + { + eta[ir] = cosine * states.wavefunctions[2][ir] + sine * phase * states.wavefunctions[3][ir]; + out_direction[ir] + = -sine * std::conj(phase) * states.wavefunctions[2][ir] + cosine * states.wavefunctions[3][ir]; + } + + auto dot = [volume_element](const Vector& lhs, const Vector& rhs) { + return ModuleRI::sternheimer_fd_grid_dot(lhs, rhs, volume_element); + }; + EXPECT_NEAR(std::abs(dot(occupied, eta)), 0.0, 1.0e-12); + EXPECT_NEAR(std::abs(dot(occupied, out_direction)), 0.0, 1.0e-12); + EXPECT_NEAR(std::abs(dot(eta, out_direction)), 0.0, 1.0e-12); + + Vector h_eta; + Vector h_out; + hamiltonian.apply(eta, h_eta); + hamiltonian.apply(out_direction, h_out); + const double fd_eta_diagonal = std::real(dot(eta, h_eta)); + const double delta_eigenvalue = fd_eta_diagonal + 0.37; + EXPECT_GT(std::abs(delta_eigenvalue - fd_eta_diagonal), 0.3); + + Vector residual = h_eta; + ModuleRI::SternheimerRPA::project_out_subspace({occupied, eta}, dot, residual); + const Complex out_eta_coupling = dot(out_direction, residual); + const Complex eta_out_coupling = std::conj(out_eta_coupling); + + const Complex rhs_eta(0.27, -0.19); + const Complex rhs_out(-0.31, 0.23); + Vector rhs(grid.size(), Complex(0.0, 0.0)); + for (std::size_t ir = 0; ir != rhs.size(); ++ir) + { + rhs[ir] = rhs_eta * eta[ir] + rhs_out * out_direction[ir]; + } + + constexpr double omega = 0.42; + const double occupied_eigenvalue = states.eigenvalues[0]; + const Complex delta_block(delta_eigenvalue - occupied_eigenvalue, omega); + const Complex out_block = dot(out_direction, h_out) - occupied_eigenvalue + Complex(0.0, omega); + const Complex determinant + = delta_block * out_block - eta_out_coupling * out_eta_coupling; + const Complex expected_eta_coefficient + = (rhs_eta * out_block - eta_out_coupling * rhs_out) / determinant; + const Complex expected_out_coefficient + = (delta_block * rhs_out - out_eta_coupling * rhs_eta) / determinant; + + ModuleRI::SternheimerRPA::SolverOptions solver_options; + solver_options.max_iter = 80; + solver_options.residual_tol = 1.0e-12; + const ModuleRI::SternheimerDeltaVirtualState virtual_state{eta, residual, delta_eigenvalue}; + const Complex perturbation_matrix_element = -rhs_eta; + const auto result = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + {occupied}, + occupied_eigenvalue, + rhs, + {virtual_state}, + {perturbation_matrix_element}, + omega, + volume_element, + solver_options); + + ASSERT_TRUE(result.solver.converged); + EXPECT_LT(result.residual_norm, 1.0e-9); + Vector expected_out(grid.size(), Complex(0.0, 0.0)); + Vector expected_total(grid.size(), Complex(0.0, 0.0)); + for (std::size_t ir = 0; ir != expected_out.size(); ++ir) + { + expected_out[ir] = expected_out_coefficient * out_direction[ir]; + expected_total[ir] = expected_out[ir] + expected_eta_coefficient * eta[ir]; + } + expect_vector_near(result.response.out_wavefunction, expected_out, 1.0e-9); + expect_vector_near(result.response.reconstructed_wavefunction, expected_total, 1.0e-9); + + const Complex expected_sos = rhs_eta / delta_block; + const Complex expected_pulay = -eta_out_coupling * expected_out_coefficient / delta_block; + ASSERT_EQ(result.response.sos_coefficients.size(), 1); + ASSERT_EQ(result.response.pulay_coefficients.size(), 1); + EXPECT_NEAR(result.response.sos_coefficients[0].real(), expected_sos.real(), 1.0e-10); + EXPECT_NEAR(result.response.sos_coefficients[0].imag(), expected_sos.imag(), 1.0e-10); + EXPECT_NEAR(result.response.pulay_coefficients[0].real(), expected_pulay.real(), 1.0e-10); + EXPECT_NEAR(result.response.pulay_coefficients[0].imag(), expected_pulay.imag(), 1.0e-10); + EXPECT_NEAR(result.response.coefficients[0].real(), expected_eta_coefficient.real(), 1.0e-9); + EXPECT_NEAR(result.response.coefficients[0].imag(), expected_eta_coefficient.imag(), 1.0e-9); +} + +TEST(SternheimerDelta, IndependentAOCouplingsMatchPatchedFullGridScalarAndBatch) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + Hamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + constexpr double dv = 0.6; + const Hamiltonian hamiltonian(grid, {0.1, -0.05, 0.2, -0.1, 0.05}); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, dv); + Vector candidate = states.wavefunctions[2]; + for (std::size_t r = 0; r != candidate.size(); ++r) + { + candidate[r] += Complex(0.25, -0.1) * states.wavefunctions[3][r]; + } + ModuleRI::SternheimerDeltaSubspaceOptions subspace_options; + subspace_options.max_virtual_states = 1; + const auto subspace = ModuleRI::build_delta_sternheimer_subspace(hamiltonian, + {states.wavefunctions[0]}, + {candidate}, + dv, + subspace_options); + const auto& eta = subspace.virtual_states[0].orbital; + const auto fixed + = ModuleRI::build_delta_sternheimer_fixed_subspace({states.wavefunctions[0]}, subspace.virtual_states); + const std::vector> potentials = {{0.7, -0.3, 0.2, 0.5, -0.4}, {-0.2, 0.4, 0.8, -0.1, 0.3}}; + Hamiltonian::Matrix rhs(2), patched_rhs(2); + std::vector> couplings(2); + for (std::size_t channel = 0; channel != rhs.size(); ++channel) + { + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(potentials[channel], + states.wavefunctions[0], + rhs[channel]); + couplings[channel] = ModuleRI::delta_sternheimer_perturbation_matrix_elements(subspace.virtual_states, + potentials[channel], + states.wavefunctions[0], + dv); + const Complex change(0.12 * (channel + 1), -0.07); + couplings[channel][0] += change; + patched_rhs[channel] = rhs[channel]; + for (std::size_t r = 0; r != eta.size(); ++r) + { + patched_rhs[channel][r] -= change * eta[r]; + } + } + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 100; + options.residual_tol = 1e-12; + constexpr double omega = 0.45; + const auto batch = ModuleRI::solve_delta_sternheimer_linear_response_batch(hamiltonian, + fixed, + states.eigenvalues[0], + rhs, + subspace.virtual_states, + couplings, + omega, + dv, + options); + for (std::size_t channel = 0; channel != rhs.size(); ++channel) + { + const auto standard = ModuleRI::solve_sternheimer_fd_linear_response(hamiltonian, + {states.wavefunctions[0]}, + states.eigenvalues[0], + patched_rhs[channel], + omega, + dv, + options); + const auto scalar = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + fixed, + states.eigenvalues[0], + rhs[channel], + subspace.virtual_states, + couplings[channel], + omega, + dv, + options); + ASSERT_TRUE(standard.solver.converged); + for (const auto* result: {&scalar, &batch[channel]}) + { + ASSERT_TRUE(result->solver.converged); + EXPECT_LT(result->residual_norm, 1e-9); + expect_vector_near(result->response.reconstructed_wavefunction, standard.delta_wavefunction, 1e-9); + for (std::size_t left = 0; left != rhs.size(); ++left) + { + const Complex block_projection + = std::conj(couplings[left][0]) * result->response.coefficients[0] + - ModuleRI::sternheimer_fd_grid_dot(rhs[left], result->response.out_wavefunction, dv); + const Complex full_projection + = -ModuleRI::sternheimer_fd_grid_dot(patched_rhs[left], standard.delta_wavefunction, dv); + EXPECT_NEAR(std::abs(block_projection - full_projection), 0.0, 1e-9); + } + } + } +} + +TEST(SternheimerDelta, ComplexMultiVirtualResponseMatchesIndependentHybridBlock) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + Hamiltonian::Grid grid{5, 1, 1, 0.6, 1.0, 1.0, true}; + constexpr double dv = 0.6; + const Hamiltonian hamiltonian(grid, {0.1, -0.05, 0.2, -0.1, 0.05}); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 5, dv); + const auto dot + = [](const Vector& lhs, const Vector& rhs) { return ModuleRI::sternheimer_fd_grid_dot(lhs, rhs, dv); }; + const auto rotate = [](Vector& first, Vector& second, double angle, double phase_angle) { + const Vector original_first = first; + const Vector original_second = second; + const Complex phase = std::exp(Complex(0.0, phase_angle)); + for (std::size_t r = 0; r != first.size(); ++r) + { + first[r] = std::cos(angle) * original_first[r] + std::sin(angle) * phase * original_second[r]; + second[r] = -std::sin(angle) * std::conj(phase) * original_first[r] + std::cos(angle) * original_second[r]; + } + }; + // Two AO directions and their orthogonal grid complement span all unoccupied states. + std::vector basis + = {states.wavefunctions[1], states.wavefunctions[2], states.wavefunctions[3], states.wavefunctions[4]}; + rotate(basis[0], basis[2], 0.41, 0.37); + rotate(basis[1], basis[3], 0.33, -0.47); + rotate(basis[0], basis[1], 0.29, 0.53); + rotate(basis[2], basis[3], 0.32, -0.61); + std::vector h_basis(4); + for (int a = 0; a != 4; ++a) + { + hamiltonian.apply(basis[a], h_basis[a]); + for (int b = 0; b != 4; ++b) + { + EXPECT_NEAR(std::abs(dot(basis[b], basis[a]) - Complex(a == b ? 1.0 : 0.0)), 0.0, 1.e-12); + } + } + std::vector virtuals; + for (int a = 0; a != 2; ++a) + { + Vector residual(grid.size(), 0.0); + for (int q = 2; q != 4; ++q) + { + const Complex coupling = dot(basis[q], h_basis[a]); + for (std::size_t r = 0; r != residual.size(); ++r) + { + residual[r] += coupling * basis[q][r]; + } + } + const double ao_energy = dot(basis[a], h_basis[a]).real() + (a == 0 ? 0.37 : -0.28); + virtuals.push_back({basis[a], residual, ao_energy}); + } + const auto fixed = ModuleRI::build_delta_sternheimer_fixed_subspace({states.wavefunctions[0]}, virtuals); + const std::vector> potentials_ry = {{0.7, -0.3, 0.2, 0.5, -0.4}, {-0.2, 0.4, 0.8, -0.1, 0.3}}; + const std::vector couplings_ha + = {{Complex(0.17, -0.13), Complex(-0.09, 0.21)}, {Complex(-0.11, 0.08), Complex(0.23, 0.16)}}; + Hamiltonian::Matrix rhs(2), couplings_ry = couplings_ha; + for (int channel = 0; channel != 2; ++channel) + { + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(potentials_ry[channel], + states.wavefunctions[0], + rhs[channel]); + for (int a = 0; a != 2; ++a) + { + couplings_ry[channel][a] *= 2.0; + EXPECT_GT(std::abs(couplings_ry[channel][a] + dot(basis[a], rhs[channel])), 1.e-3); + } + } + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 100; + options.residual_tol = 1.e-12; + for (const double omega: {0.45, -0.45}) + { + const auto batch = ModuleRI::solve_delta_sternheimer_linear_response_batch(hamiltonian, + fixed, + states.eigenvalues[0], + rhs, + virtuals, + couplings_ry, + omega, + dv, + options); + ASSERT_EQ(batch.size(), rhs.size()); + for (int channel = 0; channel != 2; ++channel) + { + // Assemble and solve the full 4x4 hybrid equation independently of the Schur solver. + std::vector matrix(4, Vector(4)); + Vector expected(4); + for (int row = 0; row != 4; ++row) + { + expected[row] = row < 2 ? -couplings_ry[channel][row] : dot(basis[row], rhs[channel]); + for (int col = 0; col != 4; ++col) + { + matrix[row][col] = row < 2 && col < 2 ? Complex(row == col ? virtuals[row].eigenvalue : 0.0) + : dot(basis[row], h_basis[col]); + if (row == col) + { + matrix[row][col] += Complex(-states.eigenvalues[0], omega); + } + } + } + for (int col = 0; col != 4; ++col) + { + int pivot = col; + for (int row = col + 1; row != 4; ++row) + { + if (std::abs(matrix[row][col]) > std::abs(matrix[pivot][col])) + { + pivot = row; + } + } + ASSERT_GT(std::abs(matrix[pivot][col]), 1.e-12); + std::swap(matrix[col], matrix[pivot]); + std::swap(expected[col], expected[pivot]); + const Complex diagonal = matrix[col][col]; + for (int j = col; j != 4; ++j) + { + matrix[col][j] /= diagonal; + } + expected[col] /= diagonal; + for (int row = 0; row != 4; ++row) + { + if (row == col) + { + continue; + } + const Complex factor = matrix[row][col]; + for (int j = col; j != 4; ++j) + { + matrix[row][j] -= factor * matrix[col][j]; + } + expected[row] -= factor * expected[col]; + } + } + Vector expected_out(grid.size(), 0.0), expected_total(grid.size(), 0.0); + for (std::size_t r = 0; r != expected_total.size(); ++r) + { + expected_out[r] = expected[2] * basis[2][r] + expected[3] * basis[3][r]; + expected_total[r] = expected_out[r] + expected[0] * basis[0][r] + expected[1] * basis[1][r]; + } + const auto scalar = ModuleRI::solve_delta_sternheimer_linear_response(hamiltonian, + fixed, + states.eigenvalues[0], + rhs[channel], + virtuals, + couplings_ry[channel], + omega, + dv, + options); + for (const auto* result: {&scalar, &batch[channel]}) + { + ASSERT_TRUE(result->solver.converged); + EXPECT_LT(result->residual_norm, 1.e-9); + expect_vector_near(result->response.out_wavefunction, expected_out, 1.e-9); + expect_vector_near(result->response.reconstructed_wavefunction, expected_total, 1.e-9); + for (int a = 0; a != 2; ++a) + { + EXPECT_NEAR(std::abs(result->response.coefficients[a] - expected[a]), 0.0, 1.e-9); + } + for (int left = 0; left != 2; ++left) + { + Complex actual_left = -0.5 * dot(rhs[left], result->response.out_wavefunction); + Complex expected_left = -0.5 * dot(rhs[left], expected_out); + for (int a = 0; a != 2; ++a) + { + actual_left += std::conj(couplings_ha[left][a]) * result->response.coefficients[a]; + expected_left += std::conj(couplings_ha[left][a]) * expected[a]; + } + EXPECT_NEAR(std::abs(actual_left - expected_left), 0.0, 1.e-9); + } + } + } + } +} + +TEST(SternheimerDelta, ComplexQPerturbationMatrixElementsMatchGridIntegral) +{ + ModuleRI::SternheimerDeltaVirtualState eta0; + eta0.orbital = {Complex(1.0, 1.0), Complex(0.5, -0.25)}; + eta0.residual = {Complex(0.0, 0.0), Complex(0.0, 0.0)}; + eta0.eigenvalue = 1.0; + const std::vector virtual_states = {eta0}; + const Vector perturbation = {Complex(2.0, -0.5), Complex(-1.0, 0.75)}; + const Vector occupied = {Complex(0.25, 0.5), Complex(-0.75, 1.0)}; + constexpr double volume_element = 0.2; + + const auto elements = ModuleRI::delta_sternheimer_perturbation_matrix_elements( + virtual_states, perturbation, occupied, volume_element); + const Complex expected = volume_element + * (std::conj(eta0.orbital[0]) * perturbation[0] * occupied[0] + + std::conj(eta0.orbital[1]) * perturbation[1] * occupied[1]); + + ASSERT_EQ(elements.size(), 1); + EXPECT_NEAR(elements[0].real(), expected.real(), 1.0e-14); + EXPECT_NEAR(elements[0].imag(), expected.imag(), 1.0e-14); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_dft_zero_order_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_dft_zero_order_test.cpp new file mode 100644 index 00000000000..d5d40f4c06a --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_dft_zero_order_test.cpp @@ -0,0 +1,62 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_dft_zero_order.h" + +#include +#include +#include + +TEST(SternheimerDFTZeroOrder, ExactEigenvalueReferencePasses) +{ + ModuleRI::SternheimerFDZeroOrderStates fd_states; + fd_states.eigenvalues = {-0.5, 0.125, 0.5}; + fd_states.residual_norms = {1.0e-13, 2.0e-13, 3.0e-13}; + + const std::vector dft_eigenvalues = {-0.5, 0.125, 0.5}; + const auto comparison + = ModuleRI::compare_sternheimer_fd_zero_order_to_dft(fd_states, dft_eigenvalues, 1.0e-10); + + ASSERT_EQ(comparison.bands.size(), 3); + EXPECT_TRUE(comparison.all_eigenvalues_within_tolerance); + EXPECT_DOUBLE_EQ(comparison.max_abs_fd_minus_dft, 0.0); + EXPECT_EQ(comparison.bands[1].band_index, 1); + EXPECT_DOUBLE_EQ(comparison.bands[1].fd_eigenvalue, 0.125); + EXPECT_DOUBLE_EQ(comparison.bands[1].dft_eigenvalue, 0.125); + EXPECT_DOUBLE_EQ(comparison.bands[1].fd_minus_dft, 0.0); + EXPECT_DOUBLE_EQ(comparison.bands[1].fd_residual_norm, 2.0e-13); + EXPECT_TRUE(comparison.bands[1].eigenvalue_within_tolerance); +} + +TEST(SternheimerDFTZeroOrder, EigenvalueMismatchIsReportedPerBand) +{ + ModuleRI::SternheimerFDZeroOrderStates fd_states; + fd_states.eigenvalues = {-0.5, 0.125, 0.5}; + fd_states.residual_norms = {1.0e-13, 2.0e-13, 3.0e-13}; + + const std::vector dft_eigenvalues = {-0.5, 0.1, 0.5}; + const auto comparison + = ModuleRI::compare_sternheimer_fd_zero_order_to_dft(fd_states, dft_eigenvalues, 1.0e-3); + + ASSERT_EQ(comparison.bands.size(), 3); + EXPECT_FALSE(comparison.all_eigenvalues_within_tolerance); + EXPECT_NEAR(comparison.max_abs_fd_minus_dft, 0.025, 1.0e-15); + EXPECT_EQ(comparison.bands[1].band_index, 1); + EXPECT_DOUBLE_EQ(comparison.bands[1].fd_eigenvalue, 0.125); + EXPECT_DOUBLE_EQ(comparison.bands[1].dft_eigenvalue, 0.1); + EXPECT_NEAR(comparison.bands[1].fd_minus_dft, 0.025, 1.0e-15); + EXPECT_FALSE(comparison.bands[1].eigenvalue_within_tolerance); +} + +TEST(SternheimerDFTZeroOrder, RejectsInconsistentInputs) +{ + ModuleRI::SternheimerFDZeroOrderStates fd_states; + fd_states.eigenvalues = {-0.5, 0.125}; + fd_states.residual_norms = {1.0e-13}; + + EXPECT_THROW(ModuleRI::compare_sternheimer_fd_zero_order_to_dft(fd_states, {-0.5, 0.125}, 1.0e-10), + std::invalid_argument); + + fd_states.residual_norms = {1.0e-13, 2.0e-13}; + EXPECT_THROW(ModuleRI::compare_sternheimer_fd_zero_order_to_dft(fd_states, {-0.5}, 1.0e-10), + std::invalid_argument); + EXPECT_THROW(ModuleRI::compare_sternheimer_fd_zero_order_to_dft(fd_states, {-0.5, 0.125}, -1.0), + std::invalid_argument); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_hamiltonian_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_hamiltonian_test.cpp new file mode 100644 index 00000000000..510e6173ea9 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_hamiltonian_test.cpp @@ -0,0 +1,654 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_hamiltonian.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace +{ + +using Complex = std::complex; +using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; +using Vector = Hamiltonian::Vector; +using Matrix = Hamiltonian::Matrix; + +void ExpectOrderEightDenseMatrixIsHermitian(const bool periodic) +{ + Hamiltonian::Grid grid{6, 5, 4, 0.4, 0.5, 0.6, periodic}; + std::vector potential(grid.size()); + for (int ir = 0; ir != grid.size(); ++ir) + { + potential[static_cast(ir)] = 0.07 * (ir % 11) - 0.13 * (ir % 5); + } + Hamiltonian hamiltonian(grid, potential, 1.0, nullptr, 8); + + const auto matrix = hamiltonian.dense_matrix(); + for (std::size_t row = 0; row != matrix.size(); ++row) + { + for (std::size_t col = 0; col != matrix.size(); ++col) + { + const Complex difference = matrix[row][col] - std::conj(matrix[col][row]); + EXPECT_NEAR(difference.real(), 0.0, 1.0e-12); + EXPECT_NEAR(difference.imag(), 0.0, 1.0e-12); + } + } +} + +double PlaneWaveKineticMaxError(const Hamiltonian& hamiltonian, const std::array& modes) +{ + const auto& grid = hamiltonian.grid(); + constexpr double two_pi = 6.283185307179586476925286766559005768; + const std::array reduced_wavevector{modes[0] + grid.kpoint[0], + modes[1] + grid.kpoint[1], + modes[2] + grid.kpoint[2]}; + const auto dual = ModuleRI::sternheimer_fd_grid_dual_vectors(grid); + std::array cartesian_wavevector{}; + for (int direction = 0; direction != 3; ++direction) + { + for (int component = 0; component != 3; ++component) + { + cartesian_wavevector[component] += two_pi * reduced_wavevector[direction] * dual[direction][component]; + } + } + double kinetic_energy = 0.0; + for (const double component: cartesian_wavevector) + { + kinetic_energy += component * component; + } + kinetic_energy *= hamiltonian.kinetic_prefactor(); + + Vector psi(static_cast(grid.size())); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const double phase = two_pi + * (reduced_wavevector[0] * ix / grid.nx + reduced_wavevector[1] * iy / grid.ny + + reduced_wavevector[2] * iz / grid.nz); + psi[static_cast(hamiltonian.index(ix, iy, iz))] + = Complex(std::cos(phase), std::sin(phase)); + } + } + } + + Vector kinetic_psi; + hamiltonian.apply_kinetic(psi, kinetic_psi); + double max_error = 0.0; + for (std::size_t ir = 0; ir != psi.size(); ++ir) + { + max_error = std::max(max_error, std::abs(kinetic_psi[ir] - kinetic_energy * psi[ir])); + } + return max_error; +} + +} // namespace + +TEST(SternheimerFDHamiltonian, ConstantFunctionHasOnlyLocalPotential) +{ + Hamiltonian::Grid grid{4, 3, 2, 0.2, 0.3, 0.4, true}; + const std::vector potential(grid.size(), 1.25); + Hamiltonian hamiltonian(grid, potential); + const Vector psi(grid.size(), Complex(2.0, -1.0)); + + Vector hpsi; + hamiltonian.apply(psi, hpsi); + + ASSERT_EQ(hpsi.size(), psi.size()); + for (std::size_t ir = 0; ir != psi.size(); ++ir) + { + EXPECT_NEAR(hpsi[ir].real(), (1.25 * psi[ir]).real(), 1.0e-12); + EXPECT_NEAR(hpsi[ir].imag(), (1.25 * psi[ir]).imag(), 1.0e-12); + } +} + +TEST(SternheimerFDHamiltonian, UsesABACUSRealSpaceIndexOrder) +{ + Hamiltonian::Grid grid{2, 3, 4, 1.0, 1.0, 1.0, true}; + std::vector potential(grid.size(), 0.0); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const int abacus_index = (ix * grid.ny + iy) * grid.nz + iz; + potential[abacus_index] = static_cast(100 * ix + 10 * iy + iz); + } + } + } + + Hamiltonian hamiltonian(grid, potential, 0.0); + const Vector psi(grid.size(), Complex(1.0, 0.0)); + Vector hpsi; + hamiltonian.apply(psi, hpsi); + + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const int abacus_index = (ix * grid.ny + iy) * grid.nz + iz; + EXPECT_EQ(hamiltonian.index(ix, iy, iz), abacus_index); + EXPECT_DOUBLE_EQ(hpsi[abacus_index].real(), potential[abacus_index]); + EXPECT_DOUBLE_EQ(hpsi[abacus_index].imag(), 0.0); + } + } + } +} + +TEST(SternheimerFDHamiltonian, ApplyUsesRequestedOpenMPThreadsAndMatchesSerial) +{ +#ifndef _OPENMP + GTEST_SKIP() << "OpenMP is not enabled in this build."; +#else + Hamiltonian::Grid grid{24, 12, 8, 0.2, 0.3, 0.4, true}; + std::vector potential(static_cast(grid.size()), 0.0); + Vector psi(static_cast(grid.size())); + for (int ir = 0; ir != grid.size(); ++ir) + { + potential[static_cast(ir)] = 0.01 * (ir % 17); + psi[static_cast(ir)] = Complex(0.03 * (ir % 11), -0.02 * (ir % 7)); + } + Hamiltonian hamiltonian(grid, potential, 1.0, nullptr, 8); + + const int previous_threads = omp_get_max_threads(); + omp_set_dynamic(0); + omp_set_num_threads(1); + int serial_threads = 0; + Vector serial; + hamiltonian.apply(psi, serial, &serial_threads); + + omp_set_num_threads(4); + int parallel_threads = 0; + Vector parallel; + hamiltonian.apply(psi, parallel, ¶llel_threads); + omp_set_num_threads(previous_threads); + + EXPECT_EQ(serial_threads, 1); + EXPECT_EQ(parallel_threads, 4); + ASSERT_EQ(parallel.size(), serial.size()); + for (std::size_t ir = 0; ir != serial.size(); ++ir) + { + EXPECT_NEAR(parallel[ir].real(), serial[ir].real(), 1.0e-13); + EXPECT_NEAR(parallel[ir].imag(), serial[ir].imag(), 1.0e-13); + } +#endif +} + +TEST(SternheimerFDHamiltonian, PlaneWaveHasSecondOrderFiniteDifferenceKineticEnergy) +{ + constexpr int nx = 12; + constexpr int mode = 2; + constexpr double length = 3.0; + const double hx = length / nx; + Hamiltonian::Grid grid{nx, 1, 1, hx, 1.0, 1.0, true}; + const std::vector potential(grid.size(), 0.0); + Hamiltonian hamiltonian(grid, potential); + + Vector psi(grid.size()); + constexpr double pi = 3.141592653589793238462643383279502884; + for (int ix = 0; ix != nx; ++ix) + { + const double phase = 2.0 * pi * mode * ix / nx; + psi[ix] = Complex(std::cos(phase), std::sin(phase)); + } + + Vector hpsi; + hamiltonian.apply(psi, hpsi); + const double expected_kinetic = (1.0 - std::cos(2.0 * pi * mode / nx)) / (hx * hx); + + for (int ix = 0; ix != nx; ++ix) + { + EXPECT_NEAR(hpsi[ix].real(), (expected_kinetic * psi[ix]).real(), 1.0e-12); + EXPECT_NEAR(hpsi[ix].imag(), (expected_kinetic * psi[ix]).imag(), 1.0e-12); + } +} + +TEST(SternheimerFDHamiltonian, TwistedBoundaryHasBlochPlaneWaveKineticEnergy) +{ + constexpr int nx = 10; + constexpr int mode = 1; + constexpr double reduced_k = 0.25; + constexpr double length = 4.0; + constexpr double pi = 3.141592653589793238462643383279502884; + const double hx = length / nx; + Hamiltonian::Grid grid{nx, 1, 1, hx, 1.0, 1.0, true}; + grid.kpoint = {reduced_k, 0.0, 0.0}; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0)); + + Vector psi(grid.size()); + const double phase_step = 2.0 * pi * (mode + reduced_k) / nx; + for (int ix = 0; ix != nx; ++ix) + { + const double phase = phase_step * ix; + psi[ix] = Complex(std::cos(phase), std::sin(phase)); + } + + Vector hpsi; + hamiltonian.apply(psi, hpsi); + const double expected_kinetic = (1.0 - std::cos(phase_step)) / (hx * hx); + for (int ix = 0; ix != nx; ++ix) + { + EXPECT_NEAR(hpsi[ix].real(), (expected_kinetic * psi[ix]).real(), 1.0e-12); + EXPECT_NEAR(hpsi[ix].imag(), (expected_kinetic * psi[ix]).imag(), 1.0e-12); + } + EXPECT_EQ(hamiltonian.kpoint(), grid.kpoint); +} + +TEST(SternheimerFDHamiltonian, TwistedBoundaryDenseMatrixIsHermitian) +{ + Hamiltonian::Grid grid{3, 2, 2, 0.4, 0.7, 0.9, true}; + grid.kpoint = {0.25, -0.125, 0.375}; + std::vector potential(grid.size(), 0.0); + for (std::size_t ir = 0; ir != potential.size(); ++ir) + { + potential[ir] = 0.1 * static_cast(ir); + } + Hamiltonian hamiltonian(grid, potential); + + const auto matrix = hamiltonian.dense_matrix(); + for (std::size_t row = 0; row != matrix.size(); ++row) + { + for (std::size_t col = 0; col != matrix.size(); ++col) + { + const Complex difference = matrix[row][col] - std::conj(matrix[col][row]); + EXPECT_NEAR(difference.real(), 0.0, 1.0e-12); + EXPECT_NEAR(difference.imag(), 0.0, 1.0e-12); + } + } +} + +TEST(SternheimerFDHamiltonian, OrderEightImprovesNonorthogonalTwistedPlaneWaveKineticEnergy) +{ + Hamiltonian::Grid grid{18, 17, 16, 1.0, 1.0, 1.0, true}; + grid.kpoint = {0.125, -0.2, 0.3}; + grid.lattice_vectors = {{{0.0, 5.1, 5.1}, {5.1, 0.0, 5.1}, {5.1, 5.1, 0.0}}}; + const std::vector potential(static_cast(grid.size()), 0.0); + const Hamiltonian order_two(grid, potential, 1.0, nullptr, 2); + const Hamiltonian order_eight(grid, potential, 1.0, nullptr, 8); + + const double order_two_error = PlaneWaveKineticMaxError(order_two, {1, -1, 2}); + const double order_eight_error = PlaneWaveKineticMaxError(order_eight, {1, -1, 2}); + + EXPECT_GT(order_two_error, 1.0e-4); + EXPECT_LT(order_eight_error, 0.02 * order_two_error); +} + +TEST(SternheimerFDHamiltonian, SkipsZeroMixedDerivativesOnOrthogonalGrid) +{ + Hamiltonian::Grid grid{8, 7, 6, 0.4, 0.5, 0.6, true}; + const Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0, nullptr, 8); + + EXPECT_EQ(hamiltonian.active_mixed_derivative_pair_count(), 0); + EXPECT_EQ(hamiltonian.cached_first_derivative_directions(), (std::array{false, false, false})); +} + +TEST(SternheimerFDHamiltonian, CachesOnlyDirectionsNeededByActiveMixedDerivatives) +{ + Hamiltonian::Grid grid{8, 7, 6, 1.0, 1.0, 1.0, true}; + grid.lattice_vectors = {{{2.0, 0.0, 0.0}, {1.0, 2.0, 0.0}, {0.0, 0.0, 2.0}}}; + const Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0, nullptr, 8); + + EXPECT_EQ(hamiltonian.active_mixed_derivative_pair_count(), 1); + EXPECT_EQ(hamiltonian.cached_first_derivative_directions(), (std::array{false, true, false})); +} + +TEST(SternheimerFDHamiltonian, OrderEightNonorthogonalTwistedDenseMatrixIsHermitian) +{ + Hamiltonian::Grid grid{5, 5, 5, 1.0, 1.0, 1.0, true}; + grid.kpoint = {0.25, -0.125, 0.375}; + grid.lattice_vectors = {{{0.0, 2.3, 2.1}, {2.2, 0.0, 2.4}, {2.0, 2.5, 0.0}}}; + std::vector potential(static_cast(grid.size()), 0.0); + for (std::size_t ir = 0; ir != potential.size(); ++ir) + { + potential[ir] = 0.03 * static_cast(ir % 13); + } + const Hamiltonian hamiltonian(grid, potential, 1.0, nullptr, 8); + + const auto matrix = hamiltonian.dense_matrix(); + for (std::size_t row = 0; row != matrix.size(); ++row) + { + for (std::size_t col = 0; col != matrix.size(); ++col) + { + const Complex difference = matrix[row][col] - std::conj(matrix[col][row]); + EXPECT_NEAR(difference.real(), 0.0, 1.0e-11); + EXPECT_NEAR(difference.imag(), 0.0, 1.0e-11); + } + } +} + +TEST(SternheimerFDHamiltonian, RejectsInvalidTwistedBoundary) +{ + Hamiltonian::Grid grid{3, 1, 1, 1.0, 1.0, 1.0, false}; + grid.kpoint = {0.25, 0.0, 0.0}; + EXPECT_THROW(Hamiltonian(grid, std::vector(grid.size(), 0.0)), std::invalid_argument); + + grid.periodic = true; + grid.kpoint = {std::numeric_limits::quiet_NaN(), 0.0, 0.0}; + EXPECT_THROW(Hamiltonian(grid, std::vector(grid.size(), 0.0)), std::invalid_argument); +} + +TEST(SternheimerFDHamiltonian, DenseMatrixMatchesApply) +{ + Hamiltonian::Grid grid{2, 2, 1, 0.5, 0.7, 1.0, true}; + const std::vector potential = {0.0, 0.1, 0.2, 0.3}; + Hamiltonian hamiltonian(grid, potential); + const Vector psi = {Complex(1.0, 0.5), Complex(-2.0, 1.0), Complex(0.0, -1.0), Complex(3.0, 2.0)}; + + Vector hpsi; + hamiltonian.apply(psi, hpsi); + const auto matrix = hamiltonian.dense_matrix(); + + Vector matrix_hpsi(psi.size(), Complex(0.0, 0.0)); + for (std::size_t row = 0; row != psi.size(); ++row) + { + for (std::size_t col = 0; col != psi.size(); ++col) + { + matrix_hpsi[row] += matrix[row][col] * psi[col]; + } + } + + for (std::size_t ir = 0; ir != psi.size(); ++ir) + { + EXPECT_NEAR(hpsi[ir].real(), matrix_hpsi[ir].real(), 1.0e-12); + EXPECT_NEAR(hpsi[ir].imag(), matrix_hpsi[ir].imag(), 1.0e-12); + } +} + +TEST(SternheimerFDHamiltonian, ApplyIncludesNonlocalProjector) +{ + Hamiltonian::Grid grid{2, 1, 1, 1.0, 1.0, 1.0, true}; + ModuleRI::SternheimerFDNonlocalProjector::ProjectorBlock block; + block.projectors = {{Complex(1.0, 0.0), Complex(0.0, 0.0)}}; + block.d_matrix = {{Complex(2.0, 0.0)}}; + auto nonlocal_projector = std::make_shared( + grid.size(), + 1.0, + std::vector{block}); + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 0.0, nonlocal_projector); + + const Vector psi = {Complex(3.0, 0.0), Complex(4.0, 0.0)}; + Vector hpsi; + hamiltonian.apply(psi, hpsi); + + ASSERT_EQ(hpsi.size(), psi.size()); + EXPECT_NEAR(hpsi[0].real(), 6.0, 1.0e-12); + EXPECT_NEAR(hpsi[0].imag(), 0.0, 1.0e-12); + EXPECT_NEAR(hpsi[1].real(), 0.0, 1.0e-12); + EXPECT_NEAR(hpsi[1].imag(), 0.0, 1.0e-12); + ASSERT_NE(hamiltonian.nonlocal_projector(), nullptr); +} + +TEST(SternheimerFDHamiltonian, OperatorComponentsSumToFullHamiltonian) +{ + Hamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + grid.kpoint = {0.25, 0.0, 0.0}; + ModuleRI::SternheimerFDNonlocalProjector::ProjectorBlock block; + block.projectors = {{Complex(1.0, 0.0), Complex(0.0, 0.0), Complex(0.5, -0.25), Complex(0.0, 0.0)}}; + block.d_matrix = {{Complex(1.5, 0.0)}}; + auto nonlocal_projector = std::make_shared( + grid.size(), + 0.5, + std::vector{block}); + Hamiltonian hamiltonian(grid, {0.1, -0.2, 0.3, -0.4}, 1.0, nonlocal_projector); + const Vector psi = {Complex(0.2, -0.1), Complex(-0.4, 0.3), Complex(0.7, 0.2), Complex(-0.1, -0.5)}; + + Vector full; + Vector kinetic; + Vector local; + Vector nonlocal; + hamiltonian.apply(psi, full); + hamiltonian.apply_kinetic(psi, kinetic); + hamiltonian.apply_local_potential(psi, local); + hamiltonian.apply_nonlocal(psi, nonlocal); + + ASSERT_EQ(full.size(), psi.size()); + for (std::size_t ir = 0; ir != full.size(); ++ir) + { + const Complex decomposed = kinetic[ir] + local[ir] + nonlocal[ir]; + EXPECT_NEAR(full[ir].real(), decomposed.real(), 1.0e-13); + EXPECT_NEAR(full[ir].imag(), decomposed.imag(), 1.0e-13); + } +} + +TEST(SternheimerFDHamiltonian, ApplyBatchMatchesScalarFD8TwistedSkewNonlocal) +{ + Hamiltonian::Grid grid{5, 4, 3, 0.4, 0.5, 0.6, true}; + grid.kpoint = {0.25, -0.125, 0.375}; + grid.lattice_vectors = {{{0.2, 2.1, 1.8}, {2.3, 0.1, 2.0}, {1.9, 2.4, -0.1}}}; + + std::vector potential(static_cast(grid.size())); + ModuleRI::SternheimerFDNonlocalProjector::ProjectorBlock block; + block.projectors.assign(2, Vector(static_cast(grid.size()))); + for (int ir = 0; ir != grid.size(); ++ir) + { + potential[static_cast(ir)] = 0.03 * (ir % 11) - 0.02 * (ir % 7); + block.projectors[0][static_cast(ir)] + = Complex(0.01 * (ir % 13), -0.02 * (ir % 5)); + block.projectors[1][static_cast(ir)] + = Complex(-0.015 * (ir % 9), 0.0125 * (ir % 17)); + } + block.d_matrix = {{Complex(0.8, 0.0), Complex(0.1, 0.2)}, + {Complex(0.1, -0.2), Complex(-0.3, 0.0)}}; + const auto nonlocal_projector = std::make_shared( + grid.size(), + 0.125, + std::vector{block}); + const Hamiltonian hamiltonian(grid, potential, 1.0, nonlocal_projector, 8); + + Matrix inputs(4, Vector(static_cast(grid.size()))); + for (std::size_t column = 0; column != inputs.size(); ++column) + { + for (int ir = 0; ir != grid.size(); ++ir) + { + inputs[column][static_cast(ir)] + = Complex(0.01 * static_cast((ir + 3 * column) % 19), + -0.0075 * static_cast((2 * ir + column) % 23)); + } + } + + Matrix expected(inputs.size()); + for (std::size_t column = 0; column != inputs.size(); ++column) + { + hamiltonian.apply(inputs[column], expected[column]); + } + Matrix actual; + hamiltonian.apply_batch(inputs, actual); + + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t column = 0; column != expected.size(); ++column) + { + ASSERT_EQ(actual[column].size(), expected[column].size()); + for (std::size_t ir = 0; ir != expected[column].size(); ++ir) + { + EXPECT_NEAR(actual[column][ir].real(), expected[column][ir].real(), 1.0e-13); + EXPECT_NEAR(actual[column][ir].imag(), expected[column][ir].imag(), 1.0e-13); + } + } + + Matrix empty_output{{Complex(1.0, 0.0)}}; + hamiltonian.apply_batch({}, empty_output); + EXPECT_TRUE(empty_output.empty()); + + Matrix one_output; + hamiltonian.apply_batch(Matrix{inputs.front()}, one_output); + ASSERT_EQ(one_output.size(), 1U); + EXPECT_EQ(one_output.front(), expected.front()); + + Matrix invalid_inputs = inputs; + invalid_inputs[2].pop_back(); + EXPECT_THROW(hamiltonian.apply_batch(invalid_inputs, actual), std::invalid_argument); +} + +TEST(SternheimerFDHamiltonian, DenseMatrixIsHermitianForLocalRealPotential) +{ + Hamiltonian::Grid grid{3, 2, 1, 0.4, 0.7, 1.0, true}; + const std::vector potential = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5}; + Hamiltonian hamiltonian(grid, potential); + + const auto matrix = hamiltonian.dense_matrix(); + for (std::size_t row = 0; row != matrix.size(); ++row) + { + for (std::size_t col = 0; col != matrix.size(); ++col) + { + const Complex difference = matrix[row][col] - std::conj(matrix[col][row]); + EXPECT_NEAR(difference.real(), 0.0, 1.0e-12); + EXPECT_NEAR(difference.imag(), 0.0, 1.0e-12); + } + } +} + +TEST(SternheimerFDHamiltonian, DenseMatrixIsHermitianForOrderEightLocalRealPotentialPeriodic) +{ + ExpectOrderEightDenseMatrixIsHermitian(true); +} + +TEST(SternheimerFDHamiltonian, DenseMatrixIsHermitianForOrderEightLocalRealPotentialNonperiodic) +{ + ExpectOrderEightDenseMatrixIsHermitian(false); +} + +TEST(SternheimerFDHamiltonian, DenseDiagonalizationReturnsFreeParticleEigenpairs) +{ + constexpr int nx = 4; + constexpr double hx = 1.0; + Hamiltonian::Grid grid{nx, 1, 1, hx, 1.0, 1.0, true}; + const std::vector potential(grid.size(), 0.25); + Hamiltonian hamiltonian(grid, potential); + + const auto eigenpairs = hamiltonian.diagonalize_dense(); + + const std::vector expected_eigenvalues = {0.25, 1.25, 1.25, 2.25}; + ASSERT_EQ(eigenpairs.eigenvalues.size(), expected_eigenvalues.size()); + ASSERT_EQ(eigenpairs.eigenvectors.size(), expected_eigenvalues.size()); + for (std::size_t ib = 0; ib != expected_eigenvalues.size(); ++ib) + { + EXPECT_NEAR(eigenpairs.eigenvalues[ib], expected_eigenvalues[ib], 1.0e-12); + + Vector hpsi; + hamiltonian.apply(eigenpairs.eigenvectors[ib], hpsi); + + double norm = 0.0; + for (std::size_t ir = 0; ir != hpsi.size(); ++ir) + { + const Complex residual = hpsi[ir] - eigenpairs.eigenvalues[ib] * eigenpairs.eigenvectors[ib][ir]; + EXPECT_NEAR(residual.real(), 0.0, 1.0e-12); + EXPECT_NEAR(residual.imag(), 0.0, 1.0e-12); + norm += std::norm(eigenpairs.eigenvectors[ib][ir]); + } + EXPECT_NEAR(norm, 1.0, 1.0e-12); + } +} + +TEST(SternheimerFDHamiltonian, KineticPrefactorScalesFiniteDifferenceLaplacian) +{ + constexpr int nx = 4; + constexpr double hx = 1.0; + Hamiltonian::Grid grid{nx, 1, 1, hx, 1.0, 1.0, true}; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0); + + const auto eigenpairs = hamiltonian.diagonalize_dense(); + + const std::vector expected_eigenvalues = {0.0, 2.0, 2.0, 4.0}; + ASSERT_EQ(eigenpairs.eigenvalues.size(), expected_eigenvalues.size()); + EXPECT_NEAR(hamiltonian.kinetic_prefactor(), 1.0, 1.0e-15); + for (std::size_t ib = 0; ib != expected_eigenvalues.size(); ++ib) + { + EXPECT_NEAR(eigenpairs.eigenvalues[ib], expected_eigenvalues[ib], 1.0e-12); + } +} + +TEST(SternheimerFDHamiltonian, FiltersSiSymmetryToTheFD8DiscreteStencilGroup) +{ + Hamiltonian::Grid grid{15, 15, 15, 0.0, 0.0, 0.0, true}; + constexpr double half_lattice = 5.102262569990759; + grid.lattice_vectors + = {{{0.0, half_lattice, half_lattice}, {half_lattice, 0.0, half_lattice}, {half_lattice, half_lattice, 0.0}}}; + using Rotation = ModuleRI::SternheimerFDReducedRotation; + const std::vector rotations = { + {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}, {{{0, -1, 0}, {1, -1, 0}, {0, -1, 1}}}, + {{{-1, 1, 0}, {-1, 0, 0}, {-1, 0, 1}}}, {{{-1, 0, 1}, {-1, 0, 0}, {-1, 1, 0}}}, + {{{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}}, {{{0, 0, -1}, {1, 0, -1}, {0, 1, -1}}}, + {{{0, 1, -1}, {1, 0, -1}, {0, 0, -1}}}, {{{-1, 0, 1}, {-1, 1, 0}, {-1, 0, 0}}}, + {{{1, -1, 0}, {0, -1, 1}, {0, -1, 0}}}, {{{0, -1, 0}, {0, -1, 1}, {1, -1, 0}}}, + {{{0, 1, -1}, {0, 0, -1}, {1, 0, -1}}}, {{{0, 0, 1}, {0, 1, 0}, {1, 0, 0}}}, + {{{1, 0, -1}, {0, 0, -1}, {0, 1, -1}}}, {{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}}}, + {{{-1, 0, 0}, {-1, 0, 1}, {-1, 1, 0}}}, {{{-1, 1, 0}, {-1, 0, 1}, {-1, 0, 0}}}, + {{{1, 0, -1}, {0, 1, -1}, {0, 0, -1}}}, {{{0, -1, 1}, {1, -1, 0}, {0, -1, 0}}}, + {{{0, 0, -1}, {0, 1, -1}, {1, 0, -1}}}, {{{0, -1, 1}, {0, -1, 0}, {1, -1, 0}}}, + {{{0, 1, 0}, {0, 0, 1}, {1, 0, 0}}}, {{{-1, 0, 0}, {-1, 1, 0}, {-1, 0, 1}}}, + {{{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}}, {{{1, -1, 0}, {0, -1, 0}, {0, -1, 1}}}, + {{{-1, 0, 0}, {0, -1, 0}, {0, 0, -1}}}, {{{0, 1, 0}, {-1, 1, 0}, {0, 1, -1}}}, + {{{1, -1, 0}, {1, 0, 0}, {1, 0, -1}}}, {{{1, 0, -1}, {1, 0, 0}, {1, -1, 0}}}, + {{{-1, 0, 0}, {0, 0, -1}, {0, -1, 0}}}, {{{0, 0, 1}, {-1, 0, 1}, {0, -1, 1}}}, + {{{0, -1, 1}, {-1, 0, 1}, {0, 0, 1}}}, {{{1, 0, -1}, {1, -1, 0}, {1, 0, 0}}}, + {{{-1, 1, 0}, {0, 1, -1}, {0, 1, 0}}}, {{{0, 1, 0}, {0, 1, -1}, {-1, 1, 0}}}, + {{{0, -1, 1}, {0, 0, 1}, {-1, 0, 1}}}, {{{0, 0, -1}, {0, -1, 0}, {-1, 0, 0}}}, + {{{-1, 0, 1}, {0, 0, 1}, {0, -1, 1}}}, {{{0, 0, -1}, {-1, 0, 0}, {0, -1, 0}}}, + {{{1, 0, 0}, {1, 0, -1}, {1, -1, 0}}}, {{{1, -1, 0}, {1, 0, -1}, {1, 0, 0}}}, + {{{-1, 0, 1}, {0, -1, 1}, {0, 0, 1}}}, {{{0, 1, -1}, {-1, 1, 0}, {0, 1, 0}}}, + {{{0, 0, 1}, {0, -1, 1}, {-1, 0, 1}}}, {{{0, 1, -1}, {0, 1, 0}, {-1, 1, 0}}}, + {{{0, -1, 0}, {0, 0, -1}, {-1, 0, 0}}}, {{{1, 0, 0}, {1, -1, 0}, {1, 0, -1}}}, + {{{0, -1, 0}, {-1, 0, 0}, {0, 0, -1}}}, {{{-1, 1, 0}, {0, 1, 0}, {0, 1, -1}}}, + }; + + EXPECT_EQ(ModuleRI::sternheimer_fd_stencil_symmetry_indices(grid, 8, rotations), + (std::vector{0, 4, 11, 13, 20, 22, 24, 28, 35, 37, 44, 46})); +} + +TEST(SternheimerFDHamiltonian, RequiresExactPeriodicGridPermutationForSpatialSymmetry) +{ + using Rotation = ModuleRI::SternheimerFDReducedRotation; + const Hamiltonian::Grid square{4, 4, 3, 1.0, 1.0, 1.0, true}; + const Rotation identity = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}; + const Rotation swap_xy = {{{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}}; + const Rotation invert_x = {{{-1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}; + + EXPECT_TRUE(ModuleRI::sternheimer_fd_grid_rotation_is_permutation(square, identity)); + EXPECT_TRUE(ModuleRI::sternheimer_fd_grid_rotation_is_permutation(square, swap_xy)); + EXPECT_TRUE(ModuleRI::sternheimer_fd_grid_rotation_is_permutation(square, invert_x)); + + const Hamiltonian::Grid rectangular{4, 5, 3, 1.0, 1.0, 1.0, true}; + EXPECT_FALSE(ModuleRI::sternheimer_fd_grid_rotation_is_permutation(rectangular, swap_xy)); +} + +TEST(SternheimerFDHamiltonian, RejectsHexagonalC3WhenTheFD8StencilIsNotCovariant) +{ + using Rotation = ModuleRI::SternheimerFDReducedRotation; + Hamiltonian::Grid grid{24, 24, 48, 0.0, 0.0, 0.0, true}; + constexpr double lattice_a = 2.5; + grid.lattice_vectors = {{{lattice_a, 0.0, 0.0}, + {-0.5 * lattice_a, std::sqrt(3.0) * 0.5 * lattice_a, 0.0}, + {0.0, 0.0, 20.0}}}; + const Rotation identity = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}; + const Rotation c3 = {{{-1, -1, 0}, {1, 0, 0}, {0, 0, 1}}}; + + // The C3 maps the periodic tensor grid exactly, but the mixed-derivative + // finite-difference stencil assigns different weights to its mapped offsets. + EXPECT_TRUE(ModuleRI::sternheimer_fd_grid_rotation_is_permutation(grid, c3)); + EXPECT_EQ(ModuleRI::sternheimer_fd_stencil_symmetry_indices(grid, 8, {identity, c3}), + (std::vector{0})); +} + +TEST(SternheimerFDHamiltonian, FiltersStencilSymmetryWhenGridPermutationFails) +{ + using Rotation = ModuleRI::SternheimerFDReducedRotation; + const Hamiltonian::Grid rectangular{4, 5, 3, 1.0, 1.0, 1.0, true}; + const Rotation identity = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}; + const Rotation swap_xy = {{{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}}; + + EXPECT_EQ(ModuleRI::sternheimer_fd_second_order_stencil_symmetry_indices( + rectangular, {identity, swap_xy}), + (std::vector{0})); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_nonlocal_projector_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_nonlocal_projector_test.cpp new file mode 100644 index 00000000000..afdaddd0624 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_nonlocal_projector_test.cpp @@ -0,0 +1,162 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_nonlocal_projector.h" + +#include +#include +#include +#include + +namespace +{ + +using Projector = ModuleRI::SternheimerFDNonlocalProjector; +using Complex = Projector::Complex; +using Vector = Projector::Vector; + +} // namespace + +TEST(SternheimerFDNonlocalProjector, SingleProjectorActsAsLowRankPotential) +{ + const double volume_element = 0.5; + const Vector beta = {Complex(1.0, 0.0), Complex(1.0, 0.0)}; + Projector::ProjectorBlock block; + block.projectors = {beta}; + block.d_matrix = {{Complex(3.0, 0.0)}}; + + Projector projector(2, volume_element, {block}); + + Vector output; + projector.apply(beta, output); + + ASSERT_EQ(output.size(), beta.size()); + EXPECT_NEAR(output[0].real(), 3.0, 1.0e-12); + EXPECT_NEAR(output[0].imag(), 0.0, 1.0e-12); + EXPECT_NEAR(output[1].real(), 3.0, 1.0e-12); + EXPECT_NEAR(output[1].imag(), 0.0, 1.0e-12); + + const Vector orthogonal = {Complex(1.0, 0.0), Complex(-1.0, 0.0)}; + projector.apply(orthogonal, output); + EXPECT_NEAR(std::abs(output[0]), 0.0, 1.0e-12); + EXPECT_NEAR(std::abs(output[1]), 0.0, 1.0e-12); +} + +TEST(SternheimerFDNonlocalProjector, CoupledProjectorsUseDMatrix) +{ + const double volume_element = 1.0; + Projector::ProjectorBlock block; + block.projectors = {{Complex(1.0, 0.0), Complex(0.0, 0.0)}, {Complex(0.0, 0.0), Complex(1.0, 0.0)}}; + block.d_matrix = {{Complex(2.0, 0.0), Complex(0.5, -0.25)}, {Complex(0.5, 0.25), Complex(4.0, 0.0)}}; + + Projector projector(2, volume_element, {block}); + const Vector psi = {Complex(1.0, 2.0), Complex(-3.0, 0.5)}; + + Vector output; + projector.apply(psi, output); + + ASSERT_EQ(output.size(), psi.size()); + EXPECT_NEAR(output[0].real(), 2.0 * 1.0 + (0.5 * -3.0 + 0.25 * 0.5), 1.0e-12); + EXPECT_NEAR(output[0].imag(), 2.0 * 2.0 + (0.5 * 0.5 - 0.25 * -3.0), 1.0e-12); + EXPECT_NEAR(output[1].real(), (0.5 * 1.0 - 0.25 * 2.0) + 4.0 * -3.0, 1.0e-12); + EXPECT_NEAR(output[1].imag(), (0.5 * 2.0 + 0.25 * 1.0) + 4.0 * 0.5, 1.0e-12); +} + +TEST(SternheimerFDNonlocalProjector, AddToAccumulatesOnExistingVector) +{ + const double volume_element = 1.0; + Projector::ProjectorBlock block; + block.projectors = {{Complex(1.0, 0.0), Complex(0.0, 0.0)}}; + block.d_matrix = {{Complex(2.0, 0.0)}}; + Projector projector(2, volume_element, {block}); + + const Vector psi = {Complex(3.0, 0.0), Complex(4.0, 0.0)}; + Vector output = {Complex(10.0, 0.0), Complex(20.0, 0.0)}; + projector.add_to(psi, output); + + EXPECT_NEAR(output[0].real(), 16.0, 1.0e-12); + EXPECT_NEAR(output[0].imag(), 0.0, 1.0e-12); + EXPECT_NEAR(output[1].real(), 20.0, 1.0e-12); + EXPECT_NEAR(output[1].imag(), 0.0, 1.0e-12); +} + +TEST(SternheimerFDNonlocalProjector, SparseBatchMatchesRepeatedScalarApplications) +{ + const double volume_element = 0.25; + Projector::ProjectorBlock block; + block.projectors = {{Complex(0.0, 0.0), + Complex(1.0, -0.5), + Complex(0.0, 0.0), + Complex(0.0, 0.0), + Complex(-0.25, 0.75), + Complex(0.0, 0.0)}, + {Complex(0.5, 0.25), + Complex(0.0, 0.0), + Complex(0.0, 0.0), + Complex(-1.0, 0.0), + Complex(0.0, 0.0), + Complex(0.0, 0.0)}}; + block.d_matrix = {{Complex(2.0, 0.0), Complex(0.25, -0.5)}, {Complex(0.25, 0.5), Complex(3.0, 0.0)}}; + Projector projector(6, volume_element, {block}); + + EXPECT_EQ(projector.projector_value_count(), 12U); + EXPECT_EQ(projector.nonzero_projector_value_count(), 4U); + + const Projector::Matrix input = {{Complex(1.0, 0.0), + Complex(2.0, -1.0), + Complex(-3.0, 0.5), + Complex(0.0, 2.0), + Complex(1.5, -0.25), + Complex(-0.5, 0.0)}, + {Complex(-1.0, 1.0), + Complex(0.0, 0.5), + Complex(2.0, 0.0), + Complex(1.0, -1.0), + Complex(-2.0, 0.75), + Complex(0.25, 0.5)}, + {Complex(0.5, -0.5), + Complex(-1.0, 0.0), + Complex(0.0, 0.0), + Complex(3.0, 0.25), + Complex(0.75, 1.0), + Complex(-2.0, -1.0)}}; + + Projector::Matrix batch_output; + projector.apply_batch(input, batch_output); + ASSERT_EQ(batch_output.size(), input.size()); + for (std::size_t column = 0; column != input.size(); ++column) + { + Vector scalar_output; + projector.apply(input[column], scalar_output); + ASSERT_EQ(batch_output[column].size(), scalar_output.size()); + for (std::size_t ir = 0; ir != scalar_output.size(); ++ir) + { + EXPECT_NEAR(std::abs(batch_output[column][ir] - scalar_output[ir]), 0.0, 1.0e-13); + } + } + + Projector::Matrix accumulated(input.size(), Vector(6, Complex(0.75, -0.25))); + projector.add_to_batch(input, accumulated); + for (std::size_t column = 0; column != input.size(); ++column) + { + Vector scalar_accumulated(6, Complex(0.75, -0.25)); + projector.add_to(input[column], scalar_accumulated); + for (std::size_t ir = 0; ir != scalar_accumulated.size(); ++ir) + { + EXPECT_NEAR(std::abs(accumulated[column][ir] - scalar_accumulated[ir]), 0.0, 1.0e-13); + } + } +} + +TEST(SternheimerFDNonlocalProjector, RejectsInvalidBlocks) +{ + Projector::ProjectorBlock block; + block.projectors = {{Complex(1.0, 0.0), Complex(0.0, 0.0)}}; + block.d_matrix = {{Complex(1.0, 0.0)}}; + + EXPECT_THROW(Projector(2, 0.0, {block}), std::invalid_argument); + + block.projectors[0].resize(1); + EXPECT_THROW(Projector(2, 1.0, {block}), std::invalid_argument); + + block.projectors = {{Complex(1.0, 0.0), Complex(0.0, 0.0)}}; + block.d_matrix = {}; + EXPECT_THROW(Projector(2, 1.0, {block}), std::invalid_argument); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_preconditioner_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_preconditioner_test.cpp new file mode 100644 index 00000000000..410ca852c39 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_preconditioner_test.cpp @@ -0,0 +1,155 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_preconditioner.h" + +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +TEST(SternheimerFDPreconditioner, InvertsPeriodicFDKineticShiftOnBlochMode) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + using Vector = Hamiltonian::Vector; + + Hamiltonian::Grid grid{8, 6, 4, 0.41, 0.53, 0.67, true}; + grid.kpoint = {0.17, -0.11, 0.08}; + Vector mode(static_cast(grid.size())); + const double pi = std::acos(-1.0); + constexpr int mx = 2; + constexpr int my = -1; + constexpr int mz = 1; + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const double phase = 2.0 * pi + * ((mx + grid.kpoint[0]) * static_cast(ix) / grid.nx + + (my + grid.kpoint[1]) * static_cast(iy) / grid.ny + + (mz + grid.kpoint[2]) * static_cast(iz) / grid.nz); + const int index = (ix * grid.ny + iy) * grid.nz + iz; + mode[static_cast(index)] + = std::exp(Complex(0.0, phase)); + } + } + } + + constexpr double reference_eigenvalue = -0.83; + constexpr double omega = 0.37; + constexpr double eta = 0.0; + for (const int order : {2, 4, 6, 8}) + { + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0, nullptr, order); + ModuleRI::SternheimerFDSpectralPreconditioner preconditioner( + hamiltonian, reference_eigenvalue, omega, eta); + Vector preconditioned; + preconditioner.apply(mode, preconditioned); + + Vector recovered; + hamiltonian.apply_kinetic(preconditioned, recovered); + const Complex shift(-reference_eigenvalue + eta, omega); + for (std::size_t ir = 0; ir != recovered.size(); ++ir) + { + recovered[ir] += shift * preconditioned[ir]; + EXPECT_NEAR(recovered[ir].real(), mode[ir].real(), 2.0e-11) << "FD order " << order; + EXPECT_NEAR(recovered[ir].imag(), mode[ir].imag(), 2.0e-11) << "FD order " << order; + } + } +} + +TEST(SternheimerFDPreconditioner, RejectsNegativeRegularization) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + Hamiltonian::Grid grid{4, 4, 4, 0.5, 0.5, 0.5, true}; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0, nullptr, 8); + + EXPECT_THROW(ModuleRI::SternheimerFDSpectralPreconditioner(hamiltonian, -0.5, 0.2, -0.1), + std::invalid_argument); +} + +TEST(SternheimerFDPreconditioner, BatchMatchesIndependentScalarApplications) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + using Vector = Hamiltonian::Vector; + using Matrix = Hamiltonian::Matrix; + Hamiltonian::Grid grid{8, 6, 4, 0.41, 0.53, 0.67, true}; + grid.kpoint = {0.17, -0.11, 0.08}; + const Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0, nullptr, 8); + const ModuleRI::SternheimerFDSpectralPreconditioner preconditioner(hamiltonian, -0.83, 0.37, 0.02); + + Matrix input(4, Vector(static_cast(grid.size()))); + for (std::size_t column = 0; column != input.size(); ++column) + { + for (int ir = 0; ir != grid.size(); ++ir) + { + input[column][static_cast(ir)] + = Complex(0.01 * static_cast((ir + 2 * column) % 17), + -0.02 * static_cast((3 * ir + column) % 13)); + } + } + Matrix expected(input.size()); + for (std::size_t column = 0; column != input.size(); ++column) + { + preconditioner.apply(input[column], expected[column]); + } + + Matrix actual; + preconditioner.apply_batch(input, actual); + + EXPECT_EQ(actual, expected); + preconditioner.apply_batch({}, actual); + EXPECT_TRUE(actual.empty()); + input[1].pop_back(); + EXPECT_THROW(preconditioner.apply_batch(input, actual), std::invalid_argument); +} + +TEST(SternheimerFDPreconditioner, ParallelizesIndependentBatchTransforms) +{ +#ifdef _OPENMP + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + using Vector = Hamiltonian::Vector; + using Matrix = Hamiltonian::Matrix; + Hamiltonian::Grid grid{8, 6, 4, 0.41, 0.53, 0.67, true}; + grid.kpoint = {0.17, -0.11, 0.08}; + const Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0, nullptr, 8); + const ModuleRI::SternheimerFDSpectralPreconditioner preconditioner(hamiltonian, -0.83, 0.37, 0.02); + Matrix input(4, Vector(static_cast(grid.size()), Complex(0.25, -0.5))); + Matrix output; + + const int previous_threads = omp_get_max_threads(); + const int previous_dynamic = omp_get_dynamic(); + omp_set_dynamic(0); + omp_set_num_threads(4); + int threads_used = 0; + preconditioner.apply_batch(input, output, &threads_used); + omp_set_num_threads(previous_threads); + omp_set_dynamic(previous_dynamic); + + EXPECT_EQ(output.size(), input.size()); + EXPECT_GT(threads_used, 1); +#endif +} + +TEST(SternheimerFDPreconditioner, ReusesCompatibleThreadLocalWorkspace) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + Hamiltonian::Grid grid{8, 6, 4, 0.5, 0.5, 0.5, true}; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0), 1.0, nullptr, 8); + + const auto first = ModuleRI::get_thread_local_sternheimer_fd_spectral_preconditioner( + hamiltonian, -0.5, 0.2, 0.1); + const auto same = ModuleRI::get_thread_local_sternheimer_fd_spectral_preconditioner( + hamiltonian, -0.5, 0.2, 0.1); + const auto shifted = ModuleRI::get_thread_local_sternheimer_fd_spectral_preconditioner( + hamiltonian, -0.5, 0.3, 0.1); + + EXPECT_EQ(first.get(), same.get()); + EXPECT_NE(same.get(), shifted.get()); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_projector_sampler_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_projector_sampler_test.cpp new file mode 100644 index 00000000000..fa78e5cecc2 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_projector_sampler_test.cpp @@ -0,0 +1,264 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_projector_sampler.h" + +#include "source_base/matrix.h" +#include "source_base/vector3.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +constexpr double y00() +{ + return 0.28209479177387814347; +} + +ModuleRI::SternheimerFDHamiltonian::Grid line_grid(const bool periodic) +{ + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 3; + grid.ny = 1; + grid.nz = 1; + grid.hx = 1.0; + grid.hy = 10.0; + grid.hz = 10.0; + grid.periodic = periodic; + return grid; +} + +} // namespace + +TEST(SternheimerFDProjectorSampler, SamplesSProjectorWithoutMinimumImage) +{ + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 1.0, 2.0}; + radial_set.beta_radials = {{2.0, 4.0, 6.0}}; + radial_set.angular_momenta = {0}; + radial_set.d_radial = {{{1.5, 0.0}}}; + + const auto block = ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, line_grid(false), ModuleBase::Vector3(0.0, 0.0, 0.0)); + + ASSERT_EQ(block.projectors.size(), 1); + ASSERT_EQ(block.projectors[0].size(), 3); + EXPECT_NEAR(block.projectors[0][0].real(), 2.0 * y00(), 1.0e-14); + EXPECT_NEAR(block.projectors[0][1].real(), 4.0 * y00(), 1.0e-14); + EXPECT_NEAR(block.projectors[0][2].real(), 6.0 * y00(), 1.0e-14); + EXPECT_NEAR(block.d_matrix[0][0].real(), 1.5, 1.0e-14); +} + +TEST(SternheimerFDProjectorSampler, WritesProjectorsInABACUSRealSpaceIndexOrder) +{ + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 1.0, 2.0}; + radial_set.beta_radials = {{0.0, 1.0, 2.0}}; + radial_set.angular_momenta = {0}; + radial_set.d_radial = {{{1.0, 0.0}}}; + + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 2; + grid.ny = 2; + grid.nz = 2; + grid.hx = 1.0; + grid.hy = 1.0; + grid.hz = 1.0; + grid.periodic = false; + + const auto block = ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, grid, ModuleBase::Vector3(0.0, 0.0, 0.0)); + + ASSERT_EQ(block.projectors.size(), 1); + ASSERT_EQ(block.projectors[0].size(), 8); + EXPECT_NEAR(block.projectors[0][4].real(), 1.0 * y00(), 1.0e-14); // (ix, iy, iz) = (1, 0, 0) + EXPECT_NEAR(block.projectors[0][1].real(), 1.0 * y00(), 1.0e-14); // (ix, iy, iz) = (0, 0, 1) + EXPECT_NEAR(block.projectors[0][6].real(), std::sqrt(2.0) * y00(), 1.0e-14); // (1, 1, 0) +} + +TEST(SternheimerFDProjectorSampler, SamplesProjectorsAtNonorthogonalCartesianGridPoints) +{ + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 1.0}; + radial_set.beta_radials = {{0.0, 1.0}}; + radial_set.angular_momenta = {0}; + radial_set.d_radial = {{{1.0, 0.0}}}; + + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 4; + grid.ny = 4; + grid.nz = 4; + grid.hx = std::sqrt(2.0) / 4.0; + grid.hy = std::sqrt(2.0) / 4.0; + grid.hz = std::sqrt(2.0) / 4.0; + grid.periodic = false; + grid.lattice_vectors = {{{0.0, 1.0, 1.0}, + {1.0, 0.0, 1.0}, + {1.0, 1.0, 0.0}}}; + + const auto block = ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, grid, ModuleBase::Vector3(0.0, 0.0, 0.0)); + + const int index = (1 * grid.ny + 1) * grid.nz; + ASSERT_EQ(block.projectors.size(), 1); + ASSERT_EQ(block.projectors[0].size(), 64); + EXPECT_NEAR(block.projectors[0][index].real(), std::sqrt(0.375) * y00(), 1.0e-14); +} + +TEST(SternheimerFDProjectorSampler, BuildsRadialSetFromABACUSMatrices) +{ + ModuleBase::matrix betar(2, 3); + betar(0, 0) = 0.0; + betar(0, 1) = 2.0; + betar(0, 2) = 6.0; + betar(1, 0) = 0.0; + betar(1, 1) = 4.0; + betar(1, 2) = 6.0; + + ModuleBase::matrix dion(2, 2); + dion(0, 0) = 7.0; + dion(0, 1) = 8.0; + dion(1, 0) = 9.0; + dion(1, 1) = 10.0; + + const auto radial_set = ModuleRI::make_sternheimer_fd_radial_projector_set_from_abacus_matrices( + {0.0, 1.0, 2.0}, betar, {0, 1}, dion); + + EXPECT_EQ(radial_set.radial_grid.size(), 3); + ASSERT_EQ(radial_set.beta_radials.size(), 2); + EXPECT_DOUBLE_EQ(radial_set.beta_radials[0][0], 2.0); + EXPECT_DOUBLE_EQ(radial_set.beta_radials[0][2], 3.0); + EXPECT_DOUBLE_EQ(radial_set.beta_radials[1][0], 0.0); + EXPECT_DOUBLE_EQ(radial_set.beta_radials[1][2], 3.0); + ASSERT_EQ(radial_set.d_radial.size(), 2); + EXPECT_DOUBLE_EQ(radial_set.d_radial[0][1].real(), 8.0); + EXPECT_EQ(radial_set.angular_momenta[1], 1); +} + +TEST(SternheimerFDProjectorSampler, PeriodizesAllProjectorImagesAtGamma) +{ + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 1.0, 2.0}; + radial_set.beta_radials = {{0.0, 1.0, 2.0}}; + radial_set.angular_momenta = {0}; + radial_set.d_radial = {{{1.0, 0.0}}}; + + const auto block = ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, line_grid(true), ModuleBase::Vector3(0.0, 0.0, 0.0)); + + ASSERT_EQ(block.projectors.size(), 1); + EXPECT_NEAR(block.projectors[0][0].real(), 0.0, 1.0e-14); + EXPECT_NEAR(block.projectors[0][1].real(), 3.0 * y00(), 1.0e-14); + EXPECT_NEAR(block.projectors[0][2].real(), 3.0 * y00(), 1.0e-14); +} + +TEST(SternheimerFDProjectorSampler, AppliesBlochPhaseToPeriodicImages) +{ + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 0.5, 1.0}; + radial_set.beta_radials = {{0.0, 1.0, 2.0}}; + radial_set.angular_momenta = {0}; + radial_set.d_radial = {{{1.0, 0.0}}}; + + auto grid = line_grid(true); + grid.kpoint = {0.25, 0.0, 0.0}; + const auto block = ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, grid, ModuleBase::Vector3(0.0, 0.0, 0.0)); + + ASSERT_EQ(block.projectors.size(), 1); + EXPECT_NEAR(block.projectors[0][2].real(), 0.0, 1.0e-14); + EXPECT_NEAR(block.projectors[0][2].imag(), 2.0 * y00(), 1.0e-14); +} + +TEST(SternheimerFDProjectorSampler, RejectsInvalidBlochKPoint) +{ + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 1.0}; + radial_set.beta_radials = {{0.0, 1.0}}; + radial_set.angular_momenta = {0}; + radial_set.d_radial = {{{1.0, 0.0}}}; + + auto grid = line_grid(false); + grid.kpoint = {0.25, 0.0, 0.0}; + EXPECT_THROW(ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, grid, ModuleBase::Vector3(0.0, 0.0, 0.0)), + std::invalid_argument); + + grid.periodic = true; + grid.kpoint = {std::numeric_limits::quiet_NaN(), 0.0, 0.0}; + EXPECT_THROW(ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, grid, ModuleBase::Vector3(0.0, 0.0, 0.0)), + std::invalid_argument); +} + +TEST(SternheimerFDProjectorSampler, ComplexBlochProjectorsGiveHermitianNonlocalOperator) +{ + using Complex = std::complex; + using Vector = ModuleRI::SternheimerFDNonlocalProjector::Vector; + + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 0.5, 1.0}; + radial_set.beta_radials = {{0.0, 1.0, 2.0}, {0.0, 2.0, 4.0}}; + radial_set.angular_momenta = {0, 0}; + radial_set.d_radial = {{Complex(2.0, 0.0), Complex(0.5, 0.25)}, + {Complex(0.5, -0.25), Complex(3.0, 0.0)}}; + + auto grid = line_grid(true); + grid.kpoint = {0.25, 0.0, 0.0}; + const auto block = ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, grid, ModuleBase::Vector3(0.0, 0.0, 0.0)); + const int size = grid.nx * grid.ny * grid.nz; + ModuleRI::SternheimerFDNonlocalProjector projector(size, 1.0, {block}); + + ASSERT_EQ(block.d_matrix.size(), 2); + EXPECT_EQ(block.d_matrix[0][1], std::conj(block.d_matrix[1][0])); + EXPECT_GT(std::abs(block.projectors[0][2].imag()), 1.0e-12); + + std::vector matrix(size, Vector(size, Complex(0.0, 0.0))); + for (int column = 0; column != size; ++column) + { + Vector basis(size, Complex(0.0, 0.0)); + basis[column] = Complex(1.0, 0.0); + Vector output; + projector.apply(basis, output); + for (int row = 0; row != size; ++row) + { + matrix[row][column] = output[row]; + } + } + for (int row = 0; row != size; ++row) + { + for (int column = 0; column != size; ++column) + { + const Complex difference = matrix[row][column] - std::conj(matrix[column][row]); + EXPECT_NEAR(difference.real(), 0.0, 1.0e-12); + EXPECT_NEAR(difference.imag(), 0.0, 1.0e-12); + } + } +} + +TEST(SternheimerFDProjectorSampler, ExpandsDMatrixByAngularChannel) +{ + ModuleRI::SternheimerFDRadialProjectorSet radial_set; + radial_set.radial_grid = {0.0, 1.0}; + radial_set.beta_radials = {{0.0, 1.0}}; + radial_set.angular_momenta = {1}; + radial_set.d_radial = {{{5.0, 0.0}}}; + + const auto block = ModuleRI::sample_sternheimer_fd_projector_block( + radial_set, line_grid(false), ModuleBase::Vector3(0.0, 0.0, 0.0)); + + ASSERT_EQ(block.projectors.size(), 3); + ASSERT_EQ(block.d_matrix.size(), 3); + for (int i = 0; i != 3; ++i) + { + ASSERT_EQ(block.d_matrix[i].size(), 3); + for (int j = 0; j != 3; ++j) + { + EXPECT_NEAR(block.d_matrix[i][j].real(), i == j ? 5.0 : 0.0, 1.0e-14); + } + } +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_solver_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_solver_test.cpp new file mode 100644 index 00000000000..32532a5abd7 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_fd_solver_test.cpp @@ -0,0 +1,160 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_fd_solver.h" + +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +namespace +{ + +using Complex = std::complex; +using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; +using Vector = Hamiltonian::Vector; + +} // namespace + +TEST(SternheimerFDSolver, DenseZeroOrderStatesUsePhysicalGridNormalization) +{ + Hamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + const double volume_element = 0.5; + const std::vector potential(grid.size(), 0.25); + Hamiltonian hamiltonian(grid, potential); + + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 3, volume_element); + + ASSERT_EQ(states.eigenvalues.size(), 3); + ASSERT_EQ(states.wavefunctions.size(), 3); + ASSERT_EQ(states.residual_norms.size(), 3); + const std::vector expected_eigenvalues = {0.25, 4.25, 4.25}; + for (std::size_t ib = 0; ib != states.eigenvalues.size(); ++ib) + { + EXPECT_NEAR(states.eigenvalues[ib], expected_eigenvalues[ib], 1.0e-12); + EXPECT_NEAR(ModuleRI::sternheimer_fd_grid_norm(states.wavefunctions[ib], volume_element), 1.0, 1.0e-12); + EXPECT_NEAR(states.residual_norms[ib], 0.0, 1.0e-12); + } + + for (std::size_t ib = 0; ib != states.wavefunctions.size(); ++ib) + { + for (std::size_t jb = 0; jb != states.wavefunctions.size(); ++jb) + { + const Complex overlap + = ModuleRI::sternheimer_fd_grid_dot(states.wavefunctions[ib], states.wavefunctions[jb], volume_element); + const double expected = (ib == jb) ? 1.0 : 0.0; + EXPECT_NEAR(overlap.real(), expected, 1.0e-12); + EXPECT_NEAR(overlap.imag(), 0.0, 1.0e-12); + } + } +} + +TEST(SternheimerFDSolver, DenseZeroOrderRejectsInvalidArguments) +{ + Hamiltonian::Grid grid{2, 1, 1, 1.0, 1.0, 1.0, true}; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.0)); + + EXPECT_THROW(ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 0, 1.0), std::invalid_argument); + EXPECT_THROW(ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 3, 1.0), std::invalid_argument); + EXPECT_THROW(ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 1, 0.0), std::invalid_argument); +} + +TEST(SternheimerFDSolver, GridDotUsesRequestedOpenMPThreadsAndMatchesSerial) +{ +#ifndef _OPENMP + GTEST_SKIP() << "OpenMP is not enabled in this build."; +#else + constexpr std::size_t size = 16384; + Vector lhs(size); + Vector rhs(size); + for (std::size_t ir = 0; ir != size; ++ir) + { + lhs[ir] = Complex(0.01 * static_cast(ir % 23), -0.02 * static_cast(ir % 19)); + rhs[ir] = Complex(-0.03 * static_cast(ir % 13), 0.04 * static_cast(ir % 17)); + } + + const int previous_threads = omp_get_max_threads(); + omp_set_dynamic(0); + omp_set_num_threads(1); + int serial_threads = 0; + const Complex serial = ModuleRI::sternheimer_fd_grid_dot(lhs, rhs, 0.125, &serial_threads); + + omp_set_num_threads(4); + int parallel_threads = 0; + const Complex parallel = ModuleRI::sternheimer_fd_grid_dot(lhs, rhs, 0.125, ¶llel_threads); + omp_set_num_threads(previous_threads); + + EXPECT_EQ(serial_threads, 1); + EXPECT_EQ(parallel_threads, 4); + EXPECT_NEAR(parallel.real(), serial.real(), 1.0e-11); + EXPECT_NEAR(parallel.imag(), serial.imag(), 1.0e-11); +#endif +} + +TEST(SternheimerFDSolver, LanczosZeroOrderMatchesDenseLowStates) +{ + Hamiltonian::Grid grid{6, 1, 1, 0.75, 1.0, 1.0, true}; + const double volume_element = 0.75; + const std::vector potential = {0.30, -0.15, 0.05, 0.20, -0.05, 0.10}; + Hamiltonian hamiltonian(grid, potential); + + const auto dense_states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 3, volume_element); + + ModuleRI::SternheimerFDLanczosOptions options; + options.max_subspace_size = 12; + options.residual_tolerance = 1.0e-11; + options.initial_seed = 17; + const auto lanczos_states + = ModuleRI::solve_sternheimer_fd_zero_order_lanczos(hamiltonian, 3, volume_element, options); + + ASSERT_EQ(lanczos_states.eigenvalues.size(), dense_states.eigenvalues.size()); + ASSERT_EQ(lanczos_states.wavefunctions.size(), dense_states.wavefunctions.size()); + for (std::size_t ib = 0; ib != dense_states.eigenvalues.size(); ++ib) + { + EXPECT_NEAR(lanczos_states.eigenvalues[ib], dense_states.eigenvalues[ib], 1.0e-9); + EXPECT_NEAR(ModuleRI::sternheimer_fd_grid_norm(lanczos_states.wavefunctions[ib], volume_element), + 1.0, + 1.0e-10); + EXPECT_LT(lanczos_states.residual_norms[ib], 1.0e-9); + } +} + +TEST(SternheimerFDSolver, LinearResponseSolvesProjectedEigenmode) +{ + Hamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + const double volume_element = 0.5; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.25)); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 4, volume_element); + + const std::vector occupied = {states.wavefunctions[0]}; + const Vector rhs = states.wavefunctions[3]; + constexpr double omega = 0.7; + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 50; + options.residual_tol = 1.0e-12; + + const auto response = ModuleRI::solve_sternheimer_fd_linear_response(hamiltonian, + occupied, + states.eigenvalues[0], + rhs, + omega, + volume_element, + options); + + EXPECT_TRUE(response.solver.converged); + EXPECT_NEAR(response.residual_norm, 0.0, 1.0e-10); + EXPECT_NEAR(std::abs(ModuleRI::sternheimer_fd_grid_dot(occupied[0], response.delta_wavefunction, volume_element)), + 0.0, + 1.0e-12); + + const Complex factor = 1.0 / Complex(states.eigenvalues[3] - states.eigenvalues[0], omega); + for (std::size_t ir = 0; ir != rhs.size(); ++ir) + { + const Complex expected = factor * rhs[ir]; + EXPECT_NEAR(response.delta_wavefunction[ir].real(), expected.real(), 1.0e-10); + EXPECT_NEAR(response.delta_wavefunction[ir].imag(), expected.imag(), 1.0e-10); + } +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_galerkin_audit_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_galerkin_audit_test.cpp new file mode 100644 index 00000000000..b63838a1fe1 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_galerkin_audit_test.cpp @@ -0,0 +1,400 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_audit.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; +using Grid = Hamiltonian::Grid; +using Complex = Hamiltonian::Complex; +using Vector = Hamiltonian::Vector; +using Matrix = Hamiltonian::Matrix; +using Projector = ModuleRI::SternheimerFDNonlocalProjector; +using Result = ModuleRI::SternheimerGalerkinAuditMatrices; +constexpr std::size_t test_budget = 64ULL * 1024 * 1024; + +std::vector views(const Matrix& states) +{ + std::vector result; + for (const auto& state : states) + { + result.push_back(&state); + } + return result; +} + +Result audit(const std::shared_ptr& fine, const Matrix& states, const Grid& coarse, + const std::size_t budget = test_budget) +{ + return ModuleRI::audit_sternheimer_galerkin_matrices(fine, views(states), coarse, budget); +} + +Grid make_grid(const int nx, const int ny, const int nz) +{ + Grid grid{nx, ny, nz, 4.0 / nx, std::sqrt(12.74) / ny, std::sqrt(9.2) / nz, true}; + grid.lattice_vectors = {{{4.0, 0.0, 0.0}, {0.7, 3.5, 0.0}, {0.2, 0.4, 3.0}}}; + grid.kpoint = {0.21, -0.17, 0.13}; + return grid; +} + +double dvolume(const Grid& grid) +{ + return 42.0 / grid.size(); +} + +std::array reduced_position(const Grid& grid, const int index) +{ + return {static_cast(index / (grid.ny * grid.nz)) / grid.nx, + static_cast((index / grid.nz) % grid.ny) / grid.ny, + static_cast(index % grid.nz) / grid.nz}; +} + +Vector mode(const Grid& grid, const std::array& frequencies) +{ + Vector values(grid.size()); + for (int i = 0; i != grid.size(); ++i) + { + const auto r = reduced_position(grid, i); + double phase = 0.0; + for (int axis = 0; axis != 3; ++axis) + { + phase += (frequencies[axis] + grid.kpoint[axis]) * r[axis]; + } + values[i] = std::polar(1.0, 2.0 * std::acos(-1.0) * phase); + } + return values; +} + +Vector probe(const Grid& grid, const double seed) +{ + Vector values(grid.size()); + for (int i = 0; i != grid.size(); ++i) + { + values[i] = Complex(std::sin(0.31 * i + seed), std::cos(0.17 * i - 2.0 * seed)); + } + return values; +} + +Complex dot(const Vector& left, const Vector& right, const Grid& grid) +{ + Complex result(0.0, 0.0); + for (std::size_t i = 0; i != left.size(); ++i) + { + result += std::conj(left[i]) * right[i]; + } + return dvolume(grid) * result; +} + +std::shared_ptr make_fine(const Grid& grid, const int order = 8) +{ + std::vector potential(grid.size()); + Projector::ProjectorBlock block; + block.projectors = {mode(grid, {0, 0, 0}), mode(grid, {1, 0, 0})}; + block.d_matrix = {{Complex(0.08, 0.0), Complex(0.015, 0.02)}, + {Complex(0.015, -0.02), Complex(-0.04, 0.0)}}; + for (int i = 0; i != grid.size(); ++i) + { + const auto r = reduced_position(grid, i); + potential[i] = -0.7 + 0.23 * std::cos(2.0 * std::acos(-1.0) * (r[0] - r[1])); + } + auto nonlocal = std::make_shared(grid.size(), dvolume(grid), + std::vector{block}); + return std::make_shared(grid, potential, 1.0, nonlocal, order); +} + +// Construct J by direct Fourier sums, independently of the FFT transfer helper. +Matrix interpolation(const Grid& coarse, const Grid& fine) +{ + Matrix j(fine.size(), Vector(coarse.size(), Complex(0.0, 0.0))); + for (int mx = -((coarse.nx - 1) / 2); mx <= coarse.nx / 2; ++mx) + { + for (int my = -((coarse.ny - 1) / 2); my <= coarse.ny / 2; ++my) + { + for (int mz = -((coarse.nz - 1) / 2); mz <= coarse.nz / 2; ++mz) + { + const Vector c = mode(coarse, {mx, my, mz}); + const Vector f = mode(fine, {mx, my, mz}); + for (int row = 0; row != fine.size(); ++row) + { + for (int col = 0; col != coarse.size(); ++col) + { + j[row][col] += f[row] * std::conj(c[col]) / static_cast(coarse.size()); + } + } + } + } + } + return j; +} + +Vector lift(const Matrix& j, const Vector& coarse) +{ + Vector fine(j.size(), Complex(0.0, 0.0)); + for (std::size_t row = 0; row != j.size(); ++row) + { + for (std::size_t col = 0; col != coarse.size(); ++col) + { + fine[row] += j[row][col] * coarse[col]; + } + } + return fine; +} + +Vector restrict_direct(const Matrix& j, const Vector& fine, const Grid& coarse_grid, const Grid& fine_grid) +{ + Vector coarse(coarse_grid.size(), Complex(0.0, 0.0)); + for (std::size_t col = 0; col != coarse.size(); ++col) + { + for (std::size_t row = 0; row != fine.size(); ++row) + { + coarse[col] += std::conj(j[row][col]) * fine[row] * (dvolume(fine_grid) / dvolume(coarse_grid)); + } + } + return coarse; +} + +void check_matrices(const Result& result, const Hamiltonian& fine, const Matrix& lifted) +{ + const std::size_t n = lifted.size(); + ASSERT_EQ(result.dimension, n); + ASSERT_EQ(result.overlap.size(), n * n); + EXPECT_NEAR(result.filtered_matrix_identity_max_abs_error, 0.0, 3.0e-9); + using Apply = void (Hamiltonian::*)(const Vector&, Vector&) const; + const std::array apply{{&Hamiltonian::apply_kinetic, &Hamiltonian::apply_local_potential, + &Hamiltonian::apply_nonlocal, &Hamiltonian::apply}}; + const std::array actual{{&result.kinetic, &result.local_potential, + &result.nonlocal, &result.hamiltonian}}; + for (std::size_t term = 0; term != apply.size(); ++term) + { + ASSERT_EQ(actual[term]->size(), n * n); + for (std::size_t col = 0; col != n; ++col) + { + Vector image; + (fine.*apply[term])(lifted[col], image); + for (std::size_t row = 0; row != n; ++row) + { + EXPECT_NEAR(std::abs((*actual[term])[row + n * col] - dot(lifted[row], image, fine.grid())), + 0.0, 3.0e-9) << "term " << term << " row " << row << " col " << col; + } + } + } + for (std::size_t col = 0; col != n; ++col) + { + for (std::size_t row = 0; row != n; ++row) + { + const std::size_t i = row + n * col; + EXPECT_NEAR(std::abs(result.overlap[i] - dot(lifted[row], lifted[col], fine.grid())), 0.0, 2.0e-10); + EXPECT_NEAR(std::abs(result.hamiltonian[i] - result.kinetic[i] + - result.local_potential[i] - result.nonlocal[i]), 0.0, 3.0e-9); + EXPECT_NEAR(std::abs(result.hamiltonian[i] - std::conj(result.hamiltonian[col + n * row])), + 0.0, 3.0e-9); + } + } +} + +} // namespace + +TEST(SternheimerGalerkinAudit, MatchesExplicitBlochTransferAndFineComponentForms) +{ + const Grid coarse = make_grid(2, 3, 2); + const auto fine = make_fine(make_grid(5, 4, 3)); + const Matrix states{probe(fine->grid(), 0.1), probe(fine->grid(), 0.8), probe(fine->grid(), -0.4)}; + const Matrix original = states; + const auto result = audit(fine, states, coarse); + const Matrix j = interpolation(coarse, fine->grid()); + Matrix lifted; + for (std::size_t i = 0; i != states.size(); ++i) + { + const Vector projected = restrict_direct(j, states[i], coarse, fine->grid()); + EXPECT_NEAR(result.fine_norm_squared[i], dot(states[i], states[i], fine->grid()).real(), 2.0e-11); + EXPECT_NEAR(result.projected_norm_squared[i], dot(projected, projected, coarse).real(), 2.0e-11); + lifted.push_back(lift(j, projected)); + Vector difference = lifted.back(); + for (std::size_t point = 0; point != difference.size(); ++point) + { + difference[point] -= states[i][point]; + } + EXPECT_NEAR(result.reconstruction_relative_error[i], + std::sqrt(dot(difference, difference, fine->grid()).real() / result.fine_norm_squared[i]), + 2.0e-11); + EXPECT_NEAR(result.fine_norm_squared[i] - result.projected_norm_squared[i], + dot(difference, difference, fine->grid()).real(), 2.0e-10); + } + check_matrices(result, *fine, lifted); + EXPECT_EQ(states, original); +} + +TEST(SternheimerGalerkinAudit, SameGridRetainsOriginalUnnormalizedStates) +{ + const auto fine = make_fine(make_grid(4, 3, 3)); + const Matrix states{probe(fine->grid(), 0.1), probe(fine->grid(), 1.1)}; + const auto result = audit(fine, states, fine->grid()); + EXPECT_EQ(result.fine_norm_squared, result.projected_norm_squared); + EXPECT_EQ(result.reconstruction_relative_error, std::vector(states.size(), 0.0)); + check_matrices(result, *fine, states); +} + +TEST(SternheimerGalerkinAudit, KeepsZeroDependentScaledStatesInOriginalOrder) +{ + const auto fine = make_fine(make_grid(5, 4, 3)); + const Grid coarse = make_grid(3, 3, 2); + const Vector state = mode(fine->grid(), {1, 0, 0}); + Vector scaled = state; + const Complex scale(2.0, -0.7); + for (auto& value : scaled) + { + value *= scale; + } + const Matrix states{Vector(state.size(), Complex(0.0, 0.0)), scaled, state, scaled}; + const auto result = audit(fine, states, coarse); + ASSERT_EQ(result.dimension, 4U); + EXPECT_DOUBLE_EQ(result.projected_norm_squared[0], 0.0); + EXPECT_DOUBLE_EQ(result.reconstruction_relative_error[0], 0.0); + EXPECT_NEAR(result.projected_norm_squared[1], std::norm(scale) * result.projected_norm_squared[2], 2.0e-10); + EXPECT_NEAR(result.projected_norm_squared[1], result.projected_norm_squared[3], 2.0e-11); + for (std::size_t i = 0; i != 4; ++i) + { + EXPECT_NEAR(std::abs(result.overlap[i * 4]), 0.0, 2.0e-11); + EXPECT_NEAR(std::abs(result.hamiltonian[i]), 0.0, 2.0e-11); + } + EXPECT_NEAR(std::abs(result.overlap[1 + 4 * 2] - std::conj(scale) * result.overlap[2 + 4 * 2]), + 0.0, 2.0e-10); +} + +TEST(SternheimerGalerkinAudit, FineOnlyModeRemainsAZeroColumnRatherThanBeingTruncated) +{ + const auto fine = make_fine(make_grid(7, 5, 5)); + const Matrix states{mode(fine->grid(), {1, 0, 0}), mode(fine->grid(), {2, 0, 0})}; + const auto result = audit(fine, states, make_grid(3, 3, 3)); + ASSERT_EQ(result.dimension, 2U); + EXPECT_GT(result.fine_norm_squared[1], 1.0); + EXPECT_NEAR(result.projected_norm_squared[1], 0.0, 2.0e-11); + EXPECT_NEAR(result.reconstruction_relative_error[1], 1.0, 2.0e-11); + EXPECT_NEAR(std::abs(result.overlap[3]), 0.0, 2.0e-11); + const auto unfiltered = audit(fine, states, fine->grid()); + EXPECT_GT(unfiltered.overlap[3].real(), 1.0); + EXPECT_GT(std::abs(unfiltered.hamiltonian[3] - result.hamiltonian[3]), 1.0); + EXPECT_NEAR(result.filtered_matrix_identity_max_abs_error, 0.0, 3.0e-9); +} + +TEST(SternheimerGalerkinAudit, MultipleStateBlocksAndTailMatchDirectForms) +{ + const auto fine = make_fine(make_grid(5, 4, 3)); + const Grid coarse = make_grid(3, 3, 2); + Matrix states; + Matrix lifted; + const Matrix j = interpolation(coarse, fine->grid()); + for (int state = 0; state != 17; ++state) + { + states.push_back(probe(fine->grid(), 0.13 * state)); + lifted.push_back(lift(j, restrict_direct(j, states.back(), coarse, fine->grid()))); + } + check_matrices(audit(fine, states, coarse), *fine, lifted); +} + +TEST(SternheimerGalerkinAudit, GridContractionHandlesPartialFinalTile) +{ + const Grid grid = make_grid(21, 21, 19); + auto fine = std::make_shared(grid, std::vector(grid.size(), -0.7), 1.0, nullptr, 8); + const Matrix states{mode(grid, {1, 0, 0}), mode(grid, {0, 1, 1})}; + const auto result = audit(fine, states, grid); + check_matrices(result, *fine, states); +} + +TEST(SternheimerGalerkinAudit, RejectsInsufficientBudgetAndAcceptsExactEstimate) +{ + const auto fine = make_fine(make_grid(5, 4, 3)); + const Grid coarse = make_grid(3, 3, 2); + const Matrix states{probe(fine->grid(), 0.1), probe(fine->grid(), 0.8)}; + const auto required = ModuleRI::sternheimer_galerkin_audit_workspace_bytes(*fine, coarse, states.size()); + ASSERT_GT(required, 0U); + EXPECT_THROW(audit(fine, states, coarse, required - 1), std::length_error); + const auto result = audit(fine, states, coarse, required); + EXPECT_EQ(result.workspace_bytes, required); +} + +TEST(SternheimerGalerkinAudit, BudgetCountsFilteredCacheButIdentityBorrowsFullFineStates) +{ + const auto fine = make_fine(make_grid(31, 31, 31)); + const Grid coarse = make_grid(3, 3, 3); + const std::size_t count = 176; + const std::size_t fine_cache_bytes = sizeof(Complex) * fine->grid().size() * count; + const auto projected = ModuleRI::sternheimer_galerkin_audit_workspace_bytes(*fine, coarse, count); + const auto identity = ModuleRI::sternheimer_galerkin_audit_workspace_bytes(*fine, fine->grid(), count); + EXPECT_GT(projected, fine_cache_bytes); + EXPECT_LT(identity, fine_cache_bytes); + // Repeated borrowed views represent a valid dependent basis, not permission + // to deduplicate columns or bypass the full-cache budget check. + const Vector state = mode(fine->grid(), {0, 0, 0}); + const std::vector borrowed(count, &state); + EXPECT_THROW(ModuleRI::audit_sternheimer_galerkin_matrices(fine, borrowed, coarse, projected - 1), + std::length_error); +} + +TEST(SternheimerGalerkinAudit, RejectsMissingOrMalformedStates) +{ + const auto fine = make_fine(make_grid(5, 4, 3)); + const Grid coarse = make_grid(3, 3, 2); + const Matrix valid{probe(fine->grid(), 0.1)}; + EXPECT_THROW(audit(nullptr, valid, coarse), std::invalid_argument); + EXPECT_THROW(audit(fine, Matrix{}, coarse), std::invalid_argument); + EXPECT_THROW(ModuleRI::audit_sternheimer_galerkin_matrices(fine, {nullptr}, coarse, test_budget), + std::invalid_argument); + Matrix malformed = valid; + malformed.push_back(Vector(fine->grid().size() - 1)); + EXPECT_THROW(audit(fine, malformed, coarse), std::invalid_argument); + malformed = valid; + malformed[0][0] = Complex(std::numeric_limits::quiet_NaN(), 0.0); + EXPECT_THROW(audit(fine, malformed, coarse), std::invalid_argument); + malformed[0][0] = Complex(0.0, std::numeric_limits::infinity()); + EXPECT_THROW(audit(fine, malformed, coarse), std::invalid_argument); +} + +TEST(SternheimerGalerkinAudit, RejectsInvalidGridAndReference) +{ + const auto fine = make_fine(make_grid(5, 4, 3)); + const Grid coarse = make_grid(3, 3, 2); + const Matrix states{probe(fine->grid(), 0.1)}; + Grid mismatched = coarse; + mismatched.kpoint[0] += 0.01; + EXPECT_THROW(audit(fine, states, mismatched), std::invalid_argument); + mismatched = coarse; + mismatched.lattice_vectors[1][0] += 0.1; + EXPECT_THROW(audit(fine, states, mismatched), std::invalid_argument); + mismatched = coarse; + mismatched.periodic = false; + EXPECT_THROW(audit(fine, states, mismatched), std::invalid_argument); + mismatched = coarse; + mismatched.nx = 0; + EXPECT_THROW(audit(fine, states, mismatched), std::invalid_argument); + EXPECT_THROW(audit(fine, states, make_grid(6, 3, 2)), std::invalid_argument); + EXPECT_THROW(audit(make_fine(fine->grid(), 2), states, coarse), std::invalid_argument); +} + +TEST(SternheimerGalerkinAudit, EstimatorRejectsInvalidCountsAndOverflowWithoutAllocation) +{ + const auto fine = make_fine(make_grid(5, 4, 3)); + const Grid coarse = make_grid(3, 3, 2); + EXPECT_THROW(ModuleRI::sternheimer_galerkin_audit_workspace_bytes(*fine, coarse, 0), std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_galerkin_audit_workspace_bytes( + *fine, coarse, static_cast(std::numeric_limits::max()) + 1), + std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_galerkin_audit_workspace_bytes( + *fine, coarse, static_cast(std::numeric_limits::max())), + std::length_error); +} + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_galerkin_operator_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_galerkin_operator_test.cpp new file mode 100644 index 00000000000..007d1a438de --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_galerkin_operator_test.cpp @@ -0,0 +1,890 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_galerkin_operator.h" + +#include "source_base/module_external/lapack_connector.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; +using Galerkin = ModuleRI::SternheimerGalerkinOperator; +using Projector = ModuleRI::SternheimerFDNonlocalProjector; +using Grid = Hamiltonian::Grid; +using Complex = Hamiltonian::Complex; +using Vector = Hamiltonian::Vector; +using Matrix = Hamiltonian::Matrix; +constexpr double two_pi = 6.283185307179586476925286766559; + +enum class Term { Total, Kinetic, Local, Nonlocal }; + +Grid MakeGrid(const int nx, const int ny, const int nz) +{ + Grid grid{nx, ny, nz, 4.0 / nx, std::sqrt(12.74) / ny, std::sqrt(9.2) / nz, true}; + grid.lattice_vectors = {{{4.0, 0.0, 0.0}, {0.7, 3.5, 0.0}, {0.2, 0.4, 3.0}}}; + grid.kpoint = {0.21, -0.17, 0.13}; + return grid; +} + +std::array Position(const Grid& grid, const int index) +{ + return {{static_cast(index / (grid.ny * grid.nz)) / grid.nx, + static_cast((index / grid.nz) % grid.ny) / grid.ny, + static_cast(index % grid.nz) / grid.nz}}; +} + +double VolumeElement(const Grid& grid) +{ + const auto lattice = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + const auto& a = lattice[0]; + const auto& b = lattice[1]; + const auto& c = lattice[2]; + return std::abs(a[0] * (b[1] * c[2] - b[2] * c[1]) + - a[1] * (b[0] * c[2] - b[2] * c[0]) + + a[2] * (b[0] * c[1] - b[1] * c[0])) / grid.size(); +} + +Complex Inner(const Vector& left, const Vector& right) +{ + Complex value{}; + for (std::size_t i = 0; i != left.size(); ++i) + { + value += std::conj(left[i]) * right[i]; + } + return value; +} + +Vector Multiply(const Matrix& matrix, const Vector& vector) +{ + Vector result(matrix.size()); + for (std::size_t row = 0; row != matrix.size(); ++row) + { + for (std::size_t col = 0; col != vector.size(); ++col) + { + result[row] += matrix[row][col] * vector[col]; + } + } + return result; +} + +void ExpectNear(const Vector& actual, const Vector& expected, const double tolerance = 5.0e-11) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t i = 0; i != actual.size(); ++i) + { + EXPECT_NEAR(std::abs(actual[i] - expected[i]), 0.0, tolerance) << "index " << i; + } +} + +Vector Probe(const Grid& grid, const double offset = 0.0) +{ + Vector vector(grid.size()); + for (int i = 0; i != grid.size(); ++i) + { + vector[i] = Complex(std::cos(0.37 * i + offset), std::sin(0.23 * i - offset)); + } + return vector; +} + +void ApplyFine(const Hamiltonian& fine, const Term term, const Vector& input, Vector& output) +{ + switch (term) + { + case Term::Total: fine.apply(input, output); break; + case Term::Kinetic: fine.apply_kinetic(input, output); break; + case Term::Local: fine.apply_local_potential(input, output); break; + case Term::Nonlocal: fine.apply_nonlocal(input, output); break; + } +} + +void ApplyCoarse(Galerkin& op, const Term term, const Vector& input, Vector& output) +{ + switch (term) + { + case Term::Total: op.apply(input, output); break; + case Term::Kinetic: op.apply_kinetic(input, output); break; + case Term::Local: op.apply_local_potential(input, output); break; + case Term::Nonlocal: op.apply_nonlocal(input, output); break; + } +} + +void ApplyBatch(Galerkin& op, const Term term, const Matrix& input, Matrix& output) +{ + switch (term) + { + case Term::Total: op.apply_batch(input, output); break; + case Term::Kinetic: op.apply_kinetic_batch(input, output); break; + case Term::Local: op.apply_local_potential_batch(input, output); break; + case Term::Nonlocal: op.apply_nonlocal_batch(input, output); break; + } +} + +// Direct Fourier sums are independent of the FFT transfer implementation. +Matrix ExplicitInterpolation(const Grid& coarse, const Grid& fine) +{ + Matrix interpolation(fine.size(), Vector(coarse.size())); + const std::array dims{{coarse.nx, coarse.ny, coarse.nz}}; + for (int row = 0; row != fine.size(); ++row) + { + const auto rf = Position(fine, row); + for (int col = 0; col != coarse.size(); ++col) + { + const auto rc = Position(coarse, col); + for (int mode = 0; mode != coarse.size(); ++mode) + { + const std::array indices{{mode / (coarse.ny * coarse.nz), + (mode / coarse.nz) % coarse.ny, mode % coarse.nz}}; + double phase = 0.0; + for (int axis = 0; axis != 3; ++axis) + { + const int signed_mode = indices[axis] <= dims[axis] / 2 + ? indices[axis] : indices[axis] - dims[axis]; + phase += (signed_mode + coarse.kpoint[axis]) * (rf[axis] - rc[axis]); + } + interpolation[row][col] += std::polar(1.0 / coarse.size(), two_pi * phase); + } + } + } + return interpolation; +} + +Vector ExplicitAdjoint(const Matrix& interpolation, const Grid& coarse, const Grid& fine, const Vector& vector) +{ + Vector result(coarse.size()); + const double weight = VolumeElement(fine) / VolumeElement(coarse); + for (int col = 0; col != coarse.size(); ++col) + { + for (int row = 0; row != fine.size(); ++row) + { + result[col] += weight * std::conj(interpolation[row][col]) * vector[row]; + } + } + return result; +} + +Matrix ExplicitGalerkin(const Hamiltonian& fine, const Grid& coarse, const Term term) +{ + const auto interpolation = ExplicitInterpolation(coarse, fine.grid()); + Matrix fine_matrix(fine.grid().size(), Vector(fine.grid().size())); + Vector basis(fine.grid().size()); + Vector image; + for (int col = 0; col != fine.grid().size(); ++col) + { + std::fill(basis.begin(), basis.end(), Complex{}); + basis[col] = 1.0; + ApplyFine(fine, term, basis, image); + for (int row = 0; row != fine.grid().size(); ++row) + { + fine_matrix[row][col] = image[row]; + } + } + Matrix result(coarse.size(), Vector(coarse.size())); + for (int col = 0; col != coarse.size(); ++col) + { + Vector lifted(fine.grid().size()); + for (int row = 0; row != fine.grid().size(); ++row) + { + lifted[row] = interpolation[row][col]; + } + const auto restricted = ExplicitAdjoint(interpolation, coarse, fine.grid(), Multiply(fine_matrix, lifted)); + for (int row = 0; row != coarse.size(); ++row) + { + result[row][col] = restricted[row]; + } + } + return result; +} + +std::shared_ptr MakeProjector(const Grid& grid) +{ + Projector::ProjectorBlock block; + block.projectors.assign(2, Vector(grid.size())); + block.d_matrix = {{Complex(0.6, 0.0), Complex(0.1, 0.05)}, + {Complex(0.1, -0.05), Complex(0.4, 0.0)}}; + for (int i = 0; i != grid.size(); ++i) + { + const auto r = Position(grid, i); + const Complex phase = std::polar(1.0, two_pi * (grid.kpoint[0] * r[0] + + grid.kpoint[1] * r[1] + grid.kpoint[2] * r[2])); + block.projectors[0][i] = phase * (0.3 + 0.2 * std::cos(two_pi * r[0])); + block.projectors[1][i] = phase * Complex(0.2 * std::sin(two_pi * r[1]), 0.15 * std::cos(two_pi * r[2])); + } + return std::make_shared(grid.size(), VolumeElement(grid), + std::vector{block}); +} + +std::shared_ptr MakeFine(const Grid& grid) +{ + std::vector potential(grid.size()); + for (int i = 0; i != grid.size(); ++i) + { + const auto r = Position(grid, i); + potential[i] = 1.2 + 0.2 * std::cos(two_pi * r[0]) + 0.1 * std::sin(two_pi * (r[1] + r[2])); + } + return std::make_shared(grid, potential, 1.0, MakeProjector(grid), 8); +} + +Matrix MatrixProduct(const Matrix& left, const Matrix& right) +{ + Matrix result(left.size(), Vector(right.front().size())); + for (std::size_t row = 0; row != left.size(); ++row) + { + for (std::size_t col = 0; col != right.front().size(); ++col) + { + for (std::size_t inner = 0; inner != right.size(); ++inner) + { + result[row][col] += left[row][inner] * right[inner][col]; + } + } + } + return result; +} + +Matrix DirectInverse(const Matrix& matrix) +{ + const int n = static_cast(matrix.size()); + Vector a(n * n); + Vector inverse(n * n); + for (int col = 0; col != n; ++col) + { + inverse[col + n * col] = 1.0; + for (int row = 0; row != n; ++row) a[row + n * col] = matrix[row][col]; + } + std::vector pivots(n); + int info = 0; + zgesv_(&n, &n, a.data(), &n, pivots.data(), inverse.data(), &n, &info); + if (info != 0) throw std::runtime_error("Test direct complex inverse failed."); + Matrix result(n, Vector(n)); + for (int row = 0; row != n; ++row) + { + for (int col = 0; col != n; ++col) result[row][col] = inverse[row + n * col]; + } + return result; +} + +std::vector HermitianEigenvalues(const Matrix& matrix) +{ + const int n = static_cast(matrix.size()); + Vector a(n * n); + for (int col = 0; col != n; ++col) + { + for (int row = 0; row != n; ++row) a[row + n * col] = matrix[row][col]; + } + const char jobz = 'N'; + const char uplo = 'U'; + const int lwork = std::max(1, 2 * n - 1); + Vector work(lwork); + std::vector rwork(std::max(1, 3 * n - 2)); + std::vector values(n); + int info = 0; + zheev_(&jobz, &uplo, &n, a.data(), &n, values.data(), work.data(), &lwork, rwork.data(), &info); + if (info != 0) throw std::runtime_error("Test Hermitian diagonalization failed."); + return values; +} + +Matrix CoarseMatrix(Galerkin& op) +{ + const int n = op.grid().size(); + Matrix matrix(n, Vector(n)); + for (int col = 0; col != n; ++col) + { + Vector basis(n); + Vector image; + basis[col] = 1.0; + op.apply(basis, image); + for (int row = 0; row != n; ++row) matrix[row][col] = image[row]; + } + return matrix; +} + +Vector BlochState(const Grid& grid, const int xmode = 0) +{ + Vector state(grid.size()); + const double amplitude = 1.0 / std::sqrt(VolumeElement(grid) * grid.size()); + for (int i = 0; i != grid.size(); ++i) + { + const auto r = Position(grid, i); + state[i] = std::polar(amplitude, two_pi * ((xmode + grid.kpoint[0]) * r[0] + + grid.kpoint[1] * r[1] + grid.kpoint[2] * r[2])); + } + return state; +} + +Matrix VirtualProjector(const Vector& occupied, const double dv) +{ + Matrix pc(occupied.size(), Vector(occupied.size())); + for (std::size_t row = 0; row != occupied.size(); ++row) + { + for (std::size_t col = 0; col != occupied.size(); ++col) + { + pc[row][col] = (row == col ? 1.0 : 0.0) - dv * occupied[row] * std::conj(occupied[col]); + } + } + return pc; +} + +Matrix ResponseOperatorWithOccupiedFill(Matrix h, const Matrix& pc, const double occupied_energy, const double occupied_fill) +{ + for (std::size_t i = 0; i != h.size(); ++i) h[i][i] -= occupied_energy; + auto a = MatrixProduct(MatrixProduct(pc, h), pc); + // Only fill the unused occupied block. The virtual reference differences + // must already be positive; no excited-state energy adjustment is allowed. + for (std::size_t row = 0; row != a.size(); ++row) + { + for (std::size_t col = 0; col != a.size(); ++col) + { + a[row][col] += occupied_fill * ((row == col ? 1.0 : 0.0) - pc[row][col]); + } + } + return a; +} + +void ExpectUnshiftedVirtualSpectrumPositive(Matrix h, + const Matrix& pc, + const Vector& occupied, + const double dv, + const double occupied_energy) +{ + EXPECT_NEAR(dv * Inner(occupied, occupied).real(), 1.0, 1.0e-12); + ExpectNear(Multiply(pc, occupied), Vector(occupied.size()), 1.0e-12); + const auto pc2 = MatrixProduct(pc, pc); + for (std::size_t row = 0; row != pc.size(); ++row) + { + ExpectNear(pc2[row], pc[row], 1.0e-12); + for (std::size_t col = 0; col != pc.size(); ++col) + { + EXPECT_NEAR(std::abs(pc[row][col] - std::conj(pc[col][row])), 0.0, 1.0e-12); + } + } + for (std::size_t i = 0; i != h.size(); ++i) h[i][i] -= occupied_energy; + const auto unshifted = MatrixProduct(MatrixProduct(pc, h), pc); + const auto eigenvalues = HermitianEigenvalues(unshifted); + // These fixtures have one occupied state: exactly one null mode is allowed, + // and all remaining (virtual) reference differences must be positive. + ASSERT_GE(eigenvalues.size(), 2U); + EXPECT_NEAR(eigenvalues.front(), 0.0, 1.0e-10); + EXPECT_GT(eigenvalues[1], 1.0e-8); +} + +Matrix FinePerturbationProducts(const Grid& fine, const Vector& source) +{ + Matrix products(3, Vector(fine.size())); + for (int i = 0; i != fine.size(); ++i) + { + const auto r = Position(fine, i); + products[0][i] = Complex(std::cos(two_pi * r[0]), 0.2 * std::sin(two_pi * r[1])) * source[i]; + products[1][i] = Complex(0.3 * std::cos(two_pi * r[0]) + std::sin(two_pi * r[1]), + 0.1 * std::cos(two_pi * r[2])) * source[i]; + products[2][i] = Complex(0.2 + 0.1 * std::cos(two_pi * (r[0] + r[1])), + 0.4 * std::sin(two_pi * r[0])) * source[i]; + } + return products; +} + +Matrix DirectResponse(const Matrix& a, const Matrix& rhs_vectors, const double dv, const double omega) +{ + Matrix plus = a; + Matrix minus = a; + for (std::size_t i = 0; i != a.size(); ++i) + { + plus[i][i] += Complex(0.0, omega); + minus[i][i] -= Complex(0.0, omega); + } + const auto plus_inverse = DirectInverse(plus); + const auto minus_inverse = DirectInverse(minus); + Matrix chi(rhs_vectors.size(), Vector(rhs_vectors.size())); + // Synthetic two-sided response: identical projected vertices on left/right, + // chi = -B^dagger [(A+i*w)^-1 + (A-i*w)^-1] B, with the grid inner product. + // This tests the FullGrid FD8 reference construction, not old weak-A or RPA acceptance. + for (std::size_t col = 0; col != rhs_vectors.size(); ++col) + { + const auto xp = Multiply(plus_inverse, rhs_vectors[col]); + const auto xm = Multiply(minus_inverse, rhs_vectors[col]); + ExpectNear(Multiply(plus, xp), rhs_vectors[col], 1.0e-11); + ExpectNear(Multiply(minus, xm), rhs_vectors[col], 1.0e-11); + for (std::size_t row = 0; row != rhs_vectors.size(); ++row) + { + chi[row][col] = -dv * (Inner(rhs_vectors[row], xp) + Inner(rhs_vectors[row], xm)); + } + } + return chi; +} + +} // namespace + +TEST(SternheimerGalerkinOperator, EveryComponentMatchesExplicitFullMatrixAndIsHermitian) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine = MakeFine(MakeGrid(6, 5, 4)); + Galerkin op(fine, coarse); + EXPECT_EQ(op.fine_hamiltonian().finite_difference_order(), 8); + EXPECT_EQ(&op.fine_hamiltonian(), fine.get()); + ASSERT_GT(fine->active_mixed_derivative_pair_count(), 0); + Matrix total(coarse.size(), Vector(coarse.size())); + Matrix sum(coarse.size(), Vector(coarse.size())); + for (const auto term: {Term::Total, Term::Kinetic, Term::Local, Term::Nonlocal}) + { + const auto expected = ExplicitGalerkin(*fine, coarse, term); + Matrix actual(coarse.size(), Vector(coarse.size())); + for (int col = 0; col != coarse.size(); ++col) + { + Vector basis(coarse.size()); + basis[col] = 1.0; + Vector image; + ApplyCoarse(op, term, basis, image); + ASSERT_EQ(image.size(), basis.size()); + for (int row = 0; row != coarse.size(); ++row) + { + actual[row][col] = image[row]; + if (term == Term::Total) total[row][col] = image[row]; + else sum[row][col] += image[row]; + } + } + for (int row = 0; row != coarse.size(); ++row) + { + ExpectNear(actual[row], expected[row]); + for (int col = 0; col != coarse.size(); ++col) + { + EXPECT_NEAR(std::abs(actual[row][col] - std::conj(actual[col][row])), 0.0, 5.0e-11); + } + } + } + double imaginary_max = 0.0; + for (int row = 0; row != coarse.size(); ++row) + { + ExpectNear(total[row], sum[row]); + for (const auto value: total[row]) imaginary_max = std::max(imaginary_max, std::abs(value.imag())); + } + EXPECT_GT(imaginary_max, 1.0e-3); +} + +TEST(SternheimerGalerkinOperator, PositiveFineOperatorBoundsCoarseRayleighQuotients) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine = MakeFine(MakeGrid(6, 5, 4)); + const auto eigenpairs = fine->diagonalize_dense(); + ASSERT_GT(eigenpairs.eigenvalues.front(), 0.0); + const auto interpolation = ExplicitInterpolation(coarse, fine->grid()); + Galerkin op(fine, coarse); + for (int trial = 0; trial != 5; ++trial) + { + const auto psi = Probe(coarse, 0.3 * trial); + const auto lifted = Multiply(interpolation, psi); + Vector coarse_image; + Vector fine_image; + op.apply(psi, coarse_image); + fine->apply(lifted, fine_image); + const double coarse_norm = Inner(psi, psi).real(); + const double fine_norm = Inner(lifted, lifted).real(); + const Complex rayleigh = Inner(psi, coarse_image) / coarse_norm; + EXPECT_NEAR(coarse_norm * VolumeElement(coarse), fine_norm * VolumeElement(fine->grid()), 1.0e-10); + EXPECT_NEAR(rayleigh.imag(), 0.0, 1.0e-11); + EXPECT_NEAR(rayleigh.real(), (Inner(lifted, fine_image) / fine_norm).real(), 1.0e-10); + EXPECT_GE(rayleigh.real(), eigenpairs.eigenvalues.front() - 1.0e-10); + EXPECT_LE(rayleigh.real(), eigenpairs.eigenvalues.back() + 1.0e-10); + op.apply_kinetic(psi, coarse_image); + EXPECT_GE(Inner(psi, coarse_image).real(), -1.0e-10); + } +} + +TEST(SternheimerGalerkinOperator, ConstantFinePotentialShiftIsExactlyCoarseIdentityShift) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine = MakeFine(MakeGrid(6, 5, 4)); + auto potential = fine->local_potential(); + constexpr double shift = 1.73; + for (auto& value: potential) value += shift; + const Hamiltonian shifted(fine->grid(), potential, 1.0, MakeProjector(fine->grid()), 8); + Galerkin first(fine, coarse); + Galerkin second(shifted, coarse); + const auto psi = Probe(coarse); + for (const auto term: {Term::Total, Term::Kinetic, Term::Local, Term::Nonlocal}) + { + Vector before; + Vector after; + ApplyCoarse(first, term, psi, before); + ApplyCoarse(second, term, psi, after); + for (std::size_t i = 0; i != psi.size(); ++i) + { + const auto expected = before[i] + ((term == Term::Total || term == Term::Local) ? shift * psi[i] : Complex{}); + EXPECT_NEAR(std::abs(after[i] - expected), 0.0, 5.0e-11); + } + } +} + +TEST(SternheimerGalerkinOperator, NonlocalRestrictionKeepsOriginalDAndDoesNotNormalizeProjectors) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine = MakeFine(MakeGrid(6, 5, 4)); + const auto original_blocks = fine->nonlocal_projector()->blocks(); + auto coarse_blocks = original_blocks; + const auto interpolation = ExplicitInterpolation(coarse, fine->grid()); + for (auto& block: coarse_blocks) + { + for (auto& beta: block.projectors) + { + beta = ExplicitAdjoint(interpolation, coarse, fine->grid(), beta); + } + } + const Projector coarse_projector(coarse.size(), VolumeElement(coarse), coarse_blocks); + Galerkin op(fine, coarse); + const auto psi = Probe(coarse); + Vector actual; + Vector expected; + op.apply_nonlocal(psi, actual); + coarse_projector.apply(psi, expected); + ExpectNear(actual, expected); + ASSERT_EQ(fine->nonlocal_projector()->blocks().size(), original_blocks.size()); + for (std::size_t i = 0; i != original_blocks.size(); ++i) + { + EXPECT_EQ(fine->nonlocal_projector()->blocks()[i].d_matrix, original_blocks[i].d_matrix); + EXPECT_EQ(fine->nonlocal_projector()->blocks()[i].projectors, original_blocks[i].projectors); + } +} + +TEST(SternheimerGalerkinOperator, PointSampledLocalPotentialAliasesButGalerkinDoesNot) +{ + const auto coarse = MakeGrid(3, 1, 1); + const auto fine_grid = MakeGrid(9, 1, 1); + std::vector potential(fine_grid.size()); + for (int i = 0; i != fine_grid.size(); ++i) + { + potential[i] = std::cos(two_pi * 3.0 * i / fine_grid.nx); + } + const Hamiltonian fine(fine_grid, potential, 1.0, nullptr, 8); + Galerkin op(fine, coarse); + const auto psi = Probe(coarse); + Vector actual; + op.apply_local_potential(psi, actual); + ExpectNear(actual, Vector(coarse.size()), 1.0e-12); + // Every coarse point samples cos(6*pi*x) as +1 although P V_f P is zero. + const Hamiltonian sampled(coarse, std::vector(coarse.size(), 1.0), 1.0, nullptr, 8); + Vector aliased; + sampled.apply_local_potential(psi, aliased); + ExpectNear(aliased, psi); + EXPECT_GT(std::abs(Inner(psi, aliased) - Inner(psi, actual)), 1.0); +} + +TEST(SternheimerGalerkinOperator, SameGridExactlyMatchesFullGridFineFD8NotHybridResponse) +{ + const auto fine = MakeFine(MakeGrid(3, 2, 2)); + Galerkin op(fine, fine->grid()); + for (const auto term: {Term::Total, Term::Kinetic, Term::Local, Term::Nonlocal}) + { + auto psi = Probe(fine->grid()); + Vector expected; + ApplyFine(*fine, term, psi, expected); + ApplyCoarse(op, term, psi, psi); + EXPECT_EQ(psi, expected); + const Matrix input{Probe(fine->grid()), Probe(fine->grid(), 0.4)}; + Matrix output; + ApplyBatch(op, term, input, output); + ASSERT_EQ(output.size(), input.size()); + for (std::size_t i = 0; i != input.size(); ++i) + { + ApplyFine(*fine, term, input[i], expected); + EXPECT_EQ(output[i], expected); + } + Matrix empty; + ApplyBatch(op, term, empty, empty); + EXPECT_TRUE(empty.empty()); + } +} + +TEST(SternheimerGalerkinOperator, SeparateWorkersApplyConcurrentlyWithSharedImmutableFineHamiltonian) +{ +#ifndef _OPENMP + GTEST_SKIP() << "OpenMP is not enabled in this build."; +#else + const auto coarse = MakeGrid(3, 2, 2); + const auto fine = MakeFine(MakeGrid(6, 5, 4)); + std::array, 2> workers; + for (auto& worker: workers) worker.reset(new Galerkin(fine, coarse)); + const Matrix inputs{Probe(coarse), Probe(coarse, 0.4)}; + for (const auto term: {Term::Total, Term::Kinetic, Term::Local, Term::Nonlocal}) + { + Matrix expected(2); + Matrix actual(2); + for (int worker = 0; worker != 2; ++worker) + { + ApplyCoarse(*workers[worker], term, inputs[worker], expected[worker]); + } +#pragma omp parallel for num_threads(2) schedule(static) + for (int worker = 0; worker < 2; ++worker) + { + for (int repeat = 0; repeat != 4; ++repeat) + { + ApplyCoarse(*workers[worker], term, inputs[worker], actual[worker]); + } + } + for (int worker = 0; worker != 2; ++worker) ExpectNear(actual[worker], expected[worker]); + } +#endif +} + +TEST(SternheimerGalerkinOperator, OwnsFineLifetimeAndPreservesZeroNonlocalComponent) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine_grid = MakeGrid(6, 5, 4); + auto fine = MakeFine(fine_grid); + const std::weak_ptr weak = fine; + Galerkin shared(fine, coarse); + fine.reset(); + EXPECT_FALSE(weak.expired()); + Vector output; + EXPECT_NO_THROW(shared.apply(Probe(coarse), output)); + Galerkin copied(Hamiltonian(fine_grid, std::vector(fine_grid.size(), 1.0), 1.0, nullptr, 8), coarse); + EXPECT_EQ(copied.fine_hamiltonian().finite_difference_order(), 8); + EXPECT_EQ(copied.fine_hamiltonian().nonlocal_projector(), nullptr); + copied.apply_nonlocal(Probe(coarse), output); + EXPECT_EQ(output, Vector(coarse.size())); +} + +TEST(SternheimerGalerkinOperator, StreamedComponentBatchesMatchScalarAndUseIndependentScratch) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine = MakeFine(MakeGrid(6, 5, 4)); + Galerkin first(fine, coarse); + Galerkin second(fine, coarse); + const auto workspace = first.workspace_bytes(); + const Matrix inputs{Probe(coarse), Probe(coarse, 0.4), Probe(coarse, 0.8)}; + for (const auto term: {Term::Total, Term::Kinetic, Term::Local, Term::Nonlocal}) + { + Matrix outputs; + ApplyBatch(first, term, inputs, outputs); + ASSERT_EQ(outputs.size(), inputs.size()); + for (std::size_t i = 0; i != inputs.size(); ++i) + { + Vector expected; + ApplyCoarse(second, term, inputs[i], expected); + ExpectNear(outputs[i], expected); + } + Matrix in_place = inputs; + ApplyBatch(first, term, in_place, in_place); + for (std::size_t i = 0; i != inputs.size(); ++i) ExpectNear(in_place[i], outputs[i]); + EXPECT_EQ(first.workspace_bytes(), workspace); + } +} + +TEST(SternheimerGalerkinOperator, RejectsMalformedInputsBeforeChangingOutputs) +{ + const auto coarse = MakeGrid(3, 2, 2); + Galerkin op(MakeFine(MakeGrid(6, 5, 4)), coarse); + const Vector sentinel{Complex(17.0, -2.0)}; + for (const auto term: {Term::Total, Term::Kinetic, Term::Local, Term::Nonlocal}) + { + Vector output = sentinel; + EXPECT_THROW(ApplyCoarse(op, term, Vector(coarse.size() - 1), output), std::invalid_argument); + EXPECT_EQ(output, sentinel); + Matrix batch_output{sentinel}; + EXPECT_THROW(ApplyBatch(op, term, Matrix{Probe(coarse), Vector(1)}, batch_output), std::invalid_argument); + EXPECT_EQ(batch_output, Matrix{sentinel}); + } +} + +TEST(SternheimerGalerkinOperator, RejectsInvalidReferenceOrGridAndPreflightsWorkspaceBudget) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine = MakeFine(MakeGrid(6, 5, 4)); + EXPECT_THROW((Galerkin(std::shared_ptr{}, coarse)), std::invalid_argument); + const Hamiltonian second_order(fine->grid(), fine->local_potential(), 1.0, nullptr, 2); + EXPECT_THROW((Galerkin(second_order, coarse)), std::invalid_argument); + auto invalid = coarse; + invalid.kpoint[0] += 0.1; + EXPECT_THROW((Galerkin(fine, invalid)), std::invalid_argument); + invalid = coarse; + invalid.lattice_vectors[0][0] += 0.1; + EXPECT_THROW((Galerkin(fine, invalid)), std::invalid_argument); + invalid = coarse; + invalid.periodic = false; + EXPECT_THROW((Galerkin(fine, invalid)), std::invalid_argument); + invalid = coarse; + invalid.nx = 7; + EXPECT_THROW((Galerkin(fine, invalid)), std::invalid_argument); + invalid = coarse; + invalid.nx = 0; + EXPECT_THROW((Galerkin(fine, invalid)), std::invalid_argument); + invalid = coarse; + invalid.nx = std::numeric_limits::max(); + EXPECT_THROW((Galerkin(fine, invalid)), std::invalid_argument); + const auto bytes = Galerkin::workspace_bytes_required(*fine, coarse); + EXPECT_GT(bytes, 2 * fine->grid().size() * sizeof(Complex)); + EXPECT_THROW((Galerkin(fine, coarse, bytes - 1)), std::length_error); + Galerkin exact_budget(fine, coarse, bytes); + EXPECT_EQ(exact_budget.workspace_bytes(), bytes); + Vector output; + EXPECT_NO_THROW(exact_budget.apply(Probe(coarse), output)); +} + +TEST(SternheimerGalerkinOperator, OccupiedFillLeavesUnshiftedVirtualBlockUnchanged) +{ + const Matrix h{{-1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 3.0}}; + const auto pc = VirtualProjector(Vector{1.0, 0.0, 0.0}, 1.0); + const auto a = ResponseOperatorWithOccupiedFill(h, pc, -1.0, 0.8); + EXPECT_EQ(a, (Matrix{{0.8, 0.0, 0.0}, {0.0, 2.0, 0.0}, {0.0, 0.0, 4.0}})); + const auto other_fill = ResponseOperatorWithOccupiedFill(h, pc, -1.0, 1.6); + EXPECT_EQ(MatrixProduct(MatrixProduct(pc, a), pc), MatrixProduct(MatrixProduct(pc, other_fill), pc)); +} + +TEST(SternheimerGalerkinOperator, OccupiedFillCannotHideNegativeVirtualGap) +{ + const Matrix h{{-1.0, 0.0, 0.0}, {0.0, -1.2, 0.0}, {0.0, 0.0, 1.0}}; + const auto pc = VirtualProjector(Vector{1.0, 0.0, 0.0}, 1.0); + const auto a = ResponseOperatorWithOccupiedFill(h, pc, -1.0, 0.8); + EXPECT_NEAR(a[1][1].real(), -0.2, 1.0e-14); + EXPECT_LT(HermitianEigenvalues(a).front(), -0.19); +} + +TEST(SternheimerGalerkinOperator, PositiveAAndResponseSignDoNotValidateAnInvalidPc) +{ + const Matrix h{{0.0, 0.0}, {0.0, 2.0}}; + const Vector occupied{1.0, 0.0}; + const auto correct_pc = VirtualProjector(occupied, 1.0); + // Using an incorrectly normalized occupied state makes Pc indefinite and + // non-idempotent, although the filled A can still be strictly positive. + const auto bad_pc = VirtualProjector(Vector{std::sqrt(1.5), 0.0}, 1.0); + const auto bad_pc2 = MatrixProduct(bad_pc, bad_pc); + EXPECT_LT(HermitianEigenvalues(bad_pc).front(), -0.49); + EXPECT_GT(std::abs(bad_pc2[0][0] - bad_pc[0][0]), 0.74); + const auto bad_a = ResponseOperatorWithOccupiedFill(h, bad_pc, 0.0, 0.8); + ASSERT_GT(HermitianEigenvalues(bad_a).front(), 0.0); + const auto bad_rhs = Multiply(bad_pc, occupied); + EXPECT_GT(std::abs(Inner(occupied, bad_rhs)), 0.49); + const auto bad_chi = DirectResponse(bad_a, Matrix{bad_rhs}, 1.0, 0.37); + EXPECT_LT(bad_chi[0][0].real(), -0.1); + EXPECT_NEAR(bad_chi[0][0].imag(), 0.0, 1.0e-14); + const auto correct_a = ResponseOperatorWithOccupiedFill(h, correct_pc, 0.0, 0.8); + const auto correct_chi = DirectResponse(correct_a, Matrix{Multiply(correct_pc, occupied)}, 1.0, 0.37); + EXPECT_EQ(correct_chi, (Matrix{{0.0}})); +} + +TEST(SternheimerGalerkinOperator, FineProductProjectedDirectResponseIsHermitianNonpositiveAndDielectricPositive) +{ + const auto coarse = MakeGrid(3, 2, 2); + const auto fine_grid = MakeGrid(6, 5, 4); + const Hamiltonian fine(fine_grid, std::vector(fine_grid.size(), 1.2), 1.0, nullptr, 8); + Galerkin op(fine, coarse); + ModuleRI::SternheimerGridTransfer transfer(coarse, fine_grid); + const auto source = BlochState(fine_grid); + Vector occupied; + Vector hsource; + transfer.restrict_adjoint(source, occupied); + fine.apply(source, hsource); + const double occupied_energy = VolumeElement(fine_grid) * Inner(source, hsource).real(); + for (std::size_t i = 0; i != source.size(); ++i) + { + EXPECT_NEAR(std::abs(hsource[i] - occupied_energy * source[i]), 0.0, 1.0e-11); + } + const double dv = VolumeElement(coarse); + EXPECT_NEAR(dv * Inner(occupied, occupied).real(), 1.0, 1.0e-12); + const auto pc = VirtualProjector(occupied, dv); + const auto coarse_matrix = CoarseMatrix(op); + ExpectUnshiftedVirtualSpectrumPositive(coarse_matrix, pc, occupied, dv, occupied_energy); + const auto a = ResponseOperatorWithOccupiedFill(coarse_matrix, pc, occupied_energy, 0.8); + ASSERT_GT(HermitianEigenvalues(a).front(), 0.0); + const auto products = FinePerturbationProducts(fine_grid, source); + const auto explicit_j = ExplicitInterpolation(coarse, fine_grid); + Matrix rhs; + for (const auto& product: products) + { + Vector restricted; + transfer.restrict_adjoint(product, restricted); + ExpectNear(restricted, ExplicitAdjoint(explicit_j, coarse, fine_grid, product), 1.0e-12); + rhs.push_back(Multiply(pc, restricted)); + EXPECT_NEAR(std::abs(dv * Inner(occupied, rhs.back())), 0.0, 1.0e-12); + } + const auto chi = DirectResponse(a, rhs, dv, 0.37); + Matrix epsilon(chi.size(), Vector(chi.size())); + const std::array coulomb{{0.4, 1.3, 2.1}}; + double off_diagonal_imaginary = 0.0; + for (std::size_t row = 0; row != chi.size(); ++row) + { + EXPECT_LT(chi[row][row].real(), -1.0e-5); + for (std::size_t col = 0; col != chi.size(); ++col) + { + EXPECT_NEAR(std::abs(chi[row][col] - std::conj(chi[col][row])), 0.0, 1.0e-12); + epsilon[row][col] = (row == col ? 1.0 : 0.0) - std::sqrt(coulomb[row] * coulomb[col]) * chi[row][col]; + off_diagonal_imaginary = std::max(off_diagonal_imaginary, std::abs(chi[row][col].imag())); + } + } + EXPECT_GT(off_diagonal_imaginary, 1.0e-4); + EXPECT_LE(HermitianEigenvalues(chi).back(), 1.0e-12); + EXPECT_GE(HermitianEigenvalues(epsilon).front(), 1.0 - 1.0e-12); +} + +TEST(SternheimerGalerkinOperator, SameGridDirectResponseEqualsFullGridFineFD8DirectSolve) +{ + const auto grid = MakeGrid(3, 2, 2); + const Hamiltonian fine(grid, std::vector(grid.size(), 1.2), 1.0, nullptr, 8); + Galerkin op(fine, grid); + ModuleRI::SternheimerGridTransfer identity(grid, grid); + const auto source = BlochState(grid); + const double dv = VolumeElement(grid); + const auto fine_matrix = fine.dense_matrix(); + const auto coarse_matrix = CoarseMatrix(op); + ASSERT_EQ(coarse_matrix, fine_matrix); + const double occupied_energy = dv * Inner(source, Multiply(fine_matrix, source)).real(); + const auto pc = VirtualProjector(source, dv); + ExpectUnshiftedVirtualSpectrumPositive(fine_matrix, pc, source, dv, occupied_energy); + ExpectUnshiftedVirtualSpectrumPositive(coarse_matrix, pc, source, dv, occupied_energy); + const auto fine_a = ResponseOperatorWithOccupiedFill(fine_matrix, pc, occupied_energy, 0.8); + const auto coarse_a = ResponseOperatorWithOccupiedFill(coarse_matrix, pc, occupied_energy, 0.8); + ASSERT_GT(HermitianEigenvalues(fine_a).front(), 0.0); + Matrix coarse_rhs; + Matrix fine_rhs; + for (const auto& product: FinePerturbationProducts(grid, source)) + { + Vector restricted; + identity.restrict_adjoint(product, restricted); + EXPECT_EQ(restricted, product); + coarse_rhs.push_back(Multiply(pc, restricted)); + fine_rhs.push_back(Multiply(pc, product)); + } + const auto fine_chi = DirectResponse(fine_a, fine_rhs, dv, 0.37); + const auto coarse_chi = DirectResponse(coarse_a, coarse_rhs, dv, 0.37); + for (std::size_t row = 0; row != fine_chi.size(); ++row) ExpectNear(coarse_chi[row], fine_chi[row], 1.0e-13); +} + +TEST(SternheimerGalerkinOperator, RestrictingPerturbationAndSourceSeparatelyLosesRetainedProduct) +{ + auto coarse = MakeGrid(3, 1, 1); + auto fine = MakeGrid(9, 1, 1); + ModuleRI::SternheimerGridTransfer fields(coarse, fine); + const auto source = BlochState(fine, 1); + Vector potential(fine.size()); + Vector product(fine.size()); + for (int i = 0; i != fine.size(); ++i) + { + potential[i] = std::polar(1.0, -2.0 * two_pi * i / fine.nx); + product[i] = potential[i] * source[i]; + } + Vector occupied; + Vector correct; + fields.restrict_adjoint(source, occupied); + fields.restrict_adjoint(product, correct); + const auto pc = VirtualProjector(occupied, VolumeElement(coarse)); + correct = Multiply(pc, correct); + // A scalar potential has k=0, while the source carries the Bloch twist. + // Source mode +1 and potential mode -2 produce retained mode -1. R(V) is + // zero, so (R V)(R source) misses a unit-norm Pc R(V source) entirely. + coarse.kpoint = {0.0, 0.0, 0.0}; + fine.kpoint = {0.0, 0.0, 0.0}; + ModuleRI::SternheimerGridTransfer scalars(coarse, fine); + Vector restricted_potential; + scalars.restrict_adjoint(potential, restricted_potential); + Vector wrong(occupied.size()); + for (std::size_t i = 0; i != wrong.size(); ++i) wrong[i] = restricted_potential[i] * occupied[i]; + wrong = Multiply(pc, wrong); + const double dv = VolumeElement(coarse); + EXPECT_NEAR(dv * Inner(correct, correct).real(), 1.0, 1.0e-12); + EXPECT_LT(dv * Inner(wrong, wrong).real(), 1.0e-24); + EXPECT_GT(dv * (Inner(correct, correct) - Inner(wrong, wrong)).real(), 0.99); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_grid_diagnostics_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_grid_diagnostics_test.cpp new file mode 100644 index 00000000000..86f74ff17b9 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_grid_diagnostics_test.cpp @@ -0,0 +1,277 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Complex = std::complex; + +std::string read_text_file(const std::string& path) +{ + std::ifstream input(path.c_str()); + std::ostringstream contents; + contents << input.rdbuf(); + return contents.str(); +} + +} // namespace + +TEST(SternheimerGridDiagnostics, AnalyticAOCouplingsUseRecoveredNonorthogonalCoordinates) +{ + ModuleRI::SternheimerDeltaGridFunction a, b; + a.values = {1., 0., 0.}; + b.values = {Complex(0.3, 0.2), 2., 0.}; + for (int direction = 0; direction < 3; ++direction) + { + a.gradients[direction].assign(3, 0.); + b.gradients[direction].assign(3, 0.); + } + const std::vector basis = {a, b}; + const std::vector ci = {Complex(0.7, 0.1), Complex(-0.2, 0.3)}; + const std::vector ca = {Complex(-0.1, 0.4), Complex(0.6, -0.2)}; + const auto psi = ModuleRI::linear_combination_delta_sternheimer_grid_functions(basis, ci); + const auto eta = ModuleRI::linear_combination_delta_sternheimer_grid_functions(basis, ca); + double error = -1.; + const auto coordinates + = ModuleRI::recover_delta_grid_ao_coefficients(basis, {&psi.values, &eta.values}, 0.4, 1.e-10, error); + EXPECT_LT(error, 1.e-12); + for (int p = 0; p < 2; ++p) + { + EXPECT_NEAR(std::abs(coordinates[0][p] - ci[p]), 0., 1.e-12); + EXPECT_NEAR(std::abs(coordinates[1][p] - ca[p]), 0., 1.e-12); + } + const std::string path = "STERNHEIMER_ANALYTIC_AO_TEST.dat"; + { + std::ofstream out(path); + out << "ABACUS_STERNHEIMER_AO_POTENTIALS 1 2 1 Gamma_Hartree\n" + "2 0\n0.3 0.4\n0.3 -0.4\n-1 0\n"; + } + const auto matrices = ModuleRI::read_sternheimer_ao_potentials(path, 2, 1); + const auto tensor = ModuleRI::contract_sternheimer_ao_potentials(matrices, {coordinates[0]}, {coordinates[1]}); + const Complex expected = std::conj(ca[0]) * (2. * ci[0] + Complex(0.3, 0.4) * ci[1]) + + std::conj(ca[1]) * (Complex(0.3, -0.4) * ci[0] - ci[1]); + EXPECT_NEAR(std::abs(tensor.at(0, 0, 0) - expected), 0., 1.e-12); + std::vector branch(1, 0.); + ModuleRI::accumulate_sternheimer_analytic_ao_column(tensor, 0, {Complex(0.2, -0.1)}, 2., 0, branch); + EXPECT_NEAR(std::abs(branch[0] - 2. * std::conj(expected) * Complex(0.2, -0.1)), 0., 1.e-12); + EXPECT_THROW(ModuleRI::read_sternheimer_ao_potentials(path, 3, 1), std::runtime_error); + const std::vector outside = {0., 0., 1.}; + EXPECT_THROW(ModuleRI::recover_delta_grid_ao_coefficients(basis, {&outside}, 0.4, 1.e-10, error), + std::runtime_error); + std::remove(path.c_str()); +} + +TEST(SternheimerGridDiagnostics, AnalyticAOCoordinatesRejectIllConditionedSampledBasis) +{ + ModuleRI::SternheimerDeltaGridFunction a, b; + a.values = {1., 0.}; + b.values = {1., 1.e-7}; + const std::vector state = {2., 1.e-7}; + double error = -1.; + // Tiny reconstruction error alone does not bound the AO coefficient error. + EXPECT_THROW(ModuleRI::recover_delta_grid_ao_coefficients({a, b}, {&state}, 1., 1.e-8, error), std::runtime_error); +} + +TEST(SternheimerGridDiagnostics, AnalyticAOPotentialReaderRejectsMalformedData) +{ + const std::string path = "STERNHEIMER_ANALYTIC_AO_BAD.dat"; + for (const std::string& data: + {"2 0\n0.3 0.4\n0.3 0.4\n-1 0\n", "nan 0\n0 0\n0 0\n1 0\n", "1 0\n0 0\n0 0\n", "1 0\n0 0\n0 0\n1 0\nextra\n"}) + { + std::ofstream out(path); + out << "ABACUS_STERNHEIMER_AO_POTENTIALS 1 2 1 Gamma_Hartree\n" << data; + out.close(); + EXPECT_THROW(ModuleRI::read_sternheimer_ao_potentials(path, 2, 1), std::runtime_error); + } + std::remove(path.c_str()); +} + +TEST(SternheimerGridDiagnostics, MultiChannelAnalyticAOProjectionMatchesDirectComplexContraction) +{ + ModuleRI::SternheimerAOPotentials potentials; + potentials.nao = 2; + potentials.naux = 2; + potentials.values + = {2.0, Complex(0.3, 0.4), Complex(0.3, -0.4), -1.0, -0.7, Complex(-0.2, 0.5), Complex(-0.2, -0.5), 1.3}; + const std::vector> occupied + = {{Complex(0.7, 0.1), Complex(-0.2, 0.3)}, {Complex(0.2, -0.4), Complex(0.5, 0.1)}}; + const std::vector> virtuals + = {{Complex(-0.1, 0.4), Complex(0.6, -0.2)}, {Complex(0.3, 0.2), Complex(-0.2, 0.5)}}; + const std::vector occupations = {2.0, 0.7}; + const auto tensor = ModuleRI::contract_sternheimer_ao_potentials(potentials, occupied, virtuals); + std::vector branch(4, Complex(0.13, -0.07)); + std::vector expected = branch; + for (int channel = 0; channel != 2; ++channel) + { + for (int i = 0; i != 2; ++i) + { + const std::vector response + = {Complex(0.2 * (channel + 1), -0.1 * (i + 1)), Complex(-0.3 * (i + 1), 0.15 * (channel + 1))}; + ModuleRI::accumulate_sternheimer_analytic_ao_column(tensor, i, response, occupations[i], channel, branch); + for (int mu = 0; mu != 2; ++mu) + { + for (int a = 0; a != 2; ++a) + { + for (int p = 0; p != 2; ++p) + { + for (int q = 0; q != 2; ++q) + { + expected[mu * 2 + channel] += occupations[i] * virtuals[a][p] + * std::conj(potentials.values[mu * 4 + p * 2 + q]) + * std::conj(occupied[i][q]) * response[a]; + } + } + } + } + } + } + for (std::size_t index = 0; index != branch.size(); ++index) + { + EXPECT_NEAR(std::abs(branch[index] - expected[index]), 0.0, 1.e-12); + } + EXPECT_THROW(ModuleRI::contract_sternheimer_ao_potentials(potentials, {{1.0}}, virtuals), std::invalid_argument); + EXPECT_THROW(ModuleRI::accumulate_sternheimer_analytic_ao_column(tensor, 0, {1.0}, 2.0, 0, branch), + std::invalid_argument); + EXPECT_THROW(ModuleRI::accumulate_sternheimer_analytic_ao_column(tensor, 0, {1.0, 1.0}, 2.0, 2, branch), + std::invalid_argument); +} + +TEST(SternheimerGridDiagnostics, MeasuresResponseComponentReconstruction) +{ + const std::vector sos = {Complex(1.0, 0.5), Complex(-0.5, 0.25)}; + const std::vector pulay = {Complex(0.25, -0.5), Complex(0.75, 0.0)}; + const std::vector qspace = {Complex(-0.1, 0.0), Complex(0.0, 0.5)}; + std::vector total(sos.size()); + for (std::size_t index = 0; index != total.size(); ++index) + { + total[index] = sos[index] + pulay[index] + qspace[index]; + } + + EXPECT_NEAR(ModuleRI::relative_component_reconstruction_error(total, sos, pulay, qspace), 0.0, 1.0e-15); + + total[0] += Complex(1.0e-4, 0.0); + EXPECT_GT(ModuleRI::relative_component_reconstruction_error(total, sos, pulay, qspace), 0.0); + EXPECT_THROW(ModuleRI::relative_component_reconstruction_error(total, sos, pulay, {}), std::invalid_argument); +} + +TEST(SternheimerGridDiagnostics, ValidatesProductionModeAndStableComponentFilenames) +{ + EXPECT_NO_THROW(ModuleRI::validate_grid_diagnostic_request(false, false, false, false, false)); + EXPECT_NO_THROW(ModuleRI::validate_grid_diagnostic_request(true, true, true, true, true)); + EXPECT_THROW(ModuleRI::validate_grid_diagnostic_request(true, false, true, true, true), std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_grid_diagnostic_request(true, true, false, true, true), std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_grid_diagnostic_request(true, true, true, false, true), std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_grid_diagnostic_request(true, true, true, true, false), std::invalid_argument); + + EXPECT_EQ(ModuleRI::sternheimer_component_v1_filename("sos", 1, 4, 3), + "v1_sternheimer_component_sos_iq_1_ifreq_4_rank3.dat"); + EXPECT_THROW(ModuleRI::sternheimer_component_v1_filename("total", 1, 4, 3), std::invalid_argument); +} + +TEST(SternheimerGridDiagnostics, DistributesRawKSVirtualPerturbationRows) +{ + ModuleRI::SternheimerDeltaGridFunction first_virtual; + first_virtual.values = {Complex(1.0, 0.0), Complex(0.0, 0.0)}; + ModuleRI::SternheimerDeltaGridFunction second_virtual; + second_virtual.values = {Complex(0.0, 0.0), Complex(0.0, 1.0)}; + const std::vector virtuals = {first_virtual, second_virtual}; + const std::vector> potentials = {{2.0, 3.0}, {-1.0, 4.0}}; + const std::vector> occupied = {{Complex(1.0, 0.0), Complex(2.0, 0.0)}}; + + auto rank0 = ModuleRI::build_local_delta_perturbation_tensor(virtuals, potentials, occupied, 0.5, 0, 2); + const auto rank1 = ModuleRI::build_local_delta_perturbation_tensor(virtuals, potentials, occupied, 0.5, 1, 2); + ASSERT_EQ(rank0.row_counts, (std::vector{1, 0})); + ASSERT_EQ(rank1.row_counts, (std::vector{0, 1})); + + for (std::size_t index = 0; index != rank0.tensor.values.size(); ++index) + { + rank0.tensor.values[index] += rank1.tensor.values[index]; + } + EXPECT_EQ(rank0.tensor.at(0, 0, 0), Complex(1.0, 0.0)); + EXPECT_EQ(rank0.tensor.at(0, 1, 0), Complex(0.0, -3.0)); + EXPECT_EQ(rank0.tensor.at(0, 0, 1), Complex(-0.5, 0.0)); + EXPECT_EQ(rank0.tensor.at(0, 1, 1), Complex(0.0, -4.0)); +} + +TEST(SternheimerGridDiagnostics, WritesVersionedOperatorAndPerturbationData) +{ + ModuleRI::SternheimerDeltaGridMatrices matrices; + matrices.overlap = {Complex(1.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(1.0, 0.0)}; + matrices.kinetic = {Complex(1.0, 0.0), Complex(0.1, 0.2), Complex(0.1, -0.2), Complex(2.0, 0.0)}; + matrices.local_potential = {Complex(-0.5, 0.0), Complex(0.0, 0.1), Complex(0.0, -0.1), Complex(-0.25, 0.0)}; + matrices.nonlocal = {Complex(0.2, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.3, 0.0)}; + matrices.hamiltonian.resize(4); + for (std::size_t index = 0; index != matrices.hamiltonian.size(); ++index) + { + matrices.hamiltonian[index] + = matrices.kinetic[index] + matrices.local_potential[index] + matrices.nonlocal[index]; + } + + ModuleRI::SternheimerGridDiagnosticMetadata metadata; + metadata.nx = 2; + metadata.ny = 1; + metadata.nz = 1; + metadata.spin = 1; + metadata.occupied = 1; + metadata.virtuals = 2; + metadata.auxiliaries = 3; + metadata.volume_element = 0.5; + + const std::string matrix_path = "STERNHEIMER_GRID_DIAGNOSTICS_TEST_MATRICES.dat"; + const std::string perturbation_path = "STERNHEIMER_GRID_DIAGNOSTICS_TEST_PERTURBATION.dat"; + const std::vector occupied_virtual_overlap = {Complex(0.01, 0.02), Complex(0.03, 0.04)}; + + EXPECT_NEAR(ModuleRI::relative_operator_reconstruction_error(matrices), 0.0, 1.0e-15); + ModuleRI::write_delta_grid_matrices(matrix_path, metadata, matrices, occupied_virtual_overlap, 1.0e-12); + + const std::string matrix_text = read_text_file(matrix_path); + EXPECT_NE(matrix_text.find("ABACUS_STERNHEIMER_GRID_DIAGNOSTICS 1"), std::string::npos); + EXPECT_NE(matrix_text.find("grid 2 1 1"), std::string::npos); + EXPECT_NE(matrix_text.find("spin 1"), std::string::npos); + EXPECT_NE(matrix_text.find("energy_unit Rydberg"), std::string::npos); + EXPECT_NE(matrix_text.find("matrix overlap"), std::string::npos); + EXPECT_NE(matrix_text.find("matrix kinetic"), std::string::npos); + EXPECT_NE(matrix_text.find("matrix local_potential"), std::string::npos); + EXPECT_NE(matrix_text.find("matrix nonlocal"), std::string::npos); + EXPECT_NE(matrix_text.find("matrix hamiltonian"), std::string::npos); + EXPECT_NE(matrix_text.find("matrix occupied_virtual_overlap"), std::string::npos); + EXPECT_NE(matrix_text.find("0 1"), std::string::npos); + + ModuleRI::SternheimerPerturbationTensor tensor(1, 2, 3); + tensor.at(0, 1, 2) = Complex(0.75, -0.25); + EXPECT_EQ(tensor.at(0, 1, 2), Complex(0.75, -0.25)); + EXPECT_THROW(tensor.at(1, 0, 0), std::out_of_range); + ModuleRI::write_delta_perturbation_tensor(perturbation_path, metadata, tensor); + + const std::string perturbation_text = read_text_file(perturbation_path); + EXPECT_NE(perturbation_text.find("ABACUS_STERNHEIMER_GRID_DIAGNOSTICS 1"), std::string::npos); + EXPECT_NE(perturbation_text.find("potential_unit Rydberg"), std::string::npos); + EXPECT_NE(perturbation_text.find("tensor perturbation occupied virtual auxiliary"), std::string::npos); + EXPECT_NE(perturbation_text.find("0 1 2"), std::string::npos); + + ModuleRI::SternheimerDeltaGridMatrices inconsistent = matrices; + inconsistent.hamiltonian[0] += Complex(1.0e-4, 0.0); + EXPECT_THROW( + ModuleRI::write_delta_grid_matrices(matrix_path, metadata, inconsistent, occupied_virtual_overlap, 1.0e-12), + std::runtime_error); + + ModuleRI::SternheimerGridDiagnosticMetadata invalid_metadata = metadata; + invalid_metadata.volume_element = 0.0; + EXPECT_THROW( + ModuleRI::write_delta_grid_matrices(matrix_path, invalid_metadata, matrices, occupied_virtual_overlap, 1.0e-12), + std::invalid_argument); + invalid_metadata = metadata; + invalid_metadata.auxiliaries = 4; + EXPECT_THROW(ModuleRI::write_delta_perturbation_tensor(perturbation_path, invalid_metadata, tensor), + std::invalid_argument); + + std::remove(matrix_path.c_str()); + std::remove(perturbation_path.c_str()); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_grid_transfer_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_grid_transfer_test.cpp new file mode 100644 index 00000000000..ad5cc7cb48a --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_grid_transfer_test.cpp @@ -0,0 +1,749 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_transfer.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Transfer = ModuleRI::SternheimerGridTransfer; +using Grid = Transfer::Grid; +using Vector = Transfer::Vector; +using Complex = ModuleRI::SternheimerFDHamiltonian::Complex; +using Lattice = ModuleRI::SternheimerFDLatticeVectors; + +const Lattice oblique_cell{{{5.0, 0.0, 0.0}, {-2.5, 4.3, 0.0}, {0.4, -0.2, 8.0}}}; + +std::size_t size(const Grid& grid) +{ + return static_cast(grid.nx) * grid.ny * grid.nz; +} + +Grid make_grid(const std::array& dimensions, + const ModuleRI::SternheimerReducedKPoint& kpoint = {0.0, 0.0, 0.0}) +{ + Grid grid; + grid.nx = dimensions[0]; + grid.ny = dimensions[1]; + grid.nz = dimensions[2]; + grid.lattice_vectors = oblique_cell; + grid.kpoint = kpoint; + const auto length = [](const std::array& a) { + return std::sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]); + }; + grid.hx = length(oblique_cell[0]) / grid.nx; + grid.hy = length(oblique_cell[1]) / grid.ny; + grid.hz = length(oblique_cell[2]) / grid.nz; + return grid; +} + +double volume(const Grid& grid) +{ + const auto a = ModuleRI::sternheimer_fd_grid_lattice_vectors(grid); + return std::abs(a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) + - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0]) + + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])); +} + +Complex dot(const Vector& left, const Vector& right, const Grid& grid) +{ + Complex result(0.0, 0.0); + for (std::size_t i = 0; i != left.size(); ++i) + { + result += std::conj(left[i]) * right[i]; + } + return result * (volume(grid) / static_cast(size(grid))); +} + +Vector samples(const Grid& grid, const double seed) +{ + Vector result(size(grid)); + for (std::size_t i = 0; i != result.size(); ++i) + { + const double x = static_cast(i); + result[i] = Complex(std::sin(0.37 * x + seed), std::cos(0.23 * x - 2.0 * seed)); + } + return result; +} + +Vector mode(const Grid& grid, const std::array& frequencies, const double phase = 0.0) +{ + Vector result(size(grid)); + const double two_pi = 2.0 * std::acos(-1.0); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const double angle = phase + two_pi + * ((frequencies[0] + grid.kpoint[0]) * ix / grid.nx + + (frequencies[1] + grid.kpoint[1]) * iy / grid.ny + + (frequencies[2] + grid.kpoint[2]) * iz / grid.nz); + const std::size_t index = (static_cast(ix) * grid.ny + iy) * grid.nz + iz; + result[index] = std::exp(Complex(0.0, angle)); + } + } + } + return result; +} + +void expect_near(const Vector& actual, const Vector& expected, const double tolerance = 2.0e-12) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t i = 0; i != actual.size(); ++i) + { + EXPECT_NEAR(std::abs(actual[i] - expected[i]), 0.0, tolerance) << "index " << i; + } +} + +// Independently solve a_i dot q = 2 pi (m_i + k_i) for the triangular test cell. +std::array wavevector(const Grid& grid, const std::array& frequencies) +{ + const double two_pi = 2.0 * std::acos(-1.0); + const double qx = two_pi * (frequencies[0] + grid.kpoint[0]) / 5.0; + const double qy = (two_pi * (frequencies[1] + grid.kpoint[1]) + 2.5 * qx) / 4.3; + const double qz = (two_pi * (frequencies[2] + grid.kpoint[2]) - 0.4 * qx + 0.2 * qy) / 8.0; + return {qx, qy, qz}; +} + +std::array direct_gradients(const Grid& coarse, const Grid& fine, const Vector& input) +{ + std::array expected; + for (auto& field : expected) + { + field.resize(size(fine)); + } + for (int mx = -(coarse.nx - 1) / 2; mx <= coarse.nx / 2; ++mx) + { + for (int my = -(coarse.ny - 1) / 2; my <= coarse.ny / 2; ++my) + { + for (int mz = -(coarse.nz - 1) / 2; mz <= coarse.nz / 2; ++mz) + { + const std::array frequencies{mx, my, mz}; + const Vector fine_mode = mode(fine, frequencies); + const Complex coefficient = dot(mode(coarse, frequencies), input, coarse) / volume(coarse); + const auto q = wavevector(coarse, frequencies); + for (int axis = 0; axis != 3; ++axis) + { + for (std::size_t i = 0; i != size(fine); ++i) + { + expected[axis][i] += Complex(0.0, q[axis]) * coefficient * fine_mode[i]; + } + } + } + } + } + return expected; +} + +} // namespace + +TEST(SternheimerGridTransfer, SameGridIsExactIdentityIncludingInPlace) +{ + const Grid grid = make_grid({4, 5, 3}, {0.21, -0.17, 0.08}); + Transfer transfer(grid, grid); + const Vector input = samples(grid, 0.31); + Vector output; + transfer.interpolate(input, output); + EXPECT_EQ(output, input); + transfer.restrict_adjoint(input, output); + EXPECT_EQ(output, input); + transfer.interpolate(output, output); + transfer.restrict_adjoint(output, output); + EXPECT_EQ(output, input); + EXPECT_EQ(transfer.coarse_grid().nx, grid.nx); + EXPECT_EQ(transfer.fine_grid().kpoint, grid.kpoint); +} + +TEST(SternheimerGridTransfer, PreservesWeightedNormOnObliqueBlochGrid) +{ + const Grid coarse = make_grid({4, 5, 3}, {0.21, -0.17, 0.08}); + const Grid fine = make_grid({9, 8, 7}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector input = samples(coarse, 0.31); + Vector output; + transfer.interpolate(input, output); + EXPECT_NEAR(dot(output, output, fine).real(), dot(input, input, coarse).real(), 2.0e-10); +} + +TEST(SternheimerGridTransfer, RestrictionIsVolumeWeightedAdjoint) +{ + const Grid coarse = make_grid({5, 4, 3}, {-0.31, 0.13, 0.27}); + const Grid fine = make_grid({8, 9, 7}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector x = samples(coarse, 0.19); + const Vector y = samples(fine, 0.73); + Vector jx; + Vector adjoint_y; + transfer.interpolate(x, jx); + transfer.restrict_adjoint(y, adjoint_y); + EXPECT_NEAR(std::abs(dot(jx, y, fine) - dot(x, adjoint_y, coarse)), 0.0, 2.0e-11); +} + +TEST(SternheimerGridTransfer, RoundTripRecoversArbitraryComplexCoarseVector) +{ + const Grid coarse = make_grid({4, 3, 5}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({7, 8, 9}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector input = samples(coarse, 0.41); + Vector lifted; + Vector recovered; + transfer.interpolate(input, lifted); + transfer.restrict_adjoint(lifted, recovered); + expect_near(recovered, input); +} + +TEST(SternheimerGridTransfer, MatchesIndependentDirectFourierSum) +{ + const Grid coarse = make_grid({3, 4, 2}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({5, 7, 4}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector input = samples(coarse, 0.37); + Vector expected(size(fine), Complex(0.0, 0.0)); + for (int mx = -(coarse.nx - 1) / 2; mx <= coarse.nx / 2; ++mx) + { + for (int my = -(coarse.ny - 1) / 2; my <= coarse.ny / 2; ++my) + { + for (int mz = -(coarse.nz - 1) / 2; mz <= coarse.nz / 2; ++mz) + { + const Vector coarse_mode = mode(coarse, {mx, my, mz}); + const Vector fine_mode = mode(fine, {mx, my, mz}); + const Complex coefficient = dot(coarse_mode, input, coarse) / volume(coarse); + for (std::size_t i = 0; i != expected.size(); ++i) + { + expected[i] += coefficient * fine_mode[i]; + } + } + } + } + Vector actual; + transfer.interpolate(input, actual); + expect_near(actual, expected); +} + +TEST(SternheimerGridTransfer, InterpolatesAnisotropicObliqueBlochMode) +{ + const Grid coarse = make_grid({5, 4, 3}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({8, 7, 3}, coarse.kpoint); + Transfer transfer(coarse, fine); + Vector actual; + transfer.interpolate(mode(coarse, {-2, 1, -1}, 0.47), actual); + expect_near(actual, mode(fine, {-2, 1, -1}, 0.47)); +} + +TEST(SternheimerGridTransfer, RemovesFineOnlyModesWithoutAliasing) +{ + const Grid coarse = make_grid({4, 3, 3}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({9, 7, 7}, coarse.kpoint); + Transfer transfer(coarse, fine); + for (const auto& frequencies : std::vector>{{3, 0, 0}, {0, -2, 0}, {0, 0, 2}}) + { + Vector actual; + transfer.restrict_adjoint(mode(fine, frequencies, 0.47), actual); + expect_near(actual, Vector(size(coarse), Complex(0.0, 0.0))); + } +} + +TEST(SternheimerGridTransfer, PositiveNyquistRetainsComplexPhaseWithoutPairMerging) +{ + const Grid coarse = make_grid({4, 6, 4}, {0.21, 0.08, -0.17}); + const Grid fine = make_grid({10, 12, 9}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector input = mode(coarse, {2, 3, 2}, 0.63); + Vector actual; + transfer.interpolate(input, actual); + expect_near(actual, mode(fine, {2, 3, 2}, 0.63)); + transfer.restrict_adjoint(actual, actual); + expect_near(actual, input); + transfer.restrict_adjoint(mode(fine, {-2, 0, 0}, 0.63), actual); + expect_near(actual, Vector(size(coarse), Complex(0.0, 0.0))); +} + +TEST(SternheimerGridTransfer, IntegerShiftedBlochLabelsPreserveCommonPhysicalMode) +{ + const Grid coarse = make_grid({6, 6, 4}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({11, 10, 9}, coarse.kpoint); + Grid shifted_coarse = coarse; + shifted_coarse.kpoint[0] += 1.0; + shifted_coarse.kpoint[1] -= 1.0; + Grid shifted_fine = fine; + shifted_fine.kpoint = shifted_coarse.kpoint; + Transfer original(coarse, fine); + Transfer shifted(shifted_coarse, shifted_fine); + Vector original_result; + Vector shifted_result; + original.interpolate(mode(coarse, {1, -1, 1}, 0.29), original_result); + shifted.interpolate(mode(shifted_coarse, {0, 0, 1}, 0.29), shifted_result); + expect_near(shifted_result, original_result); + expect_near(shifted_result, mode(fine, {1, -1, 1}, 0.29)); +} + +TEST(SternheimerGridTransfer, SupportsInPlaceTransferAndIndependentInstances) +{ + const Grid coarse = make_grid({3, 4, 5}, {0.1, 0.2, -0.3}); + const Grid fine = make_grid({7, 8, 9}, coarse.kpoint); + Transfer first(coarse, fine); + Transfer second(coarse, fine); + const Vector input = samples(coarse, 0.31); + Vector in_place = input; + Vector other; + first.interpolate(in_place, in_place); + second.interpolate(samples(coarse, 0.79), other); + first.restrict_adjoint(in_place, in_place); + expect_near(in_place, input); + second.restrict_adjoint(other, other); + expect_near(other, samples(coarse, 0.79)); +} + +TEST(SternheimerGridTransfer, SeparateWorkersOwnIndependentPlans) +{ + std::vector results(4); +#ifdef _OPENMP +#pragma omp parallel for num_threads(4) +#endif + for (int worker = 0; worker < 4; ++worker) + { + const Grid coarse = make_grid({3 + worker, 4, 3}, {0.13 * worker, -0.11, 0.08}); + const Grid fine = make_grid({8 + worker, 7, 6}, coarse.kpoint); + Transfer transfer(coarse, fine); + Vector lifted; + transfer.interpolate(samples(coarse, 0.31), lifted); + transfer.restrict_adjoint(lifted, results[static_cast(worker)]); + } + for (int worker = 0; worker != 4; ++worker) + { + const Grid coarse = make_grid({3 + worker, 4, 3}, {0.13 * worker, -0.11, 0.08}); + expect_near(results[static_cast(worker)], samples(coarse, 0.31)); + } +} + +TEST(SternheimerGridTransfer, ReportsNumericWorkspaceWithoutAllocatingIt) +{ + const Grid coarse = make_grid({3, 4, 5}); + const Grid fine = make_grid({7, 8, 9}); + const std::size_t expected = (size(coarse) + size(fine)) * (2 * sizeof(double) + sizeof(Complex)) + + size(coarse) * sizeof(std::size_t); + EXPECT_EQ(Transfer::workspace_bytes_required(coarse, fine), expected); + EXPECT_EQ(Transfer::workspace_bytes_required(coarse, coarse), 0U); + Grid invalid = fine; + invalid.nx = 2; + EXPECT_THROW(Transfer::workspace_bytes_required(coarse, invalid), std::invalid_argument); +} + +TEST(SternheimerGridTransfer, AcceptsImplicitAndExplicitIdenticalOrthogonalCells) +{ + Grid coarse{4, 3, 2, 1.0, 2.0, 3.0, true}; + Grid fine{8, 9, 4, 0.5, 2.0 / 3.0, 1.5, true}; + fine.lattice_vectors = {{{4.0, 0.0, 0.0}, {0.0, 6.0, 0.0}, {0.0, 0.0, 6.0}}}; + Transfer transfer(coarse, fine); + Vector actual; + transfer.interpolate(mode(coarse, {1, 1, 0}), actual); + expect_near(actual, mode(fine, {1, 1, 0})); +} + +TEST(SternheimerGridTransfer, RejectsMismatchedCellKPointAndNonperiodicGrids) +{ + const Grid coarse = make_grid({4, 4, 4}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({8, 8, 8}, coarse.kpoint); + Grid invalid = fine; + invalid.lattice_vectors[1][0] += 0.01; + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = fine; + invalid.kpoint[0] += 1.0; + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = fine; + invalid.periodic = false; + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = coarse; + invalid.periodic = false; + EXPECT_THROW((Transfer(invalid, fine)), std::invalid_argument); +} + +TEST(SternheimerGridTransfer, RejectsInvalidDimensionsAndNonfiniteGeometry) +{ + const Grid coarse = make_grid({4, 4, 4}); + const Grid fine = make_grid({8, 8, 8}); + Grid invalid = fine; + invalid.nx = 3; + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = coarse; + invalid.ny = 0; + EXPECT_THROW((Transfer(invalid, fine)), std::invalid_argument); + invalid.ny = -1; + EXPECT_THROW((Transfer(invalid, fine)), std::invalid_argument); + invalid = fine; + invalid.nx = std::numeric_limits::max(); + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = fine; + invalid.hz = 0.0; + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = fine; + invalid.kpoint[1] = std::numeric_limits::quiet_NaN(); + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = fine; + invalid.lattice_vectors[0][0] = std::numeric_limits::infinity(); + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); + invalid = fine; + invalid.lattice_vectors[2] = invalid.lattice_vectors[0]; + EXPECT_THROW((Transfer(coarse, invalid)), std::invalid_argument); +} + +TEST(SternheimerGridTransfer, RejectsWrongVectorSizesBeforeIdentityOrFFT) +{ + const Grid coarse = make_grid({3, 4, 5}); + const Grid fine = make_grid({7, 8, 9}); + Transfer transfer(coarse, fine); + Transfer identity(coarse, coarse); + Vector output; + EXPECT_THROW(transfer.interpolate(Vector(size(coarse) - 1), output), std::invalid_argument); + EXPECT_THROW(transfer.restrict_adjoint(Vector(size(fine) + 1), output), std::invalid_argument); + EXPECT_THROW(identity.interpolate(Vector(), output), std::invalid_argument); + EXPECT_THROW(identity.restrict_adjoint(Vector(), output), std::invalid_argument); +} + +TEST(SternheimerGridTransfer, MoveTransfersOwnedPlans) +{ + const Grid coarse = make_grid({3, 4, 5}); + const Grid fine = make_grid({7, 8, 9}); + Transfer original(coarse, fine); + Transfer moved(std::move(original)); + Transfer assigned(coarse, coarse); + assigned = std::move(moved); + Vector actual; + assigned.interpolate(mode(coarse, {1, -1, 2}), actual); + expect_near(actual, mode(fine, {1, -1, 2})); +} + +TEST(SternheimerGridTransfer, GradientsMatchIndependentObliqueBlochFourierSum) +{ + const Grid coarse = make_grid({3, 4, 2}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({5, 7, 4}, coarse.kpoint); + const Vector input = samples(coarse, 0.37); + Transfer transfer(coarse, fine); + Vector values; + Vector original_values; + std::array gradients; + transfer.interpolate(input, original_values); + transfer.interpolate_with_gradients(input, values, gradients); + expect_near(values, original_values); + const auto expected = direct_gradients(coarse, fine, input); + for (int axis = 0; axis != 3; ++axis) + { + expect_near(gradients[axis], expected[axis]); + } +} + +TEST(SternheimerGridTransfer, GradientRestrictionIsWeightedAdjointAndMatchesDirectSum) +{ + const Grid coarse = make_grid({3, 4, 2}, {-0.31, 0.13, 0.27}); + const Grid fine = make_grid({5, 7, 4}, coarse.kpoint); + const Vector input = samples(coarse, 0.19); + const std::array fields{{samples(fine, 0.73), samples(fine, -0.41), samples(fine, 0.17)}}; + Transfer transfer(coarse, fine); + Vector values; + Vector adjoint; + std::array gradients; + transfer.interpolate_with_gradients(input, values, gradients); + transfer.restrict_gradient_adjoint(fields, adjoint); + Complex left(0.0, 0.0); + for (int axis = 0; axis != 3; ++axis) + { + left += dot(gradients[axis], fields[axis], fine); + } + EXPECT_NEAR(std::abs(left - dot(input, adjoint, coarse)), 0.0, 3.0e-11); + Vector expected(size(coarse)); + for (int mx = -(coarse.nx - 1) / 2; mx <= coarse.nx / 2; ++mx) + { + for (int my = -(coarse.ny - 1) / 2; my <= coarse.ny / 2; ++my) + { + for (int mz = -(coarse.nz - 1) / 2; mz <= coarse.nz / 2; ++mz) + { + const std::array frequencies{mx, my, mz}; + const auto q = wavevector(coarse, frequencies); + const Vector fine_mode = mode(fine, frequencies); + const Vector coarse_mode = mode(coarse, frequencies); + Complex coefficient(0.0, 0.0); + for (int axis = 0; axis != 3; ++axis) + { + coefficient += Complex(0.0, -q[axis]) * dot(fine_mode, fields[axis], fine) / volume(fine); + } + for (std::size_t i = 0; i != expected.size(); ++i) + { + expected[i] += coefficient * coarse_mode[i]; + } + } + } + } + expect_near(adjoint, expected); +} + +TEST(SternheimerGridTransfer, SameGridGradientsAreDerivativesWhileValuesRemainExact) +{ + const Grid grid = make_grid({3, 4, 2}, {0.21, -0.17, 0.08}); + const Vector input = samples(grid, 0.31); + const std::array fields{{samples(grid, 0.1), samples(grid, -0.3), samples(grid, 0.7)}}; + Transfer transfer(grid, grid); + Vector values; + Vector adjoint; + std::array gradients; + transfer.interpolate_with_gradients(input, values, gradients); + EXPECT_EQ(values, input); + const auto expected = direct_gradients(grid, grid, input); + Complex left(0.0, 0.0); + for (int axis = 0; axis != 3; ++axis) + { + expect_near(gradients[axis], expected[axis]); + left += dot(gradients[axis], fields[axis], grid); + } + transfer.restrict_gradient_adjoint(fields, adjoint); + EXPECT_NEAR(std::abs(left - dot(input, adjoint, grid)), 0.0, 3.0e-11); + transfer.interpolate(input, values); + EXPECT_EQ(values, input); + transfer.restrict_adjoint(input, values); + EXPECT_EQ(values, input); +} + +TEST(SternheimerGridTransfer, GradientNyquistUsesPositiveComplexModeWithoutTRClaim) +{ + const Grid coarse = make_grid({4, 6, 4}, {0.21, 0.08, -0.17}); + const Grid fine = make_grid({9, 10, 7}, coarse.kpoint); + const std::array frequencies{2, 3, 2}; + const auto q = wavevector(coarse, frequencies); + Transfer transfer(coarse, fine); + Vector values; + std::array gradients; + transfer.interpolate_with_gradients(mode(coarse, frequencies, 0.63), values, gradients); + const Vector fine_mode = mode(fine, frequencies, 0.63); + for (int axis = 0; axis != 3; ++axis) + { + Vector expected = fine_mode; + for (auto& value : expected) + { + value *= Complex(0.0, q[axis]); + } + expect_near(gradients[axis], expected, 5.0e-12); + } + std::array fields{{mode(fine, {-2, 0, 0}), mode(fine, {3, 0, 0}), mode(fine, {0, -4, 0})}}; + Vector adjoint; + transfer.restrict_gradient_adjoint(fields, adjoint); + expect_near(adjoint, Vector(size(coarse))); +} + +TEST(SternheimerGridTransfer, GradientIntegerShiftedBlochLabelsPreserveCommonPhysicalMode) +{ + const Grid coarse = make_grid({6, 6, 4}, {0.17, -0.11, 0.08}); + const Grid fine = make_grid({11, 10, 9}, coarse.kpoint); + Grid shifted_coarse = coarse; + shifted_coarse.kpoint[0] += 1.0; + shifted_coarse.kpoint[1] -= 1.0; + Grid shifted_fine = fine; + shifted_fine.kpoint = shifted_coarse.kpoint; + Transfer original(coarse, fine); + Transfer shifted(shifted_coarse, shifted_fine); + Vector values; + Vector shifted_values; + std::array gradients; + std::array shifted_gradients; + original.interpolate_with_gradients(mode(coarse, {1, -1, 1}, 0.29), values, gradients); + shifted.interpolate_with_gradients(mode(shifted_coarse, {0, 0, 1}, 0.29), shifted_values, shifted_gradients); + expect_near(shifted_values, values); + for (int axis = 0; axis != 3; ++axis) + { + expect_near(shifted_gradients[axis], gradients[axis]); + } +} + +TEST(SternheimerGridTransfer, GradientAPIsSupportInputOutputAliasingButRejectOverlappingOutputs) +{ + for (const bool same_grid : {false, true}) + { + const Grid coarse = make_grid({3, 4, 2}, {0.17, -0.11, 0.08}); + const Grid fine = same_grid ? coarse : make_grid({5, 7, 4}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector input = samples(coarse, 0.37); + Vector values; + std::array expected; + transfer.interpolate_with_gradients(input, values, expected); + Vector in_place = input; + std::array gradients; + transfer.interpolate_with_gradients(in_place, in_place, gradients); + expect_near(in_place, values); + gradients[1] = input; + transfer.interpolate_with_gradients(gradients[1], in_place, gradients); + expect_near(in_place, values); + for (int axis = 0; axis != 3; ++axis) + { + expect_near(gradients[axis], expected[axis]); + } + Vector expected_adjoint; + transfer.restrict_gradient_adjoint(expected, expected_adjoint); + transfer.restrict_gradient_adjoint(gradients, gradients[2]); + expect_near(gradients[2], expected_adjoint); + const auto snapshot = gradients; + EXPECT_THROW(transfer.interpolate_with_gradients(input, gradients[0], gradients), std::invalid_argument); + EXPECT_EQ(gradients, snapshot); + } +} + +TEST(SternheimerGridTransfer, GradientValidationPrecedesAnyOutputChanges) +{ + for (const bool same_grid : {false, true}) + { + const Grid coarse = make_grid({3, 4, 2}); + const Grid fine = same_grid ? coarse : make_grid({5, 7, 4}); + Transfer transfer(coarse, fine); + const Vector sentinel(2, Complex(2.0, -3.0)); + Vector values = sentinel; + std::array gradients{{sentinel, sentinel, sentinel}}; + EXPECT_THROW(transfer.interpolate_with_gradients(Vector(size(coarse) - 1), values, gradients), + std::invalid_argument); + Vector bad = samples(coarse, 0.1); + bad[0] = Complex(std::numeric_limits::quiet_NaN(), 0.0); + EXPECT_THROW(transfer.interpolate_with_gradients(bad, values, gradients), std::invalid_argument); + std::array fields{{samples(fine, 0.1), samples(fine, 0.2), samples(fine, 0.3)}}; + fields[2].pop_back(); + EXPECT_THROW(transfer.restrict_gradient_adjoint(fields, values), std::invalid_argument); + fields[2] = samples(fine, 0.3); + fields[2][0] = Complex(0.0, std::numeric_limits::infinity()); + EXPECT_THROW(transfer.restrict_gradient_adjoint(fields, values), std::invalid_argument); + EXPECT_EQ(values, sentinel); + EXPECT_EQ(gradients, (std::array{{sentinel, sentinel, sentinel}})); + } +} + +TEST(SternheimerGridTransfer, GradientWorkspaceIncludesLazyIdentityBuffersAndEnforcesBudget) +{ + const Grid coarse = make_grid({3, 4, 2}, {0.17, -0.11, 0.08}); + for (const bool same_grid : {false, true}) + { + const Grid fine = same_grid ? coarse : make_grid({5, 7, 4}, coarse.kpoint); + const std::size_t expected = (size(coarse) + size(fine)) * (2 * sizeof(double) + sizeof(Complex)) + + size(coarse) * sizeof(std::size_t); + EXPECT_EQ(Transfer::gradient_workspace_bytes_required(coarse, fine), expected); + EXPECT_EQ(Transfer::workspace_bytes_required(coarse, fine), same_grid ? 0U : expected); + Transfer transfer(coarse, fine); + const Vector sentinel(2, Complex(1.0, 2.0)); + Vector values = sentinel; + std::array gradients{{sentinel, sentinel, sentinel}}; + const Vector input = samples(coarse, 0.1); + EXPECT_THROW(transfer.interpolate_with_gradients(input, values, gradients, expected - 1), std::length_error); + EXPECT_EQ(values, sentinel); + EXPECT_EQ(gradients, (std::array{{sentinel, sentinel, sentinel}})); + transfer.interpolate_with_gradients(input, values, gradients, expected); + EXPECT_THROW(transfer.restrict_gradient_adjoint(gradients, values, expected - 1), std::length_error); + EXPECT_NO_THROW(transfer.restrict_gradient_adjoint(gradients, values, expected)); + } + Grid invalid = coarse; + invalid.periodic = false; + EXPECT_THROW(Transfer::gradient_workspace_bytes_required(coarse, invalid), std::invalid_argument); +} + +TEST(SternheimerGridTransfer, GradientContextsAreIndependentAcrossWorkers) +{ + std::array, 4> results; +#ifdef _OPENMP +#pragma omp parallel for num_threads(4) +#endif + for (int worker = 0; worker < 4; ++worker) + { + const Grid coarse = make_grid({3, 4, 2}, {0.13 * worker, -0.11, 0.08}); + const Grid fine = worker % 2 == 0 ? coarse : make_grid({5, 7, 4}, coarse.kpoint); + Transfer transfer(coarse, fine); + Vector values; + transfer.interpolate_with_gradients(samples(coarse, 0.31), values, results[worker]); + } + for (int worker = 0; worker != 4; ++worker) + { + const Grid coarse = make_grid({3, 4, 2}, {0.13 * worker, -0.11, 0.08}); + const Grid fine = worker % 2 == 0 ? coarse : make_grid({5, 7, 4}, coarse.kpoint); + const auto expected = direct_gradients(coarse, fine, samples(coarse, 0.31)); + for (int axis = 0; axis != 3; ++axis) + { + expect_near(results[worker][axis], expected[axis]); + } + } +} + +TEST(SternheimerGridTransfer, SpectralNegativeLaplacianEqualsGradientAdjointGradient) +{ + const Grid coarse = make_grid({3, 4, 2}, {0.21, -0.17, 0.08}); + for (const bool same_grid : {false, true}) + { + const Grid fine = same_grid ? coarse : make_grid({5, 7, 4}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector input = samples(coarse, 0.31); + Vector values; + Vector actual; + Vector composed; + std::array gradients; + transfer.interpolate_with_gradients(input, values, gradients); + transfer.restrict_gradient_adjoint(gradients, composed); + transfer.apply_negative_laplacian(input, actual); + expect_near(actual, composed, 6.0e-12); + Complex gradient_form(0.0, 0.0); + for (const auto& gradient : gradients) + { + gradient_form += dot(gradient, gradient, fine); + } + EXPECT_NEAR(std::abs(dot(input, actual, coarse) - gradient_form), 0.0, 3.0e-10); + Vector in_place = input; + transfer.apply_negative_laplacian(in_place, in_place); + expect_near(in_place, actual); + } +} + +TEST(SternheimerGridTransfer, SpectralNegativeLaplacianHasAnalyticBlochNyquistEigenvalue) +{ + const Grid coarse = make_grid({4, 6, 4}, {0.21, 0.08, -0.17}); + const Grid fine = make_grid({9, 10, 7}, coarse.kpoint); + const std::array frequencies{2, 3, 2}; + const auto q = wavevector(coarse, frequencies); + Vector expected = mode(coarse, frequencies, 0.63); + const Vector input = expected; + for (auto& value : expected) + { + value *= q[0] * q[0] + q[1] * q[1] + q[2] * q[2]; + } + Transfer transfer(coarse, fine); + Vector actual; + transfer.apply_negative_laplacian(input, actual); + expect_near(actual, expected, 8.0e-12); + const auto required = Transfer::gradient_workspace_bytes_required(coarse, fine); + EXPECT_THROW(transfer.apply_negative_laplacian(input, actual, required - 1), std::length_error); + EXPECT_NO_THROW(transfer.apply_negative_laplacian(input, actual, required)); + const Vector snapshot = actual; + EXPECT_THROW(transfer.apply_negative_laplacian(Vector(), actual), std::invalid_argument); + Vector bad = input; + bad[0] = Complex(std::numeric_limits::infinity(), 0.0); + EXPECT_THROW(transfer.apply_negative_laplacian(bad, actual), std::invalid_argument); + EXPECT_EQ(actual, snapshot); +} + +TEST(SternheimerGridTransfer, SpectralOperationsRejectArithmeticOverflow) +{ + const Grid coarse = make_grid({4, 4, 4}, {0.0, 0.0, 0.0}); + const Grid fine = make_grid({6, 6, 6}, coarse.kpoint); + Transfer transfer(coarse, fine); + const Vector input(size(coarse), Complex(1.0e308, 0.0)); + Vector output; + std::array gradients; + EXPECT_THROW(transfer.interpolate_with_gradients(input, output, gradients), std::overflow_error); + EXPECT_THROW(transfer.apply_negative_laplacian(input, output), std::overflow_error); + const Vector field(size(fine), Complex(1.0e308, 0.0)); + const std::array fields{{field, field, field}}; + EXPECT_THROW(transfer.restrict_gradient_adjoint(fields, output), std::overflow_error); +} + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_kq_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_kq_test.cpp new file mode 100644 index 00000000000..1e7ef21a2a8 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_kq_test.cpp @@ -0,0 +1,131 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_kq.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +using KPoint = ModuleRI::SternheimerReducedKPoint; + +void expect_kpoint_near(const KPoint& actual, const KPoint& expected, const double tolerance = 1.0e-14) +{ + for (int direction = 0; direction != 3; ++direction) + { + EXPECT_NEAR(actual[static_cast(direction)], + expected[static_cast(direction)], + tolerance); + } +} + +} // namespace + +TEST(SternheimerKQ, FoldsReducedCoordinatesAndRecordsReciprocalShift) +{ + const auto result = ModuleRI::fold_sternheimer_kpoint({0.6, -0.6, 1.5}); + + expect_kpoint_near(result.kpoint, {-0.4, 0.4, -0.5}); + EXPECT_EQ(result.reciprocal_shift, (std::array{1, -1, 2})); +} + +TEST(SternheimerKQ, MeasuresDistanceAcrossBrillouinZoneBoundary) +{ + const double distance = ModuleRI::periodic_sternheimer_kpoint_distance({0.49, 0.1, -0.2}, {-0.49, 0.1, -0.2}); + EXPECT_NEAR(distance, 0.02, 1.0e-14); +} + +TEST(SternheimerKQ, MapsEverySourcePointToUniqueFoldedKPlusQ) +{ + const std::vector kpoints = { + {-0.25, -0.25, 0.0}, + {0.25, -0.25, 0.0}, + {-0.25, 0.25, 0.0}, + {0.25, 0.25, 0.0}, + }; + + const auto mapping = ModuleRI::build_sternheimer_kq_map(kpoints, {0.5, 0.0, 0.0}, 1.0e-12); + + ASSERT_EQ(mapping.size(), kpoints.size()); + EXPECT_EQ(mapping[0].source_index, 0); + EXPECT_EQ(mapping[0].target_index, 1); + EXPECT_EQ(mapping[1].source_index, 1); + EXPECT_EQ(mapping[1].target_index, 0); + EXPECT_EQ(mapping[2].target_index, 3); + EXPECT_EQ(mapping[3].target_index, 2); + expect_kpoint_near(mapping[0].folded_k_plus_q, kpoints[1]); + expect_kpoint_near(mapping[1].folded_k_plus_q, kpoints[0]); + EXPECT_EQ(mapping[0].reciprocal_shift, (std::array{0, 0, 0})); + EXPECT_EQ(mapping[1].reciprocal_shift, (std::array{1, 0, 0})); +} + +TEST(SternheimerKQ, RecordsShiftRelativeToTheSelectedABACUSBoundaryRepresentative) +{ + const std::vector kpoints = { + {0.0, 0.0, 0.0}, + {0.5, 0.0, 0.0}, + }; + + const auto mapping = ModuleRI::build_sternheimer_kq_map(kpoints, {0.5, 0.0, 0.0}, 1.0e-12); + + ASSERT_EQ(mapping.size(), 2); + EXPECT_EQ(mapping[0].target_index, 1); + expect_kpoint_near(mapping[0].folded_k_plus_q, kpoints[1]); + EXPECT_EQ(mapping[0].reciprocal_shift, (std::array{0, 0, 0})); + EXPECT_EQ(mapping[1].target_index, 0); + expect_kpoint_near(mapping[1].folded_k_plus_q, kpoints[0]); + EXPECT_EQ(mapping[1].reciprocal_shift, (std::array{1, 0, 0})); +} + +TEST(SternheimerKQ, RejectsQThatIsNotCommensurateWithKMesh) +{ + const std::vector kpoints = { + {-0.25, 0.0, 0.0}, + {0.25, 0.0, 0.0}, + }; + + EXPECT_THROW(ModuleRI::build_sternheimer_kq_map(kpoints, {0.125, 0.0, 0.0}, 1.0e-12), std::invalid_argument); +} + +TEST(SternheimerKQ, RejectsDuplicatePeriodicTargetPoints) +{ + const std::vector kpoints = { + {-0.5, 0.0, 0.0}, + {0.5, 0.0, 0.0}, + }; + + EXPECT_THROW(ModuleRI::build_sternheimer_kq_map(kpoints, {0.0, 0.0, 0.0}, 1.0e-12), std::invalid_argument); +} + +TEST(SternheimerKQ, UsesPositiveABACUSBlochPhaseConvention) +{ + const std::complex phase = ModuleRI::sternheimer_bloch_phase({0.25, 0.125, 0.0}, {1, 1, 0}); + const double inverse_sqrt_two = 1.0 / std::sqrt(2.0); + + EXPECT_NEAR(phase.real(), -inverse_sqrt_two, 1.0e-14); + EXPECT_NEAR(phase.imag(), inverse_sqrt_two, 1.0e-14); +} + +TEST(SternheimerKQ, BlochPhasePreservesGammaAndComposesTranslations) +{ + const KPoint gamma{0.0, 0.0, 0.0}; + EXPECT_EQ(ModuleRI::sternheimer_bloch_phase(gamma, {7, -4, 3}), std::complex(1.0, 0.0)); + + const KPoint kpoint{0.125, -0.25, 0.375}; + const std::array first{1, -2, 0}; + const std::array second{-3, 1, 2}; + const std::array sum{ + first[0] + second[0], + first[1] + second[1], + first[2] + second[2], + }; + const std::complex composed + = ModuleRI::sternheimer_bloch_phase(kpoint, first) * ModuleRI::sternheimer_bloch_phase(kpoint, second); + const std::complex direct = ModuleRI::sternheimer_bloch_phase(kpoint, sum); + + EXPECT_NEAR(composed.real(), direct.real(), 1.0e-14); + EXPECT_NEAR(composed.imag(), direct.imag(), 1.0e-14); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_owned_channel_workers_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_owned_channel_workers_test.cpp new file mode 100644 index 00000000000..6bc5401d262 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_owned_channel_workers_test.cpp @@ -0,0 +1,21 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_channel_resources.h" + +#include + +int main() +{ + const ModuleRI::SternheimerMemorySnapshot memory{ModuleRI::SternheimerMemoryAccountingMode::available, + 1000000, + 0, + 1, + "test"}; + + const auto sparse_plan = ModuleRI::plan_sternheimer_owned_channel_workers(36, 6, 30, 1, 0, memory); + const auto bandwidth_plan = ModuleRI::plan_sternheimer_owned_channel_workers(163, 13, 30, 1, 0, memory); + + assert(sparse_plan.automatic_workers == 6); + assert(sparse_plan.effective_workers == 1); + assert(bandwidth_plan.automatic_workers == 13); + assert(bandwidth_plan.effective_workers == 13); + return 0; +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_periodic_solver_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_periodic_solver_test.cpp new file mode 100644 index 00000000000..8171f08d4b3 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_periodic_solver_test.cpp @@ -0,0 +1,607 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_periodic_solver.h" +#include + +#include +#include +#include +#include + +namespace +{ +std::vector excitation_test_states( + const std::vector& eigenvalues) +{ + std::vector states(eigenvalues.size()); + for (std::size_t index = 0; index != eigenvalues.size(); ++index) + { + states[index].eigenvalue = eigenvalues[index]; + states[index].orbital = {{1.0, 0.25}}; + states[index].residual = {{0.0, -0.125}}; + } + return states; +} +} // namespace + +TEST(SternheimerExcitationGuard, AcceptsUnsortedPositiveSpectrumWithoutMutation) +{ + const std::vector source = {-0.4, -0.8, -0.6}; + const auto target = excitation_test_states({0.3, -0.2, 0.1}); + const auto source_before = source; + const auto target_before = target; + EXPECT_DOUBLE_EQ(ModuleRI::require_positive_periodic_delta_excitations(target, source, "positive"), + -0.2 - (-0.4)); + EXPECT_EQ(source, source_before); + ASSERT_EQ(target.size(), target_before.size()); + for (std::size_t index = 0; index != target.size(); ++index) + { + EXPECT_EQ(target[index].eigenvalue, target_before[index].eigenvalue); + EXPECT_EQ(target[index].orbital, target_before[index].orbital); + EXPECT_EQ(target[index].residual, target_before[index].residual); + } +} + +TEST(SternheimerExcitationGuard, AcceptsSmallPositiveGapWithoutFloor) +{ + EXPECT_DOUBLE_EQ(ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({1.0e-14}), {0.0}, "small_positive"), + 1.0e-14); +} + +TEST(SternheimerExcitationGuard, RejectsZeroGap) +{ + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({0.3, -0.4}), {-0.8, -0.4}, "zero"), + ModuleRI::SternheimerExcitationError); + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({-0.0}), {0.0}, "negative_zero"), + ModuleRI::SternheimerExcitationError); +} + +TEST(SternheimerExcitationGuard, RejectsNegativeGap) +{ + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({0.3, -0.45}), {-0.8, -0.4}, "negative"), + ModuleRI::SternheimerExcitationError); +} + +TEST(SternheimerExcitationGuard, UsesCompleteSourceMaximumNotFirstBand) +{ + // The first source band alone would give a positive gap of 0.2 Ry. + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({-0.6, 0.3}), {-0.8, -0.4}, "complete_source"), + ModuleRI::SternheimerExcitationError); +} + +TEST(SternheimerExcitationGuard, UsesSuppliedSourceReferenceNotTargetOccupiedReference) +{ + const auto target = excitation_test_states({-0.5, 0.3}); + const std::vector target_occupied = {-0.7}; + EXPECT_DOUBLE_EQ(ModuleRI::require_positive_periodic_delta_excitations( + target, target_occupied, "target_reference_control"), + -0.5 - (-0.7)); + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations( + target, {-0.4}, "actual_source_reference"), + ModuleRI::SternheimerExcitationError); +} + +TEST(SternheimerExcitationGuard, RejectsEmptySpectra) +{ + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations({}, {-0.4}, "empty_target"), + ModuleRI::SternheimerExcitationError); + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({0.2}), {}, "empty_source"), + ModuleRI::SternheimerExcitationError); + EXPECT_THROW(ModuleRI::require_positive_periodic_delta_excitations({}, {}, "both_empty"), + ModuleRI::SternheimerExcitationError); +} + +TEST(SternheimerExcitationGuard, RejectsEveryNonfiniteSourcePosition) +{ + const double invalid_values[] = {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + for (const double invalid: invalid_values) + { + for (std::size_t index = 0; index != 3; ++index) + { + std::vector source = {-0.8, -0.4, -0.6}; + source[index] = invalid; + try + { + ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({0.3, -0.2}), source, "nonfinite_source_fixture"); + FAIL() << "Accepted nonfinite source energy at " << index; + } + catch (const ModuleRI::SternheimerExcitationError& error) + { + const std::string message = error.what(); + EXPECT_NE(message.find("reason=nonfinite_source"), std::string::npos); + EXPECT_NE(message.find("source_index=" + std::to_string(index + 1)), std::string::npos); + EXPECT_NE(message.find("context={nonfinite_source_fixture}"), std::string::npos); + } + } + } +} + +TEST(SternheimerExcitationGuard, RejectsEveryNonfiniteTargetPosition) +{ + const double invalid_values[] = {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + for (const double invalid: invalid_values) + { + for (std::size_t index = 0; index != 3; ++index) + { + std::vector target = {0.3, -0.2, 0.1}; + target[index] = invalid; + try + { + ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states(target), {-0.8, -0.4}, "nonfinite_target_fixture"); + FAIL() << "Accepted nonfinite target energy at " << index; + } + catch (const ModuleRI::SternheimerExcitationError& error) + { + const std::string message = error.what(); + EXPECT_NE(message.find("reason=nonfinite_target"), std::string::npos); + EXPECT_NE(message.find("target_index=" + std::to_string(index + 1)), std::string::npos); + EXPECT_NE(message.find("context={nonfinite_target_fixture}"), std::string::npos); + } + } + } +} + +TEST(SternheimerExcitationGuard, RejectsOverflowFromFiniteExtrema) +{ + for (const double source: {std::numeric_limits::max(), -std::numeric_limits::max()}) + { + try + { + ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({-source}), {source}, "overflow_fixture"); + FAIL() << "Accepted nonfinite gap from finite extrema"; + } + catch (const ModuleRI::SternheimerExcitationError& error) + { + EXPECT_NE(std::string(error.what()).find("reason=nonfinite_gap"), std::string::npos); + } + } +} + +TEST(SternheimerExcitationGuard, ReportsFullPrecisionBoundsIndicesAndContext) +{ + const double source_max = -0.40123456789012345; + const double target_min = -0.44634567890123456; + const std::string context = "grid=24,24,48 pbe_grid=72,72,288 q=2 source_k=1 target_k=2 rank=3"; + try + { + ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({0.3, target_min, -0.42}), {-0.7, source_max, -0.5}, context); + FAIL() << "Accepted inverted spectrum"; + } + catch (const ModuleRI::SternheimerExcitationError& error) + { + const std::runtime_error& base_error = error; + const std::string message = base_error.what(); + EXPECT_NE(message.find("context={" + context + "}"), std::string::npos); + EXPECT_NE(message.find("units=Ry index_base=1"), std::string::npos); + EXPECT_NE(message.find("source_count=3 target_count=3"), std::string::npos); + EXPECT_NE(message.find("source_max_index=2"), std::string::npos); + EXPECT_NE(message.find("target_min_index=2"), std::string::npos); + EXPECT_NE(message.find("reason=nonpositive_gap"), std::string::npos); + const std::string fields[] = {"source_max_Ry=", "target_min_Ry=", "min_excitation_Ry="}; + const double expected[] = {source_max, target_min, target_min - source_max}; + for (std::size_t index = 0; index != 3; ++index) + { + const auto position = message.find(fields[index]); + ASSERT_NE(position, std::string::npos); + EXPECT_EQ(std::stod(message.substr(position + fields[index].size())), expected[index]); + } + } +} + +TEST(SternheimerExcitationGuard, ReportsFirstIndexForTiedExtrema) +{ + try + { + ModuleRI::require_positive_periodic_delta_excitations( + excitation_test_states({-0.5, -0.5}), {-0.4, -0.4}, "ties"); + FAIL() << "Accepted inverted spectrum"; + } + catch (const ModuleRI::SternheimerExcitationError& error) + { + const std::string message = error.what(); + EXPECT_NE(message.find("source_max_index=1"), std::string::npos); + EXPECT_NE(message.find("target_min_index=1"), std::string::npos); + } +} + +TEST(SternheimerPeriodicSolver, SharedProjectorReservationIncludesMetadataAndRejectsOverflow) +{ + using Projectors = ModuleRI::SternheimerPeriodicResponseProjectors; + using Vector = ModuleRI::SternheimerFDHamiltonian::Vector; + EXPECT_EQ(Projectors::estimated_storage_bytes(10, 2, 3), + 7u * (11u * sizeof(ModuleRI::SternheimerFDHamiltonian::Complex) + sizeof(const Vector*))); + EXPECT_EQ(Projectors::estimated_storage_bytes(10, 2, 3, false), + 7u * (sizeof(ModuleRI::SternheimerFDHamiltonian::Complex) + sizeof(const Vector*))); + EXPECT_THROW(Projectors::estimated_storage_bytes(std::numeric_limits::max(), 2, 3), + std::overflow_error); + EXPECT_THROW(Projectors::estimated_storage_bytes(10, std::numeric_limits::max(), 3), + std::overflow_error); +} + +TEST(SternheimerPeriodicSolver, StandardModeSolvesInFullGridComplement) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + + Hamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + constexpr double volume_element = 0.5; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.25)); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 4, volume_element); + const std::vector occupied = {states.wavefunctions[0]}; + Hamiltonian::Vector rhs = states.wavefunctions[3]; + for (std::size_t ir = 0; ir != rhs.size(); ++ir) + { + rhs[ir] += Complex(0.25, -0.125) * states.wavefunctions[0][ir]; + } + constexpr double omega = 0.7; + + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 50; + options.residual_tol = 1.0e-12; + const auto response = ModuleRI::solve_sternheimer_periodic_linear_response(false, + hamiltonian, + occupied, + states.eigenvalues[0], + rhs, + {}, + {}, + omega, + volume_element, + options); + + ASSERT_TRUE(response.solver.converged); + EXPECT_FALSE(response.has_delta_components); + EXPECT_LT(response.residual_norm, 1.0e-10); + EXPECT_NEAR(response.full_grid_equation_residual_norm, response.residual_norm, 1.0e-14); + ASSERT_EQ(response.projected_rhs.size(), states.wavefunctions[3].size()); + const Complex factor = 1.0 / Complex(states.eigenvalues[3] - states.eigenvalues[0], omega); + ASSERT_EQ(response.wavefunction.size(), rhs.size()); + for (std::size_t ir = 0; ir != rhs.size(); ++ir) + { + EXPECT_NEAR(response.projected_rhs[ir].real(), states.wavefunctions[3][ir].real(), 1.0e-10); + EXPECT_NEAR(response.projected_rhs[ir].imag(), states.wavefunctions[3][ir].imag(), 1.0e-10); + const Complex expected = factor * states.wavefunctions[3][ir]; + EXPECT_NEAR(response.wavefunction[ir].real(), expected.real(), 1.0e-10); + EXPECT_NEAR(response.wavefunction[ir].imag(), expected.imag(), 1.0e-10); + } +} + +TEST(SternheimerPeriodicSolver, StandardModeMatchesCompleteSameGridSOSWavefunctionAndResponse) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + using Vector = Hamiltonian::Vector; + + Hamiltonian::Grid grid{6, 1, 1, 0.45, 1.0, 1.0, true}; + grid.kpoint = {0.17, 0.0, 0.0}; + constexpr double volume_element = 0.45; + Hamiltonian hamiltonian(grid, {0.10, -0.07, 0.16, -0.11, 0.04, 0.13}); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 6, volume_element); + constexpr int occupied_count = 2; + constexpr int occupied_index = 1; + const std::vector occupied(states.wavefunctions.begin(), + states.wavefunctions.begin() + occupied_count); + const std::vector perturbations = { + {Complex(0.70, 0.11), Complex(-0.30, 0.05), Complex(0.21, -0.08), + Complex(0.48, 0.03), Complex(-0.37, 0.09), Complex(0.14, -0.06)}, + {Complex(-0.22, 0.07), Complex(0.51, -0.04), Complex(0.33, 0.12), + Complex(-0.41, 0.02), Complex(0.19, -0.10), Complex(0.62, 0.08)}}; + constexpr double omega = 0.63; + + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 80; + options.residual_tol = 1.0e-12; + std::vector standard_branch(perturbations.size() * perturbations.size(), Complex(0.0, 0.0)); + std::vector sos_branch = standard_branch; + for (int column = 0; column != static_cast(perturbations.size()); ++column) + { + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation( + perturbations[static_cast(column)], states.wavefunctions[occupied_index], rhs); + const auto standard = ModuleRI::solve_sternheimer_periodic_linear_response( + false, + hamiltonian, + occupied, + states.eigenvalues[occupied_index], + rhs, + {}, + {}, + omega, + volume_element, + options); + ASSERT_TRUE(standard.solver.converged); + const Vector sos = ModuleRI::build_sternheimer_fd_complete_sos_response(states, + occupied_count, + occupied_index, + rhs, + omega, + volume_element); + ASSERT_EQ(standard.wavefunction.size(), sos.size()); + for (std::size_t ir = 0; ir != sos.size(); ++ir) + { + EXPECT_NEAR(standard.wavefunction[ir].real(), sos[ir].real(), 1.0e-10); + EXPECT_NEAR(standard.wavefunction[ir].imag(), sos[ir].imag(), 1.0e-10); + } + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(perturbations, + states.wavefunctions[occupied_index], + standard.wavefunction, + volume_element, + 1.0, + column, + standard_branch); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(perturbations, + states.wavefunctions[occupied_index], + sos, + volume_element, + 1.0, + column, + sos_branch); + } + + const auto standard_m + = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(standard_branch, perturbations.size()); + const auto sos_m + = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(sos_branch, perturbations.size()); + for (std::size_t index = 0; index != standard_branch.size(); ++index) + { + EXPECT_NEAR(standard_branch[index].real(), sos_branch[index].real(), 1.0e-10); + EXPECT_NEAR(standard_branch[index].imag(), sos_branch[index].imag(), 1.0e-10); + EXPECT_NEAR(standard_m[index].real(), sos_m[index].real(), 1.0e-10); + EXPECT_NEAR(standard_m[index].imag(), sos_m[index].imag(), 1.0e-10); + } +} + +TEST(SternheimerPeriodicSolver, SpectralPreconditionerPreservesResponseAndReducesIterations) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + using Vector = Hamiltonian::Vector; + + Hamiltonian::Grid grid{48, 1, 1, 0.35, 1.0, 1.0, true}; + const double pi = std::acos(-1.0); + std::vector potential(static_cast(grid.size())); + for (int ix = 0; ix != grid.nx; ++ix) + { + potential[static_cast(ix)] + = 0.18 * std::cos(2.0 * pi * ix / grid.nx) + + 0.07 * std::sin(6.0 * pi * ix / grid.nx); + } + constexpr double volume_element = 0.35; + Hamiltonian hamiltonian(grid, potential, 1.0, nullptr, 8); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 48, volume_element); + const std::vector occupied = {states.wavefunctions[0]}; + Vector rhs(static_cast(grid.size())); + for (int ix = 0; ix != grid.nx; ++ix) + { + rhs[static_cast(ix)] + = Complex(std::cos(10.0 * pi * ix / grid.nx), + 0.3 * std::sin(14.0 * pi * ix / grid.nx)); + } + + ModuleRI::SternheimerRPA::SolverOptions identity_options; + identity_options.max_iter = 150; + identity_options.residual_tol = 1.0e-11; + identity_options.use_fd_spectral_preconditioner = false; + const auto identity = ModuleRI::solve_sternheimer_periodic_linear_response(false, + hamiltonian, + occupied, + states.eigenvalues[0], + rhs, + {}, + {}, + 0.08, + volume_element, + identity_options); + + ModuleRI::SternheimerRPA::SolverOptions preconditioned_options = identity_options; + preconditioned_options.use_fd_spectral_preconditioner = true; + preconditioned_options.fd_spectral_preconditioner_regularization = 0.2; + const auto preconditioned = ModuleRI::solve_sternheimer_periodic_linear_response(false, + hamiltonian, + occupied, + states.eigenvalues[0], + rhs, + {}, + {}, + 0.08, + volume_element, + preconditioned_options); + + ASSERT_TRUE(identity.solver.converged); + ASSERT_TRUE(preconditioned.solver.converged); + EXPECT_LT(preconditioned.solver.iterations, identity.solver.iterations); + EXPECT_LT(preconditioned.residual_norm, 1.0e-9); + ASSERT_EQ(preconditioned.wavefunction.size(), identity.wavefunction.size()); + for (std::size_t ir = 0; ir != identity.wavefunction.size(); ++ir) + { + EXPECT_NEAR(preconditioned.wavefunction[ir].real(), identity.wavefunction[ir].real(), 1.0e-8); + EXPECT_NEAR(preconditioned.wavefunction[ir].imag(), identity.wavefunction[ir].imag(), 1.0e-8); + } +} + +TEST(SternheimerPeriodicSolver, DeltaModeReportsHybridAndFullGridResidualsSeparately) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + + Hamiltonian::Grid grid{4, 1, 1, 0.5, 1.0, 1.0, true}; + constexpr double volume_element = 0.5; + Hamiltonian hamiltonian(grid, std::vector(grid.size(), 0.25)); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 4, volume_element); + const std::vector occupied = {states.wavefunctions[0]}; + const Hamiltonian::Vector& virtual_orbital = states.wavefunctions[2]; + Hamiltonian::Vector rhs = virtual_orbital; + for (std::size_t ir = 0; ir != rhs.size(); ++ir) + { + rhs[ir] += Complex(0.25, 0.0) * states.wavefunctions[3][ir]; + } + constexpr double omega = 0.7; + constexpr double diagonal_shift = 0.3; + + ModuleRI::SternheimerDeltaVirtualState virtual_state; + virtual_state.orbital = virtual_orbital; + virtual_state.residual.assign(virtual_orbital.size(), Complex(0.0, 0.0)); + virtual_state.eigenvalue = states.eigenvalues[2] + diagonal_shift; + + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 50; + options.residual_tol = 1.0e-12; + const auto response = ModuleRI::solve_sternheimer_periodic_linear_response(true, + hamiltonian, + occupied, + states.eigenvalues[0], + rhs, + {virtual_state}, + {Complex(-1.0, 0.0)}, + omega, + volume_element, + options); + + ASSERT_TRUE(response.solver.converged); + EXPECT_TRUE(response.has_delta_components); + EXPECT_LT(response.residual_norm, 1.0e-10); + EXPECT_GT(response.full_grid_equation_residual_norm, 1.0e-2); +} + +TEST(SternheimerPeriodicSolver, DeltaBatchMatchesIndependentChannels) +{ + using Hamiltonian = ModuleRI::SternheimerFDHamiltonian; + using Complex = Hamiltonian::Complex; + using Vector = Hamiltonian::Vector; + + Hamiltonian::Grid grid{6, 1, 1, 0.45, 1.0, 1.0, true}; + grid.kpoint = {0.13, 0.0, 0.0}; + constexpr double volume_element = 0.45; + Hamiltonian hamiltonian(grid, {0.11, -0.05, 0.16, -0.09, 0.03, 0.14}, 1.0, nullptr, 8); + const auto states = ModuleRI::solve_sternheimer_fd_zero_order_dense(hamiltonian, 6, volume_element); + const std::vector occupied = {states.wavefunctions[0]}; + std::vector virtual_states; + for (int index = 1; index != 3; ++index) + { + ModuleRI::SternheimerDeltaVirtualState state; + state.orbital = states.wavefunctions[static_cast(index)]; + state.residual = states.wavefunctions[4]; + for (Complex& value: state.residual) + { + value *= Complex(0.01 * index, -0.005 * index); + } + state.eigenvalue = states.eigenvalues[static_cast(index)] + 0.02 * index; + virtual_states.push_back(std::move(state)); + } + const Hamiltonian::Matrix rhs = { + states.wavefunctions[3], + states.wavefunctions[5]}; + Hamiltonian::Matrix coupled_rhs = rhs; + for (std::size_t column = 0; column != coupled_rhs.size(); ++column) + { + for (std::size_t ir = 0; ir != coupled_rhs[column].size(); ++ir) + { + coupled_rhs[column][ir] += Complex(0.3, 0.2 * (column + 1)) * states.wavefunctions[1][ir] + + Complex(-0.2, 0.15) * states.wavefunctions[2][ir] + + Complex(0.4, -0.1) * states.wavefunctions[4][ir]; + } + } + std::vector> perturbation_matrix_elements(rhs.size()); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + for (const auto& state: virtual_states) + { + perturbation_matrix_elements[column].push_back( + -ModuleRI::sternheimer_fd_grid_dot(state.orbital, coupled_rhs[column], volume_element)); + } + } + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 100; + options.residual_tol = 1.0e-12; + options.use_fd_spectral_preconditioner = true; + constexpr double omega = 0.57; + + std::vector scalar; + for (std::size_t column = 0; column != rhs.size(); ++column) + { + scalar.push_back(ModuleRI::solve_sternheimer_periodic_linear_response(true, + hamiltonian, + occupied, + states.eigenvalues[0], + coupled_rhs[column], + virtual_states, + perturbation_matrix_elements[column], + omega, + volume_element, + options)); + } + const auto batch = ModuleRI::solve_sternheimer_periodic_linear_response_batch( + true, + hamiltonian, + occupied, + states.eigenvalues[0], + coupled_rhs, + virtual_states, + perturbation_matrix_elements, + omega, + volume_element, + options); + + const ModuleRI::SternheimerPeriodicResponseProjectors projectors( + occupied, virtual_states, volume_element); + const ModuleRI::SternheimerPeriodicResponseProjectors scalar_projectors( + occupied, virtual_states, volume_element, false); + EXPECT_LT(scalar_projectors.fixed.storage_bytes(), projectors.fixed.storage_bytes()); + const auto shared_batch = ModuleRI::solve_sternheimer_periodic_linear_response_batch( + true, hamiltonian, occupied, states.eigenvalues[0], coupled_rhs, virtual_states, + perturbation_matrix_elements, omega, volume_element, options, &projectors); + const auto owning_subspace = ModuleRI::build_delta_sternheimer_fixed_subspace(occupied, virtual_states); + const auto owning_batch = ModuleRI::solve_delta_sternheimer_linear_response_batch( + hamiltonian, owning_subspace, states.eigenvalues[0], coupled_rhs, virtual_states, + perturbation_matrix_elements, omega, volume_element, options); + ASSERT_EQ(shared_batch.size(), batch.size()); + + ASSERT_EQ(batch.size(), scalar.size()); + for (std::size_t column = 0; column != scalar.size(); ++column) + { + const auto shared_scalar = ModuleRI::solve_sternheimer_periodic_linear_response( + true, hamiltonian, occupied, states.eigenvalues[0], coupled_rhs[column], virtual_states, + perturbation_matrix_elements[column], omega, volume_element, options, &scalar_projectors); + EXPECT_EQ(shared_batch[column].solver.converged, owning_batch[column].solver.converged); + EXPECT_EQ(shared_batch[column].solver.iterations, owning_batch[column].solver.iterations); + EXPECT_NEAR(shared_batch[column].residual_norm, owning_batch[column].residual_norm, 1.0e-12); + EXPECT_NEAR(shared_scalar.residual_norm, scalar[column].residual_norm, 1.0e-12); + EXPECT_GT(std::abs(shared_batch[column].delta_components.sos_coefficients[0]), 1.0e-4); + EXPECT_GT(std::abs(shared_batch[column].delta_components.pulay_coefficients[0]), 1.0e-6); + for (std::size_t state = 0; state != virtual_states.size(); ++state) + { + EXPECT_NEAR(std::abs(shared_batch[column].delta_components.coefficients[state] + - owning_batch[column].response.coefficients[state]), 0.0, 1.0e-12); + } + EXPECT_EQ(batch[column].solver.converged, scalar[column].solver.converged); + EXPECT_EQ(batch[column].solver.iterations, scalar[column].solver.iterations); + EXPECT_NEAR(batch[column].solver.relative_residual, scalar[column].solver.relative_residual, 1.0e-12); + EXPECT_NEAR(batch[column].residual_norm, scalar[column].residual_norm, 1.0e-11); + EXPECT_NEAR(batch[column].full_grid_equation_residual_norm, + scalar[column].full_grid_equation_residual_norm, + 1.0e-11); + ASSERT_EQ(batch[column].wavefunction.size(), scalar[column].wavefunction.size()); + for (std::size_t ir = 0; ir != scalar[column].wavefunction.size(); ++ir) + { + EXPECT_NEAR(std::abs(shared_batch[column].wavefunction[ir] + - owning_batch[column].response.reconstructed_wavefunction[ir]), 0.0, 1.0e-12); + EXPECT_NEAR(std::abs(shared_scalar.wavefunction[ir] - scalar[column].wavefunction[ir]), 0.0, 1.0e-12); + EXPECT_NEAR(batch[column].projected_rhs[ir].real(), scalar[column].projected_rhs[ir].real(), 1.0e-12); + EXPECT_NEAR(batch[column].projected_rhs[ir].imag(), scalar[column].projected_rhs[ir].imag(), 1.0e-12); + EXPECT_NEAR(batch[column].wavefunction[ir].real(), scalar[column].wavefunction[ir].real(), 1.0e-11); + EXPECT_NEAR(batch[column].wavefunction[ir].imag(), scalar[column].wavefunction[ir].imag(), 1.0e-11); + } + } +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_projector_locality_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_projector_locality_test.cpp new file mode 100644 index 00000000000..ae1ee0cfe44 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_projector_locality_test.cpp @@ -0,0 +1,60 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" +#include +#include + +TEST(SternheimerProjectorLocality, LargeComplexBorrowedViewsMatchExplicitProjection) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + using Vector = Projector::Vector; + using Complex = Projector::Complex; + constexpr int grid = 257; + constexpr double weight = 0.125; + const double pi = std::acos(-1.0); + for (const int count : {20, 196, 216}) + { + std::vector basis(count, Vector(grid)); + for (int b = 0; b < count-1; ++b) + for (int g = 0; g < grid; ++g) + { + const double phase = 2.0*pi*(b+1)*g/grid; + basis[b][g] = Complex(std::cos(phase), std::sin(phase)); + } + std::vector views; + for (const auto& vector : basis) views.push_back(&vector); + const Projector projector(views, weight, true); + const auto retained = projector.storage_bytes(); + for (const int width : {1, 2, 3}) + { + std::vector input(width, Vector(grid)); + for (int c = 0; c < width; ++c) + for (int g = 0; g < grid; ++g) + input[c][g] = Complex(std::sin(0.07*(g+c)), std::cos(0.11*(g+c))); + auto expected = input; + for (int c = 0; c < width; ++c) + { + std::vector coefficient(count, Complex(0.0, 0.0)); + for (int b = 0; b < count; ++b) + { + Complex norm(0.0, 0.0); + for (int g = 0; g < grid; ++g) + { + norm += std::conj(basis[b][g])*basis[b][g]; + coefficient[b] += std::conj(basis[b][g])*input[c][g]; + } + coefficient[b] = std::abs(norm) == 0.0 ? Complex(0.0, 0.0) : coefficient[b]/norm; + } + for (int g = 0; g < grid; ++g) + { + Complex correction(0.0, 0.0); + for (int b = 0; b < count; ++b) correction += coefficient[b]*basis[b][g]; + expected[c][g] -= correction; + } + } + projector.project_batch(input); + EXPECT_EQ(projector.storage_bytes(), retained); + for (int c = 0; c < width; ++c) + for (int g = 0; g < grid; ++g) + EXPECT_NEAR(std::abs(input[c][g]-expected[c][g]), 0.0, 2.0e-13); + } + } +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_representative_ownership_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_representative_ownership_test.cpp new file mode 100644 index 00000000000..27577c76b8f --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_representative_ownership_test.cpp @@ -0,0 +1,303 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" + +#include +#include +#include +#include + +namespace +{ +using ScheduleUnderTest = ModuleRI::SternheimerKPointSchedule; + +ModuleRI::SternheimerPeriodicResponsePlan make_plan(const int iq) +{ + std::vector records(16); + for (int ik = 0; ik != 16; ++ik) + { + auto& record = records[ik]; + record.local_k_index = record.global_k_index = record.zero_order_k_index = ik; + record.spin_index = 0; + record.kpoint = {static_cast(ik / 4) / 4.0, static_cast(ik % 4) / 4.0, 0.0}; + record.kweight = 1.0 / 16.0; + record.occupations = {1.0}; + } + return ModuleRI::build_sternheimer_periodic_response_plan(records, iq); +} + +std::vector tr_representatives() +{ + std::vector identity(16), reversal(16); + std::iota(identity.begin(), identity.end(), 0); + for (int ik = 0; ik != 16; ++ik) + { + reversal[ik] = ((4 - ik / 4) % 4) * 4 + (4 - ik % 4) % 4; + } + const auto orbits = ModuleRI::build_sternheimer_fixed_q_k_orbits_from_permutations( + 16, {identity, reversal}); + std::vector active(16, false); + for (const auto& orbit: orbits) + { + active[orbit.representative_ik_full] = true; + } + return active; +} +} // namespace + +TEST(SternheimerRepresentativeOwnership, BalancesTenRepresentativesAcrossFiveGroups) +{ + RecordProperty("tr_q_indices", "1,3,9,11"); + RecordProperty("representative_count", 10); + RecordProperty("group_count", 5); + const auto active = tr_representatives(); + std::vector representatives; + for (int ik = 0; ik != 16; ++ik) + { + if (active[ik]) representatives.push_back(ik + 1); + } + EXPECT_EQ(representatives, (std::vector{1, 2, 3, 5, 6, 7, 8, 9, 10, 11})); + const ScheduleUnderTest schedule(active, 5); + for (const int iq: {1, 3, 9, 11}) + { + auto plan = make_plan(iq); + std::reverse(plan.kq_pairs.begin(), plan.kq_pairs.end()); + const auto original = plan; + std::vector seen(16, 0); + for (int group = 0; group != 5; ++group) + { + SCOPED_TRACE(::testing::Message() << "q=" << iq << " group=" << group); + const auto owned = schedule.owned_pair_indices(plan, group); + EXPECT_EQ(owned.size(), 2U); + for (const auto pair_index: owned) + { + ASSERT_LT(pair_index, plan.kq_pairs.size()); + const auto& pair = plan.kq_pairs[pair_index]; + EXPECT_TRUE(active[pair.source_index]); + EXPECT_EQ(schedule.owner_group(pair.source_index), group); + EXPECT_EQ(pair.source_index, original.kq_pairs[pair_index].source_index); + EXPECT_EQ(pair.target_index, original.kq_pairs[pair_index].target_index); + EXPECT_EQ(pair.reciprocal_shift, original.kq_pairs[pair_index].reciprocal_shift); + ++seen[pair.source_index]; + } + } + for (int ik = 0; ik != 16; ++ik) EXPECT_EQ(seen[ik], active[ik] ? 1 : 0); + EXPECT_EQ(plan.iq, iq); + EXPECT_EQ(plan.kweight_sum, original.kweight_sum); + } +} + +TEST(SternheimerRepresentativeOwnership, OwnsEveryPairFrequencyReplicaOnceWithTwoPairsPerRank) +{ + RecordProperty("base_nproc", 30); + RecordProperty("nfreq", 6); + RecordProperty("pairs_per_rank", 2); + RecordProperty("unique_fragments_per_q", 60); + RecordProperty("replica_counts_tested", "1,2,3"); + const auto active = tr_representatives(); + const ScheduleUnderTest schedule(active, 5); + constexpr int nfreq = 6; + for (const int iq: {1, 3, 9, 11}) + { + const auto plan = make_plan(iq); + for (const int replicas: {1, 2, 3}) + { + for (const int shift: {-7, 0, 1, 7}) + { + const int ranks = 5 * nfreq * replicas; + std::vector seen(16 * nfreq * replicas, 0), writers(16 * nfreq, 0); + for (int rank = 0; rank != ranks; ++rank) + { + const auto layout = ModuleRI::sternheimer_nested_mpi_replica_layout( + 5, nfreq, ranks, rank, replicas > 1); + const int group = layout.local_response_slot / nfreq; + const auto owned = schedule.owned_pair_indices(plan, group); + int rank_pairs = 0; + for (const auto pair_index: owned) + { + const int source = plan.kq_pairs[pair_index].source_index; + for (int frequency = 0; frequency != nfreq; ++frequency) + { + const auto assignment = schedule.assignment(source, frequency, nfreq, 5 * nfreq, shift); + const int leader = assignment.owner_rank * replicas; + if (rank < leader || rank >= leader + replicas) continue; + EXPECT_EQ(assignment.kpoint_group, group); + EXPECT_EQ(assignment.frequency_slot, layout.local_response_slot % nfreq); + ++seen[(source * nfreq + frequency) * replicas + layout.local_replica]; + if (layout.local_replica == 0) ++writers[source * nfreq + frequency]; + ++rank_pairs; + } + } + EXPECT_EQ(rank_pairs, 2) << "q=" << iq << " rank=" << rank + << " replicas=" << replicas << " shift=" << shift; + } + for (int source = 0; source != 16; ++source) + { + for (int frequency = 0; frequency != nfreq; ++frequency) + { + EXPECT_EQ(writers[source * nfreq + frequency], active[source] ? 1 : 0); + for (int replica = 0; replica != replicas; ++replica) + EXPECT_EQ(seen[(source * nfreq + frequency) * replicas + replica], active[source] ? 1 : 0); + } + } + } + } + } +} + +TEST(SternheimerRepresentativeOwnership, PreservesFullGridPairAndFrequencyOwners) +{ + RecordProperty("full_k_count", 16); + RecordProperty("group_count", 8); + RecordProperty("nontr_mapping", "unchanged"); + const ScheduleUnderTest schedule(std::vector(16, true), 8); + for (const int iq: {5, 6, 7, 8, 10}) + { + const auto plan = make_plan(iq); + for (int group = 0; group != 8; ++group) + { + const auto owned = schedule.owned_pair_indices(plan, group); + EXPECT_EQ(owned, ModuleRI::sternheimer_owned_kq_pair_indices(plan, group, 8)); + ASSERT_EQ(owned.size(), 2U); + for (const auto pair_index: owned) + { + const int source = plan.kq_pairs[pair_index].source_index; + EXPECT_EQ(schedule.owner_group(source), ModuleRI::sternheimer_kpoint_owner_group(source, 16, 8)); + for (int frequency = 0; frequency != 6; ++frequency) + { + for (const int shift: {-7, 0, 1, 7}) + { + const auto actual = schedule.assignment(source, frequency, 6, 48, shift); + const auto original = ModuleRI::sternheimer_nested_mpi_assignment(source, 16, frequency, 6, 8, 48, shift); + EXPECT_EQ(actual.kpoint_group, original.kpoint_group); + EXPECT_EQ(actual.frequency_slot, original.frequency_slot); + EXPECT_EQ(actual.owner_rank, original.owner_rank); + } + } + } + } + } +} + +TEST(SternheimerRepresentativeOwnership, RejectsIdleGroupsAndInactiveOwners) +{ + const auto active = tr_representatives(); + EXPECT_THROW(ScheduleUnderTest(active, 0), std::invalid_argument); + EXPECT_THROW(ScheduleUnderTest(active, -1), std::invalid_argument); + EXPECT_THROW(ScheduleUnderTest(active, 11), std::invalid_argument); + EXPECT_THROW(ScheduleUnderTest(std::vector(16, false), 1), std::invalid_argument); + EXPECT_THROW(ScheduleUnderTest(std::vector(), 1), std::invalid_argument); + const ScheduleUnderTest schedule(active, 5); + EXPECT_THROW(schedule.owner_group(-1), std::invalid_argument); + EXPECT_THROW(schedule.owner_group(16), std::invalid_argument); + EXPECT_THROW(schedule.owner_group(3), std::invalid_argument); + EXPECT_THROW(schedule.assignment(3, 0, 6, 30), std::invalid_argument); + EXPECT_THROW(schedule.assignment(0, 0, 6, 48), std::invalid_argument); + EXPECT_THROW(schedule.assignment(0, 6, 6, 30), std::invalid_argument); + const auto plan = make_plan(3); + EXPECT_THROW(schedule.owned_pair_indices(plan, -1), std::invalid_argument); + EXPECT_THROW(schedule.owned_pair_indices(plan, 5), std::invalid_argument); + auto malformed = plan; + malformed.record_index_by_global_k.pop_back(); + EXPECT_THROW(schedule.owned_pair_indices(malformed, 0), std::invalid_argument); + malformed = plan; + malformed.kq_pairs.front().source_index = 16; + EXPECT_THROW(schedule.owned_pair_indices(malformed, 0), std::invalid_argument); +} + +TEST(SternheimerRepresentativeOwnership, CoversChannelBatchesExactlyOnceAcrossReplicas) +{ + const auto active = tr_representatives(); + const ScheduleUnderTest schedule(active, 5); + const auto plan = make_plan(3); + constexpr int nfreq = 6; + constexpr int occupied_states = 3; + constexpr int batches = 7; + for (const int replicas: {1, 2, 4}) + { + const int ranks = 30 * replicas; + std::vector seen(16 * nfreq * occupied_states * batches, 0); + for (int rank = 0; rank != ranks; ++rank) + { + const auto layout = ModuleRI::sternheimer_nested_mpi_replica_layout(5, nfreq, ranks, rank, true); + const auto owned = schedule.owned_pair_indices(plan, layout.local_response_slot / nfreq); + for (const auto pair_index: owned) + { + const int source = plan.kq_pairs[pair_index].source_index; + for (int frequency = 0; frequency != nfreq; ++frequency) + { + const auto assignment = schedule.assignment(source, frequency, nfreq, 30, 1); + if (layout.local_response_slot != assignment.owner_rank) continue; + for (int occupied = 0; occupied != occupied_states; ++occupied) + { + for (int batch = 0; batch != batches; ++batch) + { + if (ModuleRI::sternheimer_channel_batch_replica_owner(occupied, batch, batches, replicas) + != layout.local_replica) continue; + ++seen[((source * nfreq + frequency) * occupied_states + occupied) * batches + batch]; + } + } + } + } + } + for (int source = 0; source != 16; ++source) + { + for (int task = 0; task != nfreq * occupied_states * batches; ++task) + EXPECT_EQ(seen[source * nfreq * occupied_states * batches + task], active[source] ? 1 : 0); + } + } +} + +TEST(SternheimerRepresentativeOwnership, HandlesSparseNonzeroFirstRepresentativeAndSingleGroup) +{ + std::vector active(16, false); + for (const int source: {2, 7, 12}) active[source] = true; + const auto plan = make_plan(9); + const ScheduleUnderTest schedule(active, 3); + for (int group = 0; group != 3; ++group) + { + const auto owned = schedule.owned_pair_indices(plan, group); + ASSERT_EQ(owned.size(), 1U); + EXPECT_EQ(plan.kq_pairs[owned.front()].source_index, (std::vector{2, 7, 12})[group]); + EXPECT_EQ(schedule.assignment(plan.kq_pairs[owned.front()].source_index, 0, 6, 18).owner_rank, group * 6); + } + const ScheduleUnderTest single_group(active, 1); + EXPECT_EQ(single_group.owned_pair_indices(plan, 0).size(), 3U); + for (const int source: {2, 7, 12}) EXPECT_EQ(single_group.owner_group(source), 0); +} + +TEST(SternheimerRepresentativeOwnership, WritesSixtyUniqueFragmentsWithOriginalGlobalIndices) +{ + RecordProperty("output_index_space", "original_global_k"); + const auto active = tr_representatives(); + const ScheduleUnderTest schedule(active, 5); + for (const int iq: {1, 3, 9, 11}) + { + const auto plan = make_plan(iq); + std::set filenames; + for (int rank = 0; rank != 30; ++rank) + { + const auto owned = schedule.owned_pair_indices(plan, rank / 6); + for (const auto pair_index: owned) + { + const int source = plan.kq_pairs[pair_index].source_index; + for (int frequency = 0; frequency != 6; ++frequency) + { + if (schedule.assignment(source, frequency, 6, 30, 1).owner_rank != rank) continue; + const auto record = ModuleRI::make_sternheimer_partial_response_record( + iq, source, frequency + 1, {std::complex(1.0, 2.0)}, 1); + EXPECT_EQ(record.iq, iq); + EXPECT_EQ(record.ik_full, source); + EXPECT_EQ(record.ifrequency, frequency + 1); + EXPECT_EQ(record.matrix, (std::vector>{{2.0, 0.0}})); + EXPECT_TRUE(filenames.insert(record.filename).second); + } + } + } + EXPECT_EQ(filenames.size(), 60U); + for (int source = 0; source != 16; ++source) + { + for (int frequency = 1; frequency != 7; ++frequency) + EXPECT_EQ(filenames.count(ModuleRI::sternheimer_partial_response_filename(iq, source, frequency)), + active[source] ? 1U : 0U); + } + } +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_response_grid_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_response_grid_test.cpp new file mode 100644 index 00000000000..ee1340b94a8 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_response_grid_test.cpp @@ -0,0 +1,375 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_response_grid.h" + +#include "source_base/constants.h" +#include "source_base/matrix3.h" +#include "source_basis/module_pw/pw_basis.h" + +#include +#include +#include +#include +#include +#include + +#ifdef __MPI +#include +#endif + +namespace +{ + +std::unique_ptr make_basis(const int nx, const int ny, const int nz) +{ + auto basis = std::make_unique("cpu", "double"); +#ifdef __MPI + basis->initmpi(1, 0, MPI_COMM_SELF); +#endif + basis->initgrids(1.0, ModuleBase::Matrix3(8.0, 0.0, 0.0, 0.0, 8.0, 0.0, 0.0, 0.0, 8.0), nx, ny, nz); + basis->initparameters(false, 1.0e12); + basis->fft_bundle.initfftmode(0); + basis->setuptransform(); + basis->collect_local_pw(); + return basis; +} + +std::unique_ptr make_cutoff_basis(const double ecutwfc) +{ + auto basis = std::make_unique("cpu", "double"); +#ifdef __MPI + basis->initmpi(1, 0, MPI_COMM_SELF); +#endif + basis->initgrids(1.0, + ModuleBase::Matrix3(8.0, 0.0, 0.0, 0.0, 8.0, 0.0, 0.0, 0.0, 8.0), + 4.0 * ecutwfc); + basis->initparameters(false, 4.0 * ecutwfc); + basis->fft_bundle.initfftmode(0); + basis->setuptransform(); + basis->collect_local_pw(); + return basis; +} + +std::size_t index(const int ix, const int iy, const int iz, const int ny, const int nz) +{ + return static_cast((ix * ny + iy) * nz + iz); +} + +std::vector cosine_mode(const int nx, const int ny, const int nz, const int mx, const int my, const int mz) +{ + std::vector values(static_cast(nx) * ny * nz, 0.0); + for (int ix = 0; ix != nx; ++ix) + { + for (int iy = 0; iy != ny; ++iy) + { + for (int iz = 0; iz != nz; ++iz) + { + const double phase = ModuleBase::TWO_PI + * (static_cast(mx * ix) / nx + static_cast(my * iy) / ny + + static_cast(mz * iz) / nz); + values[index(ix, iy, iz, ny, nz)] = std::cos(phase); + } + } + } + return values; +} + +double l2_norm(const std::vector& values) +{ + double norm2 = 0.0; + for (const double value: values) + { + norm2 += value * value; + } + return std::sqrt(norm2); +} + +double relative_l2_error(const std::vector& actual, const std::vector& expected) +{ + EXPECT_EQ(actual.size(), expected.size()); + std::vector difference(actual.size(), 0.0); + for (std::size_t index = 0; index != actual.size(); ++index) + { + difference[index] = actual[index] - expected[index]; + } + return l2_norm(difference) / l2_norm(expected); +} + +} // namespace + +TEST(SternheimerResponseGrid, CutoffDecisionKeepsCompatibilityAndRejectsUpsampling) +{ + EXPECT_FALSE(ModuleRI::sternheimer_uses_independent_response_grid(0.0, 80.0)); + EXPECT_TRUE(ModuleRI::sternheimer_uses_independent_response_grid(30.0, 80.0)); + EXPECT_THROW(ModuleRI::sternheimer_uses_independent_response_grid(-1.0, 80.0), std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_uses_independent_response_grid(90.0, 80.0), std::invalid_argument); +} + +TEST(SternheimerResponseGrid, SameGridRestrictionReturnsInputExactly) +{ + const auto basis = make_basis(6, 6, 6); + std::vector input(static_cast(basis->nxyz), 0.0); + for (std::size_t ir = 0; ir != input.size(); ++ir) + { + input[ir] = 0.25 + static_cast(ir) / input.size(); + } + + const auto output = ModuleRI::restrict_sternheimer_real_field(*basis, *basis, input); + + EXPECT_EQ(output, input); +} + +TEST(SternheimerResponseGrid, EqualCutoffKeepsTheOriginalPBEGridPath) +{ + const auto pbe_basis = make_cutoff_basis(80.0); + + const auto response_grid = ModuleRI::make_sternheimer_response_grid(*pbe_basis, 80.0, 80.0, 0); + + EXPECT_FALSE(response_grid.independent); + EXPECT_EQ(response_grid.basis, pbe_basis.get()); + EXPECT_EQ(response_grid.serial_fine_basis, nullptr); + EXPECT_EQ(response_grid.serial_response_basis, nullptr); +} + +TEST(SternheimerResponseGrid, ReducedCutoffBuildsSerialFineAndResponseBases) +{ + const auto pbe_basis = make_cutoff_basis(80.0); + + const auto response_grid = ModuleRI::make_sternheimer_response_grid(*pbe_basis, 80.0, 30.0, 0); + + ASSERT_TRUE(response_grid.independent); + ASSERT_NE(response_grid.serial_fine_basis, nullptr); + ASSERT_NE(response_grid.serial_response_basis, nullptr); + EXPECT_EQ(response_grid.serial_fine_basis->nx, pbe_basis->nx); + EXPECT_EQ(response_grid.serial_fine_basis->ny, pbe_basis->ny); + EXPECT_EQ(response_grid.serial_fine_basis->nz, pbe_basis->nz); + EXPECT_LT(response_grid.serial_response_basis->nxyz, pbe_basis->nxyz); + EXPECT_EQ(response_grid.basis, response_grid.serial_response_basis.get()); +} + +TEST(SternheimerResponseGrid, ExplicitDimensionsBuildExactAnisotropicGrid) +{ + const auto pbe_basis = make_basis(18, 16, 20); + + const auto response_grid = ModuleRI::make_sternheimer_response_grid( + *pbe_basis, 80.0, 0.0, 0, std::array{12, 10, 14}, 8); + + ASSERT_TRUE(response_grid.independent); + ASSERT_NE(response_grid.serial_response_basis, nullptr); + EXPECT_EQ(response_grid.serial_response_basis->nx, 12); + EXPECT_EQ(response_grid.serial_response_basis->ny, 10); + EXPECT_EQ(response_grid.serial_response_basis->nz, 14); + EXPECT_EQ(response_grid.requested_dimensions, (std::array{12, 10, 14})); + EXPECT_STREQ(ModuleRI::sternheimer_response_grid_source_name(response_grid.source), "explicit"); +} + +TEST(SternheimerResponseGrid, ExplicitPbeDimensionsKeepOriginalPath) +{ + const auto pbe_basis = make_basis(18, 16, 20); + + const auto response_grid = ModuleRI::make_sternheimer_response_grid( + *pbe_basis, 80.0, 0.0, 0, std::array{18, 16, 20}, 8); + + EXPECT_FALSE(response_grid.independent); + EXPECT_EQ(response_grid.basis, pbe_basis.get()); + EXPECT_STREQ(ModuleRI::sternheimer_response_grid_source_name(response_grid.source), "pbe"); +} + +TEST(SternheimerResponseGrid, Molecular140Ry20AngstromSelects128WithoutChangingPbe288) +{ + ModulePW::PW_Basis pbe_basis("cpu", "double"); +#ifdef __MPI + pbe_basis.initmpi(1, 0, MPI_COMM_SELF); +#endif + const double length_bohr = 20.0 * ModuleBase::ANGSTROM_AU; + pbe_basis.initgrids(1.0, + ModuleBase::Matrix3(length_bohr, 0.0, 0.0, + 0.0, length_bohr, 0.0, + 0.0, 0.0, length_bohr), + 4.0 * 140.0); + const std::array pbe_dimensions = {pbe_basis.nx, pbe_basis.ny, pbe_basis.nz}; + ASSERT_EQ(pbe_dimensions, (std::array{288, 288, 288})); + const auto response = ModuleRI::make_sternheimer_response_grid( + pbe_basis, 140.0, 0.0, 0, std::array{128, 128, 128}, 8); + ASSERT_TRUE(response.independent); + ASSERT_NE(response.basis, &pbe_basis); + EXPECT_EQ(response.basis->nx, 128); + EXPECT_EQ(response.basis->ny, 128); + EXPECT_EQ(response.basis->nz, 128); + EXPECT_EQ(response.basis->nrxx, 128 * 128 * 128); + EXPECT_EQ(response.serial_fine_basis->nxyz, 288 * 288 * 288); + EXPECT_EQ((std::array{pbe_basis.nx, pbe_basis.ny, pbe_basis.nz}), pbe_dimensions); + EXPECT_STREQ(ModuleRI::sternheimer_response_grid_source_name(response.source), "explicit"); + RecordProperty("pbe_ecutwfc_Ry", 140); + RecordProperty("pbe_grid", "288 288 288"); + RecordProperty("response_grid", "128 128 128"); + RecordProperty("validation_scope", "native grid construction only; no molecular physics"); +} + +TEST(SternheimerResponseGrid, MolecularSpinFieldsRestrictIndependentlyWithoutChangingPbeFields) +{ + const auto pbe = make_basis(18, 16, 20); + const auto response = ModuleRI::make_sternheimer_response_grid( + *pbe, 80.0, 0.0, 0, std::array{12, 10, 14}, 8); + const auto low = cosine_mode(18, 16, 20, 1, 2, 1); + const auto high = cosine_mode(18, 16, 20, 7, 0, 0); + const auto expected_low = cosine_mode(12, 10, 14, 1, 2, 1); + for (int spin = 0; spin != 2; ++spin) + { + std::vector fine(low.size()); + std::vector expected(expected_low.size()); + for (std::size_t ir = 0; ir != fine.size(); ++ir) + { + fine[ir] = 2.0 + spin + (spin + 1.0) * low[ir] + 0.3 * high[ir]; + } + for (std::size_t ir = 0; ir != expected.size(); ++ir) + { + expected[ir] = 2.0 + spin + (spin + 1.0) * expected_low[ir]; + } + const auto original = fine; + const auto coarse = ModuleRI::restrict_sternheimer_real_field_rectangular( + *response.serial_fine_basis, *response.serial_response_basis, fine); + EXPECT_LT(relative_l2_error(coarse, expected), 1.0e-12); + EXPECT_EQ(fine, original); + } +} + +TEST(SternheimerResponseGrid, ExplicitDimensionsRejectAmbiguousOrUnsupportedSelections) +{ + const auto pbe_basis = make_basis(18, 16, 20); + + EXPECT_THROW(ModuleRI::make_sternheimer_response_grid( + *pbe_basis, 80.0, 0.0, 0, std::array{12, 0, 14}, 8), + std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_response_grid( + *pbe_basis, 80.0, 0.0, 0, std::array{12, -10, 14}, 8), + std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_response_grid( + *pbe_basis, 80.0, 30.0, 0, std::array{12, 10, 14}, 8), + std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_response_grid( + *pbe_basis, 80.0, 0.0, 0, std::array{20, 10, 14}, 8), + std::invalid_argument); + EXPECT_THROW(ModuleRI::make_sternheimer_response_grid( + *pbe_basis, 80.0, 0.0, 0, std::array{12, 10, 8}, 8), + std::invalid_argument); +} + +TEST(SternheimerResponseGrid, RestrictionPreservesConstant) +{ + const auto fine = make_basis(8, 8, 8); + const auto coarse = make_basis(4, 4, 4); + const std::vector input(static_cast(fine->nxyz), 3.25); + + const auto output = ModuleRI::restrict_sternheimer_real_field(*fine, *coarse, input); + + ASSERT_EQ(output.size(), static_cast(coarse->nxyz)); + for (const double value: output) + { + EXPECT_NEAR(value, 3.25, 1.0e-12); + } +} + +TEST(SternheimerResponseGrid, RestrictionPreservesRetainedLowGMode) +{ + const auto fine = make_basis(8, 8, 8); + const auto coarse = make_basis(4, 4, 4); + const auto input = cosine_mode(8, 8, 8, 1, 1, 0); + const auto expected = cosine_mode(4, 4, 4, 1, 1, 0); + + const auto output = ModuleRI::restrict_sternheimer_real_field(*fine, *coarse, input); + + EXPECT_LT(relative_l2_error(output, expected), 1.0e-11); +} + +TEST(SternheimerResponseGrid, RestrictionRemovesFineOnlyHighGMode) +{ + const auto fine = make_basis(8, 8, 8); + const auto coarse = make_basis(4, 4, 4); + const auto input = cosine_mode(8, 8, 8, 3, 0, 0); + + const auto output = ModuleRI::restrict_sternheimer_real_field(*fine, *coarse, input); + + EXPECT_LT(l2_norm(output), 1.0e-11 * l2_norm(input)); +} + +TEST(SternheimerResponseGrid, RestrictionDoesNotAliasRemovedModeIntoRetainedMode) +{ + const auto fine = make_basis(8, 8, 8); + const auto coarse = make_basis(4, 4, 4); + auto input = cosine_mode(8, 8, 8, 1, 0, 0); + const auto high_mode = cosine_mode(8, 8, 8, 3, 0, 0); + for (std::size_t ir = 0; ir != input.size(); ++ir) + { + input[ir] += 0.3 * high_mode[ir]; + } + const auto expected = cosine_mode(4, 4, 4, 1, 0, 0); + + const auto output = ModuleRI::restrict_sternheimer_real_field(*fine, *coarse, input); + + EXPECT_LT(relative_l2_error(output, expected), 1.0e-11); +} + +TEST(SternheimerResponseGrid, AnisotropicRestrictionPreservesCommonMode) +{ + const auto fine = make_basis(12, 10, 14); + const auto coarse = make_basis(8, 6, 10); + const auto input = cosine_mode(12, 10, 14, 2, 1, 3); + const auto expected = cosine_mode(8, 6, 10, 2, 1, 3); + + const auto output = ModuleRI::restrict_sternheimer_real_field_rectangular(*fine, *coarse, input); + + EXPECT_LT(relative_l2_error(output, expected), 1.0e-11); +} + +TEST(SternheimerResponseGrid, AnisotropicRestrictionRemovesZOnlyHighModeWithoutAliasing) +{ + const auto fine = make_basis(12, 10, 14); + const auto coarse = make_basis(8, 6, 10); + auto input = cosine_mode(12, 10, 14, 1, 1, 2); + const auto removed_z_mode = cosine_mode(12, 10, 14, 1, 1, 6); + for (std::size_t ir = 0; ir != input.size(); ++ir) + { + input[ir] += 0.25 * removed_z_mode[ir]; + } + const auto expected = cosine_mode(8, 6, 10, 1, 1, 2); + + const auto output = ModuleRI::restrict_sternheimer_real_field_rectangular(*fine, *coarse, input); + + EXPECT_LT(relative_l2_error(output, expected), 1.0e-11); +} + +TEST(SternheimerResponseGrid, RectangularRestrictionKeepsInPlaneModeBeyondSparseZSphere) +{ + const auto fine = make_basis(18, 18, 18); + const auto coarse = make_basis(12, 12, 6); + const auto input = cosine_mode(18, 18, 18, 4, 0, 0); + const auto expected = cosine_mode(12, 12, 6, 4, 0, 0); + + const auto output = ModuleRI::restrict_sternheimer_real_field_rectangular(*fine, *coarse, input); + + EXPECT_LT(relative_l2_error(output, expected), 1.0e-11); +} + +TEST(SternheimerResponseGrid, RectangularRestrictionCombinesEvenGridNyquistPair) +{ + const auto fine = make_basis(12, 10, 14); + const auto coarse = make_basis(8, 6, 10); + const auto input = cosine_mode(12, 10, 14, 4, 1, 2); + const auto expected = cosine_mode(8, 6, 10, 4, 1, 2); + + const auto output = ModuleRI::restrict_sternheimer_real_field_rectangular(*fine, *coarse, input); + + EXPECT_LT(relative_l2_error(output, expected), 1.0e-11); +} + +int main(int argc, char** argv) +{ +#ifdef __MPI + MPI_Init(&argc, &argv); +#endif + testing::InitGoogleTest(&argc, argv); + const int result = RUN_ALL_TESTS(); +#ifdef __MPI + MPI_Finalize(); +#endif + return result; +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_rpa_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_rpa_test.cpp new file mode 100644 index 00000000000..5d161141282 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_rpa_test.cpp @@ -0,0 +1,1195 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Complex = std::complex; +using Vector = ModuleRI::SternheimerRPA::Vector; + +Complex dot(const Vector& lhs, const Vector& rhs) +{ + return ModuleRI::SternheimerRPA::local_grid_dot(lhs, rhs, 1.0); +} + +void write_coulomb_v1_block(const std::string& filename, + const int pair_index, + const Complex value) +{ + std::ofstream out(filename.c_str(), std::ios::binary | std::ios::trunc); + const std::int32_t header[] = {-20129433, 1, 2, 1, 2, 1, 1, 1, pair_index}; + out.write(reinterpret_cast(header), sizeof(header)); + const std::int64_t offset = static_cast(sizeof(header) + sizeof(std::int64_t)); + out.write(reinterpret_cast(&offset), sizeof(offset)); + out.write(reinterpret_cast(&value), sizeof(value)); +} + +} // namespace + +TEST(SternheimerRPA, BuildRhsFromHartreePerturbation) +{ + const std::vector v_r = {2.0, -1.0}; + const Vector psi_r = {Complex(1.0, 1.0), Complex(2.0, 0.0)}; + + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(v_r, psi_r, rhs); + + ASSERT_EQ(rhs.size(), 2); + EXPECT_DOUBLE_EQ(rhs[0].real(), -2.0); + EXPECT_DOUBLE_EQ(rhs[0].imag(), -2.0); + EXPECT_DOUBLE_EQ(rhs[1].real(), 2.0); + EXPECT_DOUBLE_EQ(rhs[1].imag(), 0.0); +} + +TEST(SternheimerRPA, ComplexQPotentialBuildsRhsAndUsesMinusQProbe) +{ + const Vector v_q = {Complex(2.0, 1.0), Complex(-1.0, 0.5)}; + const Vector psi_k = {Complex(1.0, -1.0), Complex(0.5, 2.0)}; + const Vector delta_psi_kq = {Complex(0.25, 0.5), Complex(-1.0, 0.75)}; + constexpr double grid_weight = 0.2; + + Vector rhs; + ModuleRI::SternheimerRPA::build_rhs_from_hartree_perturbation(v_q, psi_k, rhs); + ASSERT_EQ(rhs.size(), psi_k.size()); + for (std::size_t ir = 0; ir != rhs.size(); ++ir) + { + EXPECT_NEAR(rhs[ir].real(), (-v_q[ir] * psi_k[ir]).real(), 1.0e-14); + EXPECT_NEAR(rhs[ir].imag(), (-v_q[ir] * psi_k[ir]).imag(), 1.0e-14); + } + + const Complex value = ModuleRI::SternheimerRPA::accumulate_polarizability_grid_element( + v_q, psi_k, delta_psi_kq, grid_weight); + const Complex expected + = grid_weight + * (std::conj(psi_k[0]) * std::conj(v_q[0]) * delta_psi_kq[0] + + std::conj(psi_k[1]) * std::conj(v_q[1]) * delta_psi_kq[1]); + EXPECT_NEAR(value.real(), expected.real(), 1.0e-14); + EXPECT_NEAR(value.imag(), expected.imag(), 1.0e-14); +} + +TEST(SternheimerRPA, AccumulatePolarizabilityGridElement) +{ + const std::vector v_r = {2.0, -1.0}; + const Vector psi_r = {Complex(1.0, 1.0), Complex(2.0, 0.0)}; + const Vector delta_psi_r = {Complex(0.5, 0.0), Complex(0.0, 1.0)}; + constexpr double grid_weight = 0.25; + + const Complex value + = ModuleRI::SternheimerRPA::accumulate_polarizability_grid_element(v_r, psi_r, delta_psi_r, grid_weight); + const Complex expected + = grid_weight * (std::conj(psi_r[0]) * v_r[0] * delta_psi_r[0] + std::conj(psi_r[1]) * v_r[1] * delta_psi_r[1]); + + EXPECT_NEAR(value.real(), expected.real(), 1.0e-14); + EXPECT_NEAR(value.imag(), expected.imag(), 1.0e-14); +} + +TEST(SternheimerRPA, AccumulateChi0BranchColumnIncludesOccupation) +{ + const std::vector> potentials = {{2.0, -1.0}, {0.5, 3.0}}; + const Vector psi_r = {Complex(1.0, 1.0), Complex(2.0, 0.0)}; + const Vector delta_psi_r = {Complex(0.5, 0.0), Complex(0.0, 1.0)}; + constexpr double grid_weight = 0.25; + constexpr double occupation = 2.0; + + std::vector branch(4, Complex(0.0, 0.0)); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(potentials, + psi_r, + delta_psi_r, + grid_weight, + occupation, + 1, + branch); + + const Complex expected0 = occupation + * ModuleRI::SternheimerRPA::accumulate_polarizability_grid_element(potentials[0], + psi_r, + delta_psi_r, + grid_weight); + const Complex expected1 = occupation + * ModuleRI::SternheimerRPA::accumulate_polarizability_grid_element(potentials[1], + psi_r, + delta_psi_r, + grid_weight); + EXPECT_EQ(branch[0], Complex(0.0, 0.0)); + EXPECT_EQ(branch[2], Complex(0.0, 0.0)); + EXPECT_NEAR(branch[1].real(), expected0.real(), 1.0e-14); + EXPECT_NEAR(branch[1].imag(), expected0.imag(), 1.0e-14); + EXPECT_NEAR(branch[3].real(), expected1.real(), 1.0e-14); + EXPECT_NEAR(branch[3].imag(), expected1.imag(), 1.0e-14); +} + +TEST(SternheimerRPA, CollinearSpinBranchesAddBeforeConjugateSymmetrization) +{ + const std::vector> potentials = {{2.0, -1.0}, {0.5, 3.0}}; + const Vector psi_up = {Complex(1.0, 0.5), Complex(0.5, -0.25)}; + const Vector delta_up = {Complex(-0.2, 0.3), Complex(0.4, 0.1)}; + const Vector psi_down = {Complex(0.3, -0.4), Complex(-0.8, 0.2)}; + const Vector delta_down = {Complex(0.6, 0.2), Complex(-0.1, 0.5)}; + constexpr double grid_weight = 0.125; + + std::vector spin_up(4, Complex(0.0, 0.0)); + std::vector spin_down(4, Complex(0.0, 0.0)); + std::vector combined(4, Complex(0.0, 0.0)); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(potentials, psi_up, delta_up, grid_weight, 1.0, 0, spin_up); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(potentials, + psi_down, + delta_down, + grid_weight, + 1.0, + 0, + spin_down); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(potentials, + psi_up, + delta_up, + grid_weight, + 1.0, + 0, + combined); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(potentials, + psi_down, + delta_down, + grid_weight, + 1.0, + 0, + combined); + + for (std::size_t index = 0; index != combined.size(); ++index) + { + EXPECT_NEAR(combined[index].real(), (spin_up[index] + spin_down[index]).real(), 1.0e-14); + EXPECT_NEAR(combined[index].imag(), (spin_up[index] + spin_down[index]).imag(), 1.0e-14); + } + + const auto symmetrized_combined = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(combined, 2); + const auto symmetrized_up = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(spin_up, 2); + const auto symmetrized_down = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(spin_down, 2); + for (std::size_t index = 0; index != symmetrized_combined.size(); ++index) + { + EXPECT_NEAR(symmetrized_combined[index].real(), + (symmetrized_up[index] + symmetrized_down[index]).real(), + 1.0e-14); + EXPECT_NEAR(symmetrized_combined[index].imag(), + (symmetrized_up[index] + symmetrized_down[index]).imag(), + 1.0e-14); + } +} + +TEST(SternheimerRPA, AccumulateComplexQChi0BranchColumnReadsChannelStorageDirectly) +{ + std::vector channels(2); + channels[0].potential_r = {Complex(2.0, 1.0), Complex(-1.0, 0.5)}; + channels[1].potential_r = {Complex(0.5, -0.25), Complex(3.0, 2.0)}; + const Vector psi_k = {Complex(1.0, 1.0), Complex(2.0, -0.5)}; + const Vector delta_psi_kq = {Complex(0.5, -0.25), Complex(-0.75, 1.0)}; + constexpr double grid_weight = 0.25; + constexpr double occupation = 1.5; + + std::vector direct(4, Complex(0.0, 0.0)); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(channels, + psi_k, + delta_psi_kq, + grid_weight, + occupation, + 0, + direct); + + const std::vector copied = {channels[0].potential_r, channels[1].potential_r}; + std::vector expected(4, Complex(0.0, 0.0)); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(copied, + psi_k, + delta_psi_kq, + grid_weight, + occupation, + 0, + expected); + EXPECT_EQ(direct, expected); +} + +TEST(SternheimerRPA, AccumulateComplexQChi0BranchColumnsMatchScalarColumns) +{ + std::vector channels(3); + channels[0].potential_r = {Complex(2.0, 1.0), Complex(-1.0, 0.5)}; + channels[1].potential_r = {Complex(0.5, -0.25), Complex(3.0, 2.0)}; + channels[2].potential_r = {Complex(-0.75, 0.25), Complex(1.5, -2.0)}; + const Vector psi_k = {Complex(1.0, 1.0), Complex(2.0, -0.5)}; + const std::vector delta_psi_batch{{Complex(0.5, -0.25), Complex(-0.75, 1.0)}, + {Complex(-0.25, 0.75), Complex(1.25, 0.5)}}; + const std::vector delta_psi_batch_views{&delta_psi_batch[0], &delta_psi_batch[1]}; + constexpr double grid_weight = 0.25; + constexpr double occupation = 1.5; + + std::vector batched(9, Complex(0.0, 0.0)); + ModuleRI::SternheimerRPA::accumulate_chi0_branch_columns(channels, + psi_k, + delta_psi_batch_views, + grid_weight, + occupation, + 1, + batched); + + std::vector scalar(9, Complex(0.0, 0.0)); + for (std::size_t offset = 0; offset != delta_psi_batch.size(); ++offset) + { + ModuleRI::SternheimerRPA::accumulate_chi0_branch_column(channels, + psi_k, + delta_psi_batch[offset], + grid_weight, + occupation, + static_cast(offset) + 1, + scalar); + } + for (std::size_t index = 0; index != batched.size(); ++index) + { + EXPECT_NEAR(batched[index].real(), scalar[index].real(), 1.0e-14); + EXPECT_NEAR(batched[index].imag(), scalar[index].imag(), 1.0e-14); + } +} + +TEST(SternheimerRPA, SymmetrizeImaginaryFrequencyChi0AddsAdjointBranch) +{ + const std::vector branch = {Complex(1.0, 2.0), Complex(3.0, -4.0), Complex(5.0, 6.0), Complex(-7.0, 8.0)}; + + const std::vector chi0 = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(branch, 2); + + ASSERT_EQ(chi0.size(), 4); + EXPECT_EQ(chi0[0], branch[0] + std::conj(branch[0])); + EXPECT_EQ(chi0[1], branch[1] + std::conj(branch[2])); + EXPECT_EQ(chi0[2], branch[2] + std::conj(branch[1])); + EXPECT_EQ(chi0[3], branch[3] + std::conj(branch[3])); + EXPECT_NE(chi0[1], Complex(2.0 * branch[1].real(), 0.0)); +} + +TEST(SternheimerRPA, WriteChi0V1FileUsesAtomPairBlocks) +{ + ModuleRI::SternheimerRPA::Chi0V1Metadata metadata; + metadata.iq = 1; + metadata.ifrequency = 2; + metadata.omega = 0.5; + metadata.weight = 0.125; + metadata.atom_naux = {1, 1}; + + const std::vector channels = {{0, 0, 0}, {1, 1, 0}}; + const std::vector chi0 = {Complex(1.0, 0.0), Complex(2.0, -0.5), Complex(2.0, 0.5), Complex(3.0, 0.0)}; + + const std::string filename = ::testing::TempDir() + "/sternheimer_chi0_v1_test.dat"; + ModuleRI::SternheimerRPA::write_chi0_v1_file(filename, metadata, channels, chi0); + + std::ifstream in(filename.c_str(), std::ios::binary); + ASSERT_TRUE(in.good()); + + auto read_i32 = [&in]() { + std::int32_t value = 0; + in.read(reinterpret_cast(&value), sizeof(value)); + return value; + }; + auto read_i64 = [&in]() { + std::int64_t value = 0; + in.read(reinterpret_cast(&value), sizeof(value)); + return value; + }; + auto read_double = [&in]() { + double value = 0.0; + in.read(reinterpret_cast(&value), sizeof(value)); + return value; + }; + + EXPECT_EQ(read_i32(), ModuleRI::SternheimerRPA::chi0_v1_marker()); + EXPECT_EQ(read_i32(), 1); + EXPECT_EQ(read_i32(), 2); + EXPECT_EQ(read_i32(), 2); + EXPECT_EQ(read_i32(), 1); + EXPECT_EQ(read_i32(), 2); + EXPECT_DOUBLE_EQ(read_double(), 0.5); + EXPECT_DOUBLE_EQ(read_double(), 0.125); + EXPECT_EQ(read_i32(), 3); + EXPECT_EQ(read_i32(), 1); + EXPECT_EQ(read_i32(), 1); + EXPECT_EQ(read_i32(), 0); + const std::int64_t first_offset = read_i64(); + EXPECT_EQ(read_i32(), 1); + const std::int64_t second_offset = read_i64(); + EXPECT_EQ(read_i32(), 2); + const std::int64_t third_offset = read_i64(); + EXPECT_LT(first_offset, second_offset); + EXPECT_LT(second_offset, third_offset); + + std::remove(filename.c_str()); +} + +TEST(SternheimerRPA, ReadsCoulombV1BlocksAcrossRankFiles) +{ + const std::string prefix = ::testing::TempDir() + "/sternheimer_coulomb_v1_rank"; + const std::vector blocks{Complex(1.0, 0.0), Complex(2.0, 3.0), Complex(4.0, 0.0)}; + std::vector filenames; + for (int pair_index = 0; pair_index != 3; ++pair_index) + { + const std::string filename = prefix + std::to_string(pair_index) + ".dat"; + write_coulomb_v1_block(filename, pair_index, blocks[static_cast(pair_index)]); + filenames.push_back(filename); + } + + const auto matrix = ModuleRI::SternheimerRPA::read_coulomb_v1_files(filenames); + + EXPECT_EQ(matrix.iq, 1); + EXPECT_EQ(matrix.atom_naux, std::vector({1, 1})); + EXPECT_EQ(matrix.values, + std::vector({Complex(1.0, 0.0), + Complex(2.0, 3.0), + Complex(2.0, -3.0), + Complex(4.0, 0.0)})); + for (const std::string& filename: filenames) + { + std::remove(filename.c_str()); + } +} + +TEST(SternheimerRPA, ProjectOutSubspace) +{ + const std::vector occupied + = {{Complex(1.0, 0.0), Complex(0.0, 0.0)}, {Complex(0.0, 0.0), Complex(0.0, 1.0)}}; + Vector vec = {Complex(2.0, 1.0), Complex(3.0, -4.0)}; + + ModuleRI::SternheimerRPA::project_out_subspace(occupied, dot, vec); + + EXPECT_NEAR(std::abs(dot(occupied[0], vec)), 0.0, 1.0e-14); + EXPECT_NEAR(std::abs(dot(occupied[1], vec)), 0.0, 1.0e-14); +} + +TEST(SternheimerRPA, SubspaceProjectorAcceptsNonowningPointerViews) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + using Views = std::vector; + EXPECT_TRUE((std::is_constructible::value)); + EXPECT_TRUE((std::is_constructible::value)); + EXPECT_TRUE((std::is_constructible::value)); +} + +TEST(SternheimerRPA, SubspaceProjectorRejectsMismatchedBasisSizesAtConstruction) +{ + const std::vector invalid{{Complex(1.0, 0.0)}, {Complex(1.0, 0.0), Complex(0.0, 0.0)}}; + EXPECT_THROW(ModuleRI::SternheimerSubspaceProjector(invalid, dot), std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerSubspaceProjector(invalid, 1.0), std::invalid_argument); +} + +TEST(SternheimerRPA, SubspaceProjectorBorrowsOriginalVectorsAndOwnsOnlyViewList) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + const Vector first{Complex(1.0, 0.0), Complex(0.0, 1.0), Complex(0.0, 0.0)}; + const Vector second{Complex(0.0, 1.0), Complex(1.0, 0.0), Complex(0.0, 0.0)}; + int dot_calls = 0; + const auto identity_dot = [&](const Vector& lhs, const Vector& rhs) { + EXPECT_TRUE(&lhs == &first || &lhs == &second); + ++dot_calls; + return dot(lhs, rhs); + }; + const Projector projector = [&]() { + std::vector views{&first, &second}; + return Projector(views, identity_dot); + }(); + EXPECT_EQ(dot_calls, 2); + Vector scalar{Complex(2.0, 1.0), Complex(-3.0, 0.5), Complex(4.0, -2.0)}; + std::vector batch{scalar}; + projector.project(scalar); + projector.project_batch(batch); + EXPECT_EQ(dot_calls, 6); + EXPECT_EQ(batch.front(), scalar); + EXPECT_NEAR(std::abs(scalar[0]), 0.0, 1.0e-14); + EXPECT_NEAR(std::abs(scalar[1]), 0.0, 1.0e-14); + EXPECT_EQ(scalar[2], Complex(4.0, -2.0)); +} + +TEST(SternheimerRPA, SubspaceProjectorLegacyConstructorAlsoBorrowsOriginalVectors) +{ + const std::vector basis{{Complex(1.0, 0.0), Complex(0.0, 0.0)}}; + const ModuleRI::SternheimerSubspaceProjector projector( + basis, + [&](const Vector& lhs, const Vector& rhs) { + EXPECT_EQ(&lhs, &basis.front()); + return dot(lhs, rhs); + }); + Vector value{Complex(2.0, -1.0), Complex(0.5, 0.25)}; + projector.project(value); + EXPECT_NEAR(std::abs(value.front()), 0.0, 1.0e-14); +} + +TEST(SternheimerRPA, SubspaceProjectorPointerViewsRejectNullAndInconsistentSizes) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + const Vector first(3, Complex(1.0, 0.0)); + const Vector shorter(2, Complex(1.0, 0.0)); + const std::vector> invalid_views{ + {nullptr}, {&first, nullptr}, {&first, &shorter}}; + for (const auto& views: invalid_views) + { + EXPECT_THROW(Projector(views, dot), std::invalid_argument); + EXPECT_THROW(Projector(views, 1.0), std::invalid_argument); + EXPECT_THROW(Projector(views, 1.0, true), std::invalid_argument); + } + const std::vector views{&first}; + EXPECT_THROW(Projector(views, Projector::Dot{}), std::invalid_argument); + EXPECT_THROW(Projector(views, 0.0), std::invalid_argument); + for (const bool packed: {false, true}) + { + const Projector projector(views, 1.0, packed); + Vector invalid(2); + EXPECT_THROW(projector.project(invalid), std::invalid_argument); + std::vector invalid_batch{first, invalid}; + EXPECT_THROW(projector.project_batch(invalid_batch), std::invalid_argument); + } +} + +TEST(SternheimerRPA, SubspaceProjectorPointerViewsAllowEmptyAndZeroNormBases) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + const Vector zero(3, Complex(0.0, 0.0)); + const Vector input{Complex(1.0, 2.0), Complex(3.0, 4.0), Complex(-1.0, 0.5)}; + for (const auto& views: {std::vector{}, std::vector{&zero}}) + { + for (const bool packed: {false, true}) + { + const Projector projector(views, 0.5, packed); + Vector scalar = input; + std::vector batch{input, input}; + projector.project(scalar); + projector.project_batch(batch); + EXPECT_EQ(scalar, input); + EXPECT_EQ(batch, (std::vector{input, input})); + std::vector empty; + projector.project_batch(empty); + EXPECT_TRUE(empty.empty()); + } + } +} + +TEST(SternheimerRPA, SubspaceProjectorOwnsOnePackedBasisAndNoScalarGridCopy) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + constexpr std::size_t grid_size = 1024; + Vector first(grid_size, Complex(0.0, 0.0)); + Vector second = first; + first[0] = Complex(1.0, 0.0); + second[1] = Complex(0.0, 1.0); + const std::vector views{&first, &second}; + const Projector scalar(views, 1.0); + const Projector packed(views, 1.0, true); + EXPECT_EQ(scalar.storage_bytes(), views.size() * (sizeof(const Vector*) + sizeof(Complex))); + EXPECT_EQ(packed.storage_bytes() - scalar.storage_bytes(), + views.size() * grid_size * sizeof(Complex)); + const auto bytes_before = packed.storage_bytes(); + for (int repeat = 0; repeat != 8; ++repeat) + { + std::vector batch{first, second}; + packed.project_batch(batch); + EXPECT_EQ(packed.storage_bytes(), bytes_before); + } +} + +TEST(SternheimerRPA, SubspaceProjectorReportsBasisSizeIncludingZeroNormViews) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + const Vector zero(3, Complex(0.0, 0.0)); + const Vector first(3, Complex(1.0, 0.0)); + const Projector empty(std::vector{}, 1.0); + const Projector views(std::vector{&zero, &first}, 1.0, true); + const std::vector basis{first}; + const Projector legacy(basis, dot); + EXPECT_EQ(empty.basis_size(), 0U); + EXPECT_EQ(views.basis_size(), 2U); + EXPECT_EQ(legacy.basis_size(), 1U); +} + +TEST(SternheimerRPA, SubspaceProjectorPointerViewsSupportConcurrentScalarAndBatchReuse) +{ + using Projector = ModuleRI::SternheimerSubspaceProjector; + const Vector first{Complex(1.0, 0.0), Complex(0.0, 1.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; + const Vector second{Complex(0.0, 1.0), Complex(1.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; + const Vector zero(first.size(), Complex(0.0, 0.0)); + const std::vector views{&first, &second, &zero}; + const std::vector basis{first, second, zero}; + const std::vector inputs{ + {Complex(2.0, 1.0), Complex(3.0, -4.0), Complex(0.5, 0.25), Complex(-0.2, 0.7)}, + {Complex(-1.0, 2.0), Complex(0.5, 0.25), Complex(3.0, -0.75), Complex(0.8, -0.6)}, + {Complex(0.1, -0.2), Complex(-0.3, 0.4), Complex(0.5, -0.6), Complex(0.9, 1.1)}}; + auto expected = inputs; + for (auto& column: expected) + { + ModuleRI::SternheimerRPA::project_out_subspace(basis, dot, column); + } + for (int mode = 0; mode != 3; ++mode) + { + const Projector projector = mode == 0 ? Projector(views, dot) : Projector(views, 0.25, mode == 2); + const auto bytes_before = projector.storage_bytes(); + std::atomic ready{0}; + std::atomic start{false}; + std::vector> workers; + for (int worker = 0; worker != 8; ++worker) + { + workers.push_back(std::async(std::launch::async, [&]() { + ++ready; + while (!start.load()) + { + std::this_thread::yield(); + } + double max_error = 0.0; + for (int repeat = 0; repeat != 12; ++repeat) + { + auto scalar = inputs; + auto batch = inputs; + for (auto& column: scalar) + { + projector.project(column); + } + projector.project_batch(batch); + for (std::size_t column = 0; column != inputs.size(); ++column) + { + for (std::size_t ir = 0; ir != first.size(); ++ir) + { + max_error = std::max(max_error, std::abs(scalar[column][ir] - expected[column][ir])); + max_error = std::max(max_error, std::abs(batch[column][ir] - expected[column][ir])); + } + } + } + return max_error; + })); + } + while (ready.load() != 8) + { + std::this_thread::yield(); + } + start = true; + for (auto& worker: workers) + { + EXPECT_LE(worker.get(), 1.0e-13); + } + EXPECT_EQ(projector.storage_bytes(), bytes_before); + } +} + +TEST(SternheimerRPA, CachedSubspaceProjectorReusesBasisNorms) +{ + int dot_calls = 0; + const auto counting_dot = [&dot_calls](const Vector& lhs, const Vector& rhs) { + ++dot_calls; + return dot(lhs, rhs); + }; + const std::vector occupied + = {{Complex(2.0, 0.0), Complex(0.0, 0.0)}, {Complex(0.0, 0.0), Complex(0.0, 3.0)}}; + const ModuleRI::SternheimerSubspaceProjector projector(occupied, counting_dot); + + EXPECT_EQ(dot_calls, 2); + Vector first = {Complex(2.0, 1.0), Complex(3.0, -4.0)}; + projector.project(first); + EXPECT_EQ(dot_calls, 4); + Vector second = {Complex(-1.0, 2.0), Complex(0.5, 0.25)}; + projector.project(second); + EXPECT_EQ(dot_calls, 6); + EXPECT_NEAR(std::abs(dot(occupied[0], first)), 0.0, 1.0e-14); + EXPECT_NEAR(std::abs(dot(occupied[1], first)), 0.0, 1.0e-14); + EXPECT_NEAR(std::abs(dot(occupied[0], second)), 0.0, 1.0e-14); + EXPECT_NEAR(std::abs(dot(occupied[1], second)), 0.0, 1.0e-14); +} + +TEST(SternheimerRPA, CachedSubspaceProjectorBatchMatchesScalar) +{ + const std::vector occupied + = {{Complex(2.0, 0.0), Complex(0.0, 0.0), Complex(1.0, -0.5)}, + {Complex(0.0, 0.0), Complex(0.0, 3.0), Complex(-0.25, 0.75)}}; + const ModuleRI::SternheimerSubspaceProjector projector(occupied, dot); + std::vector expected + = {{Complex(2.0, 1.0), Complex(3.0, -4.0), Complex(0.5, 0.25)}, + {Complex(-1.0, 2.0), Complex(0.5, 0.25), Complex(3.0, -0.75)}, + {Complex(0.1, -0.2), Complex(-0.3, 0.4), Complex(0.5, -0.6)}, + {Complex(-0.7, 0.8), Complex(0.9, -1.0), Complex(-1.1, 1.2)}}; + std::vector actual = expected; + for (Vector& column: expected) + { + projector.project(column); + } + + projector.project_batch(actual); + + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t column = 0; column != expected.size(); ++column) + { + ASSERT_EQ(actual[column].size(), expected[column].size()); + for (std::size_t ir = 0; ir != expected[column].size(); ++ir) + { + EXPECT_NEAR(actual[column][ir].real(), expected[column][ir].real(), 1.0e-13); + EXPECT_NEAR(actual[column][ir].imag(), expected[column][ir].imag(), 1.0e-13); + } + } + + std::vector empty; + projector.project_batch(empty); + EXPECT_TRUE(empty.empty()); + + std::vector one{{Complex(2.0, 1.0), Complex(3.0, -4.0), Complex(0.5, 0.25)}}; + projector.project_batch(one); + ASSERT_EQ(one.size(), 1U); + EXPECT_EQ(one.front(), expected.front()); + + std::vector invalid{{Complex(1.0, 0.0)}}; + EXPECT_THROW(projector.project_batch(invalid), std::invalid_argument); +} + +TEST(SternheimerRPA, WeightedSubspaceProjectorBatchMatchesScalar) +{ + const double grid_weight = 0.125; + const std::vector occupied + = {{Complex(2.0, 0.0), Complex(0.0, 0.0), Complex(1.0, -0.5)}, + {Complex(0.0, 0.0), Complex(0.0, 3.0), Complex(-0.25, 0.75)}}; + const ModuleRI::SternheimerSubspaceProjector scalar_projector( + occupied, + [grid_weight](const Vector& lhs, const Vector& rhs) { return grid_weight * dot(lhs, rhs); }); + const ModuleRI::SternheimerSubspaceProjector batch_projector(occupied, grid_weight); + std::vector expected + = {{Complex(2.0, 1.0), Complex(3.0, -4.0), Complex(0.5, 0.25)}, + {Complex(-1.0, 2.0), Complex(0.5, 0.25), Complex(3.0, -0.75)}, + {Complex(0.1, -0.2), Complex(-0.3, 0.4), Complex(0.5, -0.6)}, + {Complex(-0.7, 0.8), Complex(0.9, -1.0), Complex(-1.1, 1.2)}}; + std::vector actual = expected; + for (Vector& column: expected) + { + scalar_projector.project(column); + } + + batch_projector.project_batch(actual); + + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t column = 0; column != expected.size(); ++column) + { + ASSERT_EQ(actual[column].size(), expected[column].size()); + for (std::size_t ir = 0; ir != expected[column].size(); ++ir) + { + EXPECT_NEAR(actual[column][ir].real(), expected[column][ir].real(), 1.0e-13); + EXPECT_NEAR(actual[column][ir].imag(), expected[column][ir].imag(), 1.0e-13); + } + } + + EXPECT_THROW(ModuleRI::SternheimerSubspaceProjector(occupied, 0.0), std::invalid_argument); +} + +TEST(SternheimerRPA, OrthonormalSubspaceProjectorBatchMatchesScalar) +{ + const double grid_weight = 0.25; + const std::vector occupied + = {{Complex(2.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}, + {Complex(0.0, 0.0), Complex(0.0, 2.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}}; + const ModuleRI::SternheimerSubspaceProjector scalar_projector( + occupied, + [grid_weight](const Vector& lhs, const Vector& rhs) { return grid_weight * dot(lhs, rhs); }); + const ModuleRI::SternheimerSubspaceProjector batch_projector(occupied, grid_weight, true); + std::vector expected + = {{Complex(2.0, 1.0), Complex(3.0, -4.0), Complex(0.5, 0.25), Complex(-0.2, 0.7)}, + {Complex(-1.0, 2.0), Complex(0.5, 0.25), Complex(3.0, -0.75), Complex(0.8, -0.6)}, + {Complex(0.1, -0.2), Complex(-0.3, 0.4), Complex(0.5, -0.6), Complex(0.9, 1.1)}}; + std::vector actual = expected; + for (Vector& column: expected) + { + scalar_projector.project(column); + } + + batch_projector.project_batch(actual); + + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t column = 0; column != expected.size(); ++column) + { + ASSERT_EQ(actual[column].size(), expected[column].size()); + for (std::size_t ir = 0; ir != expected[column].size(); ++ir) + { + EXPECT_NEAR(actual[column][ir].real(), expected[column][ir].real(), 1.0e-13); + EXPECT_NEAR(actual[column][ir].imag(), expected[column][ir].imag(), 1.0e-13); + } + } +} + +TEST(SternheimerRPA, SpectralPreconditionerIsDefault) +{ + const ModuleRI::SternheimerRPA::SolverOptions options{}; + + EXPECT_TRUE(options.use_fd_spectral_preconditioner); + EXPECT_DOUBLE_EQ(options.fd_spectral_preconditioner_regularization, 0.0); +} + +TEST(SternheimerRPA, ApplyKineticPreconditioner) +{ + const std::vector kinetic = {1.0, 3.0}; + const Vector input = {Complex(2.0, 0.0), Complex(0.0, 4.0)}; + Vector output; + + ModuleRI::SternheimerRPA::apply_kinetic_preconditioner(kinetic, 0.5, 2.0, 0.1, input, output); + + ASSERT_EQ(output.size(), input.size()); + EXPECT_NEAR(output[0].real(), (input[0] / Complex(0.6, 2.0)).real(), 1.0e-14); + EXPECT_NEAR(output[0].imag(), (input[0] / Complex(0.6, 2.0)).imag(), 1.0e-14); + EXPECT_NEAR(output[1].real(), (input[1] / Complex(2.6, 2.0)).real(), 1.0e-14); + EXPECT_NEAR(output[1].imag(), (input[1] / Complex(2.6, 2.0)).imag(), 1.0e-14); +} + +TEST(SternheimerRPA, TransitionEnergyWindowUsesOccupiedToEmptyPairsAndReturnsHartree) +{ + const std::vector eigenvalues_ry = {-1.0, -0.5, 0.25, 1.5}; + const std::vector occupations = {2.0, 2.0, 0.0, 0.0}; + + const auto window + = ModuleRI::SternheimerRPA::transition_energy_window_from_eigenvalues_ry(eigenvalues_ry, occupations); + + EXPECT_DOUBLE_EQ(window.emin_ha, 0.375); + EXPECT_DOUBLE_EQ(window.emax_ha, 1.25); +} + +TEST(SternheimerRPA, OptionalTransitionEnergyWindowDoesNotBlockExternalFrequencyGrid) +{ + using Window = ModuleRI::SternheimerRPA::TransitionEnergyWindow; + Window window; + + EXPECT_FALSE(ModuleRI::SternheimerRPA::try_transition_energy_window_from_eigenvalues_ry( + {-1.0, -0.5}, {2.0, 2.0}, window)); + EXPECT_THROW(ModuleRI::SternheimerRPA::try_transition_energy_window_from_eigenvalues_ry( + {-1.0}, {2.0, 0.0}, window), + std::invalid_argument); + + EXPECT_TRUE(ModuleRI::SternheimerRPA::try_transition_energy_window_from_eigenvalues_ry( + {-1.0, 0.5}, {2.0, 0.0}, window)); + EXPECT_DOUBLE_EQ(window.emin_ha, 0.75); + EXPECT_DOUBLE_EQ(window.emax_ha, 0.75); +} + +TEST(SternheimerRPA, MergeTransitionEnergyWindowsCoversAllSpinChannels) +{ + using Window = ModuleRI::SternheimerRPA::TransitionEnergyWindow; + const auto merged = ModuleRI::SternheimerRPA::merge_transition_energy_windows({Window{0.4, 2.0}, Window{0.2, 3.5}}); + + EXPECT_DOUBLE_EQ(merged.emin_ha, 0.2); + EXPECT_DOUBLE_EQ(merged.emax_ha, 3.5); + EXPECT_THROW(ModuleRI::SternheimerRPA::merge_transition_energy_windows({}), std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::merge_transition_energy_windows({Window{0.0, 1.0}}), std::invalid_argument); +} + +#ifdef __GREENX_MINIMAX +TEST(SternheimerRPA, GreenXMinimaxFrequencyGridMatchesReference) +{ + const auto grid = ModuleRI::SternheimerRPA::generate_greenx_minimax_frequency_grid(10, 2.0, 30.0); + + const std::vector expected_nodes = {0.37397426, + 1.17910655, + 2.16745299, + 3.50267933, + 5.42267468, + 8.30517692, + 12.81707194, + 20.35677335, + 34.86933737, + 75.79619716}; + const std::vector expected_weights = {0.75729004, + 0.87356340, + 1.12983421, + 1.57927297, + 2.32153469, + 3.54877702, + 5.68714236, + 9.94234857, + 21.24030777, + 80.07961331}; + + ASSERT_EQ(grid.omega_ha.size(), expected_nodes.size()); + ASSERT_EQ(grid.weights_ha.size(), expected_weights.size()); + for (std::size_t i = 0; i != expected_nodes.size(); ++i) + { + EXPECT_NEAR(grid.omega_ha[i], expected_nodes[i], 1.0e-8); + EXPECT_NEAR(grid.weights_ha[i], expected_weights[i], 1.0e-8); + } +} +#endif + +TEST(SternheimerRPA, ReadFrequencyGridFileAcceptsTwoAndThreeColumnRows) +{ + const std::string filename = ::testing::TempDir() + "/sternheimer_frequency_grid.dat"; + { + std::ofstream out(filename.c_str()); + out << "# omega_Ha weight_Ha\n"; + out << "0.25 0.75\n"; + out << "2 0.50 1.25 # optional one-based index column\n"; + } + + const auto grid = ModuleRI::SternheimerRPA::read_frequency_grid_file(filename, 2); + + ASSERT_EQ(grid.omega_ha.size(), 2); + ASSERT_EQ(grid.weights_ha.size(), 2); + EXPECT_DOUBLE_EQ(grid.omega_ha[0], 0.25); + EXPECT_DOUBLE_EQ(grid.weights_ha[0], 0.75); + EXPECT_DOUBLE_EQ(grid.omega_ha[1], 0.50); + EXPECT_DOUBLE_EQ(grid.weights_ha[1], 1.25); + + std::remove(filename.c_str()); +} + +TEST(SternheimerRPA, ReadFrequencyGridFileRejectsInvalidRows) +{ + const std::string count_mismatch = ::testing::TempDir() + "/sternheimer_frequency_grid_count.dat"; + { + std::ofstream out(count_mismatch.c_str()); + out << "0.25 0.75\n"; + } + EXPECT_THROW(ModuleRI::SternheimerRPA::read_frequency_grid_file(count_mismatch, 2), std::runtime_error); + std::remove(count_mismatch.c_str()); + + const std::string invalid_value = ::testing::TempDir() + "/sternheimer_frequency_grid_invalid.dat"; + { + std::ofstream out(invalid_value.c_str()); + out << "-0.25 0.75\n"; + } + EXPECT_THROW(ModuleRI::SternheimerRPA::read_frequency_grid_file(invalid_value, 1), std::invalid_argument); + std::remove(invalid_value.c_str()); +} + +TEST(SternheimerRPA, FrequencyOwnerRankSupportsRankShift) +{ + EXPECT_EQ(ModuleRI::SternheimerRPA::frequency_owner_rank(0, 6, 0), 0); + EXPECT_EQ(ModuleRI::SternheimerRPA::frequency_owner_rank(5, 6, 0), 5); + EXPECT_EQ(ModuleRI::SternheimerRPA::frequency_owner_rank(0, 6, 1), 1); + EXPECT_EQ(ModuleRI::SternheimerRPA::frequency_owner_rank(5, 6, 1), 0); + EXPECT_EQ(ModuleRI::SternheimerRPA::frequency_owner_rank(0, 1, 3), 0); + EXPECT_EQ(ModuleRI::SternheimerRPA::frequency_owner_rank(1, 4, -1), 0); + EXPECT_THROW(ModuleRI::SternheimerRPA::frequency_owner_rank(-1, 6, 0), std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::frequency_owner_rank(0, 0, 0), std::invalid_argument); +} + +TEST(SternheimerRPA, AssignsChannelMPIWithinFrequencyGroups) +{ + const auto leader = ModuleRI::SternheimerRPA::frequency_mpi_assignment(0, 16, 32, 2, 1, true); + EXPECT_TRUE(leader.owns_frequency); + EXPECT_EQ(leader.frequency_leader_rank, 2); + EXPECT_EQ(leader.frequency_group_size, 2); + EXPECT_EQ(leader.frequency_group_local_rank, 0); + + const auto partner = ModuleRI::SternheimerRPA::frequency_mpi_assignment(0, 16, 32, 3, 1, true); + EXPECT_TRUE(partner.owns_frequency); + EXPECT_EQ(partner.frequency_leader_rank, 2); + EXPECT_EQ(partner.frequency_group_size, 2); + EXPECT_EQ(partner.frequency_group_local_rank, 1); + + const auto other = ModuleRI::SternheimerRPA::frequency_mpi_assignment(0, 16, 32, 4, 1, true); + EXPECT_FALSE(other.owns_frequency); + EXPECT_EQ(other.frequency_group_local_rank, -1); + + const auto wrapped = ModuleRI::SternheimerRPA::frequency_mpi_assignment(15, 16, 32, 1, 1, true); + EXPECT_TRUE(wrapped.owns_frequency); + EXPECT_EQ(wrapped.frequency_leader_rank, 0); + EXPECT_EQ(wrapped.frequency_group_local_rank, 1); + + EXPECT_EQ(ModuleRI::SternheimerRPA::channel_group_owner(0, 0, 428, 2), 0); + EXPECT_EQ(ModuleRI::SternheimerRPA::channel_group_owner(0, 1, 428, 2), 1); + EXPECT_EQ(ModuleRI::SternheimerRPA::channel_group_owner(1, 0, 428, 2), 0); + EXPECT_EQ(ModuleRI::SternheimerRPA::channel_group_owner(1, 1, 428, 2), 1); + + EXPECT_THROW(ModuleRI::SternheimerRPA::frequency_mpi_assignment(0, 16, 30, 0, 0, true), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::channel_group_owner(0, 0, 428, 0), std::invalid_argument); +} + +TEST(SternheimerRPA, AssignsGlobalEquationsUniquelyAndEvenly) +{ + EXPECT_EQ(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 0, 16, 428, 32, 0), 0); + EXPECT_EQ(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 31, 16, 428, 32, 0), 31); + EXPECT_EQ(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 32, 16, 428, 32, 0), 0); + EXPECT_EQ(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 0, 16, 428, 32, 1), 1); + + constexpr int occupied_count = 2; + constexpr int frequency_count = 3; + constexpr int channel_count = 5; + constexpr int mpi_ranks = 4; + std::vector owner_counts(mpi_ranks, 0); + for (int occupied = 0; occupied != occupied_count; ++occupied) + { + for (int frequency = 0; frequency != frequency_count; ++frequency) + { + for (int channel = 0; channel != channel_count; ++channel) + { + const int owner = ModuleRI::SternheimerRPA::global_equation_owner(occupied, + frequency, + channel, + frequency_count, + channel_count, + mpi_ranks, + 0); + ASSERT_GE(owner, 0); + ASSERT_LT(owner, mpi_ranks); + ++owner_counts[owner]; + } + } + } + int minimum_count = owner_counts.front(); + int maximum_count = owner_counts.front(); + int total_count = 0; + for (const int count: owner_counts) + { + minimum_count = std::min(minimum_count, count); + maximum_count = std::max(maximum_count, count); + total_count += count; + } + EXPECT_EQ(total_count, occupied_count * frequency_count * channel_count); + EXPECT_LE(maximum_count - minimum_count, 1); + + EXPECT_THROW(ModuleRI::SternheimerRPA::global_equation_owner(-1, 0, 0, 3, 5, 4, 0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::global_equation_owner(0, -1, 0, 3, 5, 4, 0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::global_equation_owner(0, 3, 0, 3, 5, 4, 0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 5, 3, 5, 4, 0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 0, 0, 5, 4, 0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 0, 3, 0, 4, 0), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::global_equation_owner(0, 0, 0, 3, 5, 0, 0), + std::invalid_argument); +} + +TEST(SternheimerRPA, ValidatesGlobalEquationMPILayoutRequirements) +{ + EXPECT_NO_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("frequency_grouped", + true, + true, + true, + false, + 16, + 32)); + EXPECT_NO_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("frequency_grouped", + true, + true, + false, + true, + 16, + 32)); + EXPECT_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("frequency_grouped", + true, + true, + true, + false, + 16, + 30), + std::invalid_argument); + + EXPECT_NO_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("global_equation", + true, + true, + true, + false, + 6, + 4)); + EXPECT_NO_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("global_equation", + true, + true, + false, + true, + 6, + 4)); + EXPECT_NO_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("global_equation", + true, + true, + true, + true, + 6, + 4)); + EXPECT_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("global_equation", + false, + true, + true, + false, + 6, + 4), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("global_equation", + true, + false, + true, + false, + 6, + 4), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("global_equation", + true, + true, + false, + false, + 6, + 4), + std::invalid_argument); + EXPECT_THROW(ModuleRI::SternheimerRPA::validate_mpi_layout("invalid", + true, + true, + true, + false, + 6, + 4), + std::invalid_argument); +} + +TEST(SternheimerRPA, SolveBiCGStabDiagonalComplexSystem) +{ + const Vector diagonal = {Complex(2.0, 1.0), Complex(3.0, -0.5), Complex(4.0, 2.0)}; + const Vector exact = {Complex(1.0, -0.5), Complex(-2.0, 1.0), Complex(0.25, 0.75)}; + Vector rhs(exact.size()); + for (std::size_t i = 0; i != exact.size(); ++i) + { + rhs[i] = diagonal[i] * exact[i]; + } + + ModuleRI::SternheimerRPA::LinearProblem problem; + problem.apply = [&diagonal](const Vector& input, Vector& output) { + output.resize(input.size()); + for (std::size_t i = 0; i != input.size(); ++i) + { + output[i] = diagonal[i] * input[i]; + } + }; + problem.dot = dot; + + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 50; + options.residual_tol = 1.0e-12; + + Vector solution(exact.size(), Complex(0.0, 0.0)); + const auto result = ModuleRI::SternheimerRPA::solve_bicgstab(problem, rhs, solution, options); + + EXPECT_TRUE(result.converged); + EXPECT_LE(result.relative_residual, options.residual_tol); + for (std::size_t i = 0; i != exact.size(); ++i) + { + EXPECT_NEAR(solution[i].real(), exact[i].real(), 1.0e-10); + EXPECT_NEAR(solution[i].imag(), exact[i].imag(), 1.0e-10); + } +} + +TEST(SternheimerRPA, SolveGMRESDiagonalComplexSystem) +{ + const Vector diagonal = {Complex(2.0, 1.0), Complex(3.0, -0.5), Complex(4.0, 2.0)}; + const Vector exact = {Complex(1.0, -0.5), Complex(-2.0, 1.0), Complex(0.25, 0.75)}; + Vector rhs(exact.size()); + for (std::size_t i = 0; i != exact.size(); ++i) + { + rhs[i] = diagonal[i] * exact[i]; + } + + ModuleRI::SternheimerRPA::LinearProblem problem; + problem.apply = [&diagonal](const Vector& input, Vector& output) { + output.resize(input.size()); + for (std::size_t i = 0; i != input.size(); ++i) + { + output[i] = diagonal[i] * input[i]; + } + }; + problem.dot = dot; + + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 10; + options.residual_tol = 1.0e-12; + + Vector solution(exact.size(), Complex(0.0, 0.0)); + const auto result = ModuleRI::SternheimerRPA::solve_gmres(problem, rhs, solution, options, 3); + + EXPECT_TRUE(result.converged); + EXPECT_LE(result.relative_residual, options.residual_tol); + for (std::size_t i = 0; i != exact.size(); ++i) + { + EXPECT_NEAR(solution[i].real(), exact[i].real(), 1.0e-10); + EXPECT_NEAR(solution[i].imag(), exact[i].imag(), 1.0e-10); + } +} + +TEST(SternheimerRPA, SolveGMRESBatchMatchesIndependentScalarHistories) +{ + using Matrix = std::vector; + const Vector diagonal + = {Complex(1.5, 0.2), Complex(2.5, -0.3), Complex(4.0, 0.7), Complex(7.0, -0.4)}; + const Matrix exact + = {{Complex(0.5, -0.25), Complex(-1.0, 0.75), Complex(0.2, 0.4), Complex(-0.3, 0.1)}, + {Complex(1.25, -0.5), Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}, + {Complex(-0.75, 0.2), Complex(0.6, -0.9), Complex(1.1, 0.3), Complex(-0.4, 0.8)}}; + Matrix rhs = exact; + for (Vector& column: rhs) + { + for (std::size_t i = 0; i != column.size(); ++i) + { + column[i] *= diagonal[i]; + } + } + + std::vector batch_apply_widths; + ModuleRI::SternheimerRPA::BatchLinearProblem batch_problem; + batch_problem.apply = [&diagonal, &batch_apply_widths](const Matrix& input, Matrix& output) { + batch_apply_widths.push_back(input.size()); + output = input; + for (Vector& column: output) + { + for (std::size_t i = 0; i != column.size(); ++i) + { + column[i] *= diagonal[i]; + } + } + }; + batch_problem.dot = dot; + + ModuleRI::SternheimerRPA::LinearProblem scalar_problem; + scalar_problem.apply = [&diagonal](const Vector& input, Vector& output) { + output = input; + for (std::size_t i = 0; i != output.size(); ++i) + { + output[i] *= diagonal[i]; + } + }; + scalar_problem.dot = dot; + + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 30; + options.residual_tol = 1.0e-11; + Matrix scalar_solutions + = {exact[0], Vector(diagonal.size(), Complex(0.0, 0.0)), Vector(diagonal.size(), Complex(0.0, 0.0))}; + Matrix batch_solutions = scalar_solutions; + std::vector scalar_results; + for (std::size_t column = 0; column != rhs.size(); ++column) + { + scalar_results.push_back(ModuleRI::SternheimerRPA::solve_gmres( + scalar_problem, rhs[column], scalar_solutions[column], options, 2)); + } + + const auto batch_results + = ModuleRI::SternheimerRPA::solve_gmres_batch(batch_problem, rhs, batch_solutions, options, 2); + + ASSERT_EQ(batch_results.size(), scalar_results.size()); + ASSERT_EQ(batch_solutions.size(), scalar_solutions.size()); + ASSERT_FALSE(batch_apply_widths.empty()); + EXPECT_EQ(batch_apply_widths.front(), 3U); + EXPECT_NE(std::find(batch_apply_widths.begin(), batch_apply_widths.end(), 2U), batch_apply_widths.end()); + EXPECT_NE(std::find(batch_apply_widths.begin(), batch_apply_widths.end(), 1U), batch_apply_widths.end()); + for (std::size_t column = 0; column != rhs.size(); ++column) + { + EXPECT_EQ(batch_results[column].converged, scalar_results[column].converged); + EXPECT_EQ(batch_results[column].iterations, scalar_results[column].iterations); + EXPECT_NEAR(batch_results[column].absolute_residual, + scalar_results[column].absolute_residual, + 1.0e-13); + EXPECT_NEAR(batch_results[column].relative_residual, + scalar_results[column].relative_residual, + 1.0e-13); + ASSERT_EQ(batch_solutions[column].size(), scalar_solutions[column].size()); + for (std::size_t i = 0; i != scalar_solutions[column].size(); ++i) + { + EXPECT_NEAR(batch_solutions[column][i].real(), scalar_solutions[column][i].real(), 1.0e-12); + EXPECT_NEAR(batch_solutions[column][i].imag(), scalar_solutions[column][i].imag(), 1.0e-12); + } + } +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_runtime_options_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_runtime_options_test.cpp new file mode 100644 index 00000000000..6fd0fb2db7f --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_runtime_options_test.cpp @@ -0,0 +1,208 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_runtime_options.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abacus_st_smoke.h" + +#include "source_io/module_parameter/input_parameter.h" + +#include +#include +#include +#include +#include + +namespace +{ + +constexpr const char* kTestFlag = "ABACUS_STERNHEIMER_TEST_FLAG"; +constexpr const char* kBatchWidth = "ABACUS_STERNHEIMER_CHANNEL_BATCH_WIDTH"; +constexpr const char* kWeakPreconditioner + = "ABACUS_STERNHEIMER_WEAK_PRECONDITIONER"; +constexpr const char* kWeakResidualTolerance + = "ABACUS_STERNHEIMER_WEAK_RESIDUAL_TOL"; + +class SternheimerRuntimeOptionsTest : public ::testing::Test +{ + protected: + void TearDown() override + { + unsetenv(kTestFlag); + unsetenv(kBatchWidth); + unsetenv(kWeakPreconditioner); + unsetenv(kWeakResidualTolerance); + } +}; + +} // namespace + +TEST_F(SternheimerRuntimeOptionsTest, UsesDefaultWhenEnvironmentIsMissing) +{ + unsetenv(kTestFlag); + + EXPECT_TRUE(ModuleRI::sternheimer_environment_flag(kTestFlag, true)); + EXPECT_FALSE(ModuleRI::sternheimer_environment_flag(kTestFlag, false)); +} + +TEST(SternheimerMolecularCoulomb, RequiresExplicitIsolatedContractAndMatchingBasis) +{ + Input_para input; + EXPECT_FALSE(ModuleRI::validate_sternheimer_molecular_coulomb(input)); + input.sternheimer_ao_file = "ao.dat"; + EXPECT_THROW(ModuleRI::validate_sternheimer_molecular_coulomb(input), std::invalid_argument); + input.sternheimer_molecular_coulomb = "isolated_ri"; + input.basis_type = "lcao"; + input.sternheimer_delta = true; + input.out_sternheimer_librpa = true; + input.symmetry = "-1"; + input.exx_singularity_correction = "limits"; + EXPECT_TRUE(ModuleRI::validate_sternheimer_molecular_coulomb(input)); + for (const auto member: {&Input_para::exx_rotate_abfs, + &Input_para::exx_coul_moment, + &Input_para::out_sternheimer_siab, + &Input_para::cal_force}) + { + input.*member = true; + EXPECT_THROW(ModuleRI::validate_sternheimer_molecular_coulomb(input), std::invalid_argument); + input.*member = false; + } + input.sternheimer_q_index = 1; + EXPECT_THROW(ModuleRI::validate_sternheimer_molecular_coulomb(input), std::invalid_argument); + input.sternheimer_q_index = 0; + input.exx_singularity_correction = "massidda"; + EXPECT_THROW(ModuleRI::validate_sternheimer_molecular_coulomb(input), std::invalid_argument); + input.exx_singularity_correction = "limits"; + input.out_v3d_method = "direct_reciprocal"; + EXPECT_THROW(ModuleRI::validate_sternheimer_molecular_coulomb(input), std::invalid_argument); + input.out_v3d_method = "ewald"; + input.shrink_abfs_pca_thr = 1.e-6; + EXPECT_THROW(ModuleRI::validate_sternheimer_molecular_coulomb(input), std::invalid_argument); +} + +TEST(SternheimerAbacusRuntime, RetainsExplicitProducerState) +{ + Input_para input; + std::vector auxiliary_files = {"auxiliary.orb"}; + std::ofstream log; + const ModuleRI::SternheimerAbacusRuntime runtime = { + input, + 24, + 3, + 8, + 2, + 4, + false, + auxiliary_files, + 4.0, + log}; + + EXPECT_EQ(&runtime.input, &input); + EXPECT_EQ(runtime.nlocal, 24); + EXPECT_EQ(runtime.rank, 3); + EXPECT_EQ(runtime.nproc, 8); + EXPECT_EQ(runtime.kpar_lcao, 2); + EXPECT_EQ(runtime.nthread_per_proc, 4); + EXPECT_FALSE(runtime.deepks_setorb); + EXPECT_EQ(&runtime.abfs_files, &auxiliary_files); + EXPECT_DOUBLE_EQ(runtime.abfs_kmesh_times, 4.0); + EXPECT_EQ(&runtime.log, &log); +} + +TEST_F(SternheimerRuntimeOptionsTest, UsesTwoChannelBatchesByDefaultAndOneForRollback) +{ + unsetenv(kBatchWidth); + EXPECT_EQ(ModuleRI::sternheimer_channel_batch_width(), 2); + + setenv(kBatchWidth, "1", 1); + EXPECT_EQ(ModuleRI::sternheimer_channel_batch_width(), 1); + setenv(kBatchWidth, "8", 1); + EXPECT_EQ(ModuleRI::sternheimer_channel_batch_width(), 8); +} + +TEST_F(SternheimerRuntimeOptionsTest, RejectsInvalidChannelBatchWidths) +{ + for (const char* value: {"", "0", "-1", "four", "65"}) + { + setenv(kBatchWidth, value, 1); + EXPECT_THROW(ModuleRI::sternheimer_channel_batch_width(), std::invalid_argument) << value; + } +} + +TEST_F(SternheimerRuntimeOptionsTest, ParsesRecognizedBooleanTextCaseInsensitively) +{ + for (const char* value: {"1", "true", "TRUE", "on", "On", "yes", "YES"}) + { + setenv(kTestFlag, value, 1); + EXPECT_TRUE(ModuleRI::sternheimer_environment_flag(kTestFlag, false)) << value; + } + for (const char* value: {"0", "false", "FALSE", "off", "Off", "no", "NO"}) + { + setenv(kTestFlag, value, 1); + EXPECT_FALSE(ModuleRI::sternheimer_environment_flag(kTestFlag, true)) << value; + } +} + +TEST_F(SternheimerRuntimeOptionsTest, RejectsEmptyAndUnknownBooleanText) +{ + for (const char* value: {"", "maybe", "2"}) + { + setenv(kTestFlag, value, 1); + EXPECT_THROW(ModuleRI::sternheimer_environment_flag(kTestFlag, true), std::invalid_argument) << value; + } +} + +TEST_F(SternheimerRuntimeOptionsTest, DefaultsWeakSolveToSpectralAndOnePartPerMillion) +{ + unsetenv(kWeakPreconditioner); + unsetenv(kWeakResidualTolerance); + + const auto mode = ModuleRI::sternheimer_weak_preconditioner_mode(); + EXPECT_EQ(mode, ModuleRI::SternheimerWeakPreconditionerMode::Spectral); + EXPECT_STREQ(ModuleRI::sternheimer_weak_preconditioner_name(mode), "fd_spectral"); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_weak_residual_tolerance(), 1.0e-6); +} + +TEST_F(SternheimerRuntimeOptionsTest, ParsesWeakPreconditionerModeCaseInsensitively) +{ + setenv(kWeakPreconditioner, "NoNe", 1); + auto mode = ModuleRI::sternheimer_weak_preconditioner_mode(); + EXPECT_EQ(mode, ModuleRI::SternheimerWeakPreconditionerMode::None); + EXPECT_STREQ(ModuleRI::sternheimer_weak_preconditioner_name(mode), "none"); + + setenv(kWeakPreconditioner, "SpEcTrAl", 1); + mode = ModuleRI::sternheimer_weak_preconditioner_mode(); + EXPECT_EQ(mode, ModuleRI::SternheimerWeakPreconditionerMode::Spectral); + EXPECT_STREQ(ModuleRI::sternheimer_weak_preconditioner_name(mode), "fd_spectral"); + + setenv(kWeakPreconditioner, "Fd_SpEcTrAl", 1); + mode = ModuleRI::sternheimer_weak_preconditioner_mode(); + EXPECT_EQ(mode, ModuleRI::SternheimerWeakPreconditionerMode::Spectral); + EXPECT_STREQ(ModuleRI::sternheimer_weak_preconditioner_name(mode), "fd_spectral"); +} + +TEST_F(SternheimerRuntimeOptionsTest, RejectsUnknownWeakPreconditionerModes) +{ + for (const char* value: {"", "maybe"}) + { + setenv(kWeakPreconditioner, value, 1); + EXPECT_THROW(ModuleRI::sternheimer_weak_preconditioner_mode(), + std::invalid_argument) + << value; + } +} + +TEST_F(SternheimerRuntimeOptionsTest, ParsesFinitePositiveWeakResidualTolerance) +{ + setenv(kWeakResidualTolerance, "1e-8", 1); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_weak_residual_tolerance(), 1.0e-8); + setenv(kWeakResidualTolerance, "0.000001", 1); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_weak_residual_tolerance(), 1.0e-6); +} + +TEST_F(SternheimerRuntimeOptionsTest, RejectsInvalidWeakResidualTolerance) +{ + for (const char* value: {"", "0", "-1e-6", "nan", "inf", "1e-6x"}) + { + setenv(kWeakResidualTolerance, value, 1); + EXPECT_THROW(ModuleRI::sternheimer_weak_residual_tolerance(), + std::invalid_argument) + << value; + } +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_supercell_perturbation_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_supercell_perturbation_test.cpp new file mode 100644 index 00000000000..7322a7dea98 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_supercell_perturbation_test.cpp @@ -0,0 +1,125 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_supercell_perturbation.h" + +#include +#include +#include +#include + +namespace +{ + +using Complex = std::complex; +using Channel = ModuleRI::SternheimerABFBlochGridChannel; + +Channel make_channel(const int atom_index, + const Complex value, + const int atom_local_index = 0) +{ + Channel channel; + channel.channel_index = 2 * atom_index + atom_local_index; + channel.atom_index = atom_index; + channel.atom_local_index = atom_local_index; + channel.type_index = 0; + channel.angular_momentum = 0; + channel.radial_index = 0; + channel.magnetic_index = 0; + channel.label = "H_s"; + channel.potential_r = {value, 2.0 * value}; + channel.max_abs = 2.0 * std::abs(value); + return channel; +} + +} // namespace + +TEST(SternheimerSupercellPerturbation, ParsesCommensurateTranslationSum) +{ + const auto config = ModuleRI::parse_sternheimer_supercell_translation_sum( + "repeats=2x2x2,primitive_q=0.5:0:0,atoms_per_primitive=2,basis_atom=0,channel_within_atom=0"); + EXPECT_EQ(config.repeats, (std::array{2, 2, 2})); + EXPECT_EQ(config.primitive_qpoint, (std::array{0.5, 0.0, 0.0})); + EXPECT_EQ(config.atoms_per_primitive, 2); + EXPECT_EQ(config.basis_atom, 0); + EXPECT_EQ(config.channel_within_atom, 0); + + EXPECT_THROW(ModuleRI::parse_sternheimer_supercell_translation_sum( + "repeats=2x2x2,primitive_q=0.25:0:0,atoms_per_primitive=2,basis_atom=0,channel_within_atom=0"), + std::invalid_argument); +} + +TEST(SternheimerSupercellPerturbation, CombinesEquivalentAtomsWithBlochPhase) +{ + ModuleRI::SternheimerSupercellTranslationSum config; + config.repeats = {2, 1, 1}; + config.primitive_qpoint = {0.5, 0.0, 0.0}; + config.atoms_per_primitive = 2; + config.basis_atom = 0; + config.channel_within_atom = 0; + + const std::vector channels{make_channel(0, Complex(1.0, 0.0)), + make_channel(1, Complex(20.0, 0.0)), + make_channel(2, Complex(3.0, 0.0)), + make_channel(3, Complex(40.0, 0.0))}; + const Channel combined = ModuleRI::combine_sternheimer_supercell_translation_channel(channels, config); + const double inverse_sqrt_two = 1.0 / std::sqrt(2.0); + + EXPECT_EQ(combined.channel_index, 0); + EXPECT_EQ(combined.atom_index, 0); + ASSERT_EQ(combined.potential_r.size(), 2U); + EXPECT_NEAR(combined.potential_r[0].real(), -2.0 * inverse_sqrt_two, 1.0e-14); + EXPECT_NEAR(combined.potential_r[0].imag(), 0.0, 1.0e-14); + EXPECT_NEAR(combined.potential_r[1].real(), -4.0 * inverse_sqrt_two, 1.0e-14); + EXPECT_NEAR(combined.max_abs, 4.0 * inverse_sqrt_two, 1.0e-14); +} + +TEST(SternheimerSupercellPerturbation, RejectsMissingEquivalentAtomChannel) +{ + ModuleRI::SternheimerSupercellTranslationSum config; + config.repeats = {2, 1, 1}; + config.primitive_qpoint = {0.5, 0.0, 0.0}; + config.atoms_per_primitive = 2; + config.basis_atom = 0; + config.channel_within_atom = 0; + + const std::vector channels{make_channel(0, Complex(1.0, 0.0)), + make_channel(1, Complex(20.0, 0.0)), + make_channel(3, Complex(40.0, 0.0))}; + EXPECT_THROW(ModuleRI::combine_sternheimer_supercell_translation_channel(channels, config), + std::invalid_argument); +} + +TEST(SternheimerSupercellPerturbation, CombinesAllPrimitiveChannelsWithStableMetadata) +{ + ModuleRI::SternheimerSupercellTranslationSum config; + config.repeats = {2, 1, 1}; + config.primitive_qpoint = {0.5, 0.0, 0.0}; + config.atoms_per_primitive = 2; + config.basis_atom = 0; + config.channel_within_atom = 0; + + const std::vector channels{ + make_channel(0, Complex(1.0, 0.0), 0), + make_channel(0, Complex(2.0, 0.0), 1), + make_channel(1, Complex(10.0, 0.0), 0), + make_channel(1, Complex(20.0, 0.0), 1), + make_channel(2, Complex(3.0, 0.0), 0), + make_channel(2, Complex(5.0, 0.0), 1), + make_channel(3, Complex(30.0, 0.0), 0), + make_channel(3, Complex(50.0, 0.0), 1), + }; + + const auto combined + = ModuleRI::combine_all_sternheimer_supercell_translation_channels(channels, config); + const double inverse_sqrt_two = 1.0 / std::sqrt(2.0); + + ASSERT_EQ(combined.size(), 4U); + for (std::size_t index = 0; index != combined.size(); ++index) + { + EXPECT_EQ(combined[index].channel_index, static_cast(index)); + EXPECT_EQ(combined[index].atom_index, static_cast(index / 2)); + EXPECT_EQ(combined[index].atom_local_index, static_cast(index % 2)); + } + EXPECT_NEAR(combined[0].potential_r[0].real(), -2.0 * inverse_sqrt_two, 1.0e-14); + EXPECT_NEAR(combined[1].potential_r[0].real(), -3.0 * inverse_sqrt_two, 1.0e-14); + EXPECT_NEAR(combined[2].potential_r[0].real(), -20.0 * inverse_sqrt_two, 1.0e-14); + EXPECT_NEAR(combined[3].potential_r[0].real(), -30.0 * inverse_sqrt_two, 1.0e-14); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_supercell_sector_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_supercell_sector_test.cpp new file mode 100644 index 00000000000..eb4415b74dc --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_supercell_sector_test.cpp @@ -0,0 +1,130 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_supercell_sector.h" + +#include +#include +#include +#include + +namespace +{ + +using Complex = std::complex; + +std::vector sector_vector(const int primitive_orbital, const double k) +{ + const double inverse_sqrt_two = 1.0 / std::sqrt(2.0); + const Complex phase = std::exp(Complex(0.0, 2.0 * std::acos(-1.0) * k)); + std::vector result(4, Complex(0.0, 0.0)); + result[static_cast(primitive_orbital)] = inverse_sqrt_two; + result[static_cast(2 + primitive_orbital)] = phase * inverse_sqrt_two; + return result; +} + +std::vector mixed_sector_vector(const std::array& primitive_coefficients, + const double k) +{ + const double inverse_sqrt_two = 1.0 / std::sqrt(2.0); + const Complex phase = std::exp(Complex(0.0, 2.0 * std::acos(-1.0) * k)); + return {inverse_sqrt_two * primitive_coefficients[0], + inverse_sqrt_two * primitive_coefficients[1], + phase * inverse_sqrt_two * primitive_coefficients[0], + phase * inverse_sqrt_two * primitive_coefficients[1]}; +} + +} // namespace + +TEST(SternheimerSupercellSector, RecoversSelectedTranslationSectorFromCompleteEigensystem) +{ + const std::vector eigenvalues{1.0, 2.0, 4.0, 7.0}; + const std::vector> eigenvectors{ + sector_vector(0, 0.0), + sector_vector(1, 0.0), + sector_vector(0, 0.5), + sector_vector(1, 0.5), + }; + + const auto sector = ModuleRI::recover_sternheimer_supercell_sector( + eigenvalues, eigenvectors, {2, 1, 1}, {0.5, 0.0, 0.0}); + + ASSERT_EQ(sector.eigenvalues.size(), 2U); + EXPECT_NEAR(sector.eigenvalues[0], 4.0, 1.0e-12); + EXPECT_NEAR(sector.eigenvalues[1], 7.0, 1.0e-12); + ASSERT_EQ(sector.coefficients.size(), 2U); + EXPECT_LT(sector.max_orthonormality_error, 1.0e-12); + EXPECT_LT(sector.max_full_space_residual, 1.0e-12); + for (const auto& coefficients: sector.coefficients) + { + ASSERT_EQ(coefficients.size(), 4U); + EXPECT_NEAR(coefficients[0].real(), -coefficients[2].real(), 1.0e-12); + EXPECT_NEAR(coefficients[1].real(), -coefficients[3].real(), 1.0e-12); + } +} + +TEST(SternheimerSupercellSector, RejectsIncompleteSquareEigensystem) +{ + const std::vector eigenvalues{1.0, 2.0, 3.0}; + const std::vector> eigenvectors{ + std::vector(4, Complex(0.0, 0.0)), + std::vector(4, Complex(0.0, 0.0)), + std::vector(4, Complex(0.0, 0.0)), + }; + EXPECT_THROW(ModuleRI::recover_sternheimer_supercell_sector( + eigenvalues, eigenvectors, {2, 1, 1}, {0.0, 0.0, 0.0}), + std::invalid_argument); +} + +TEST(SternheimerSupercellSector, RecoversDenseComplexNonunitarySectorWithoutLayoutAmbiguity) +{ + const std::vector eigenvalues{1.0, 3.0, 5.0, 8.0}; + const std::vector> eigenvectors{ + mixed_sector_vector({Complex(1.0, 0.25), Complex(0.4, -0.3)}, 0.0), + mixed_sector_vector({Complex(-0.2, 0.7), Complex(1.3, 0.1)}, 0.0), + mixed_sector_vector({Complex(0.8, -0.4), Complex(0.1, 0.9)}, 0.5), + mixed_sector_vector({Complex(-0.6, 0.2), Complex(1.1, -0.5)}, 0.5), + }; + + const auto sector = ModuleRI::recover_sternheimer_supercell_sector( + eigenvalues, eigenvectors, {2, 1, 1}, {0.5, 0.0, 0.0}); + + ASSERT_EQ(sector.eigenvalues.size(), 2U); + EXPECT_NEAR(sector.eigenvalues[0], 5.0, 1.0e-11); + EXPECT_NEAR(sector.eigenvalues[1], 8.0, 1.0e-11); + EXPECT_LT(sector.max_orthonormality_error, 1.0e-11); + EXPECT_LT(sector.max_full_space_residual, 1.0e-11); + for (const auto& coefficients: sector.coefficients) + { + ASSERT_EQ(coefficients.size(), 4U); + EXPECT_NEAR(std::abs(coefficients[0] + coefficients[2]), 0.0, 1.0e-11); + EXPECT_NEAR(std::abs(coefficients[1] + coefficients[3]), 0.0, 1.0e-11); + } +} + +TEST(SternheimerSupercellSector, RecoversEveryCommensurateSectorInPrimitiveKGridOrder) +{ + const std::vector eigenvalues{1.0, 2.0, 4.0, 7.0}; + const std::vector> eigenvectors{ + sector_vector(0, 0.0), + sector_vector(1, 0.0), + sector_vector(0, 0.5), + sector_vector(1, 0.5), + }; + + const auto sectors = ModuleRI::recover_all_sternheimer_supercell_sectors( + eigenvalues, eigenvectors, {2, 1, 1}); + + ASSERT_EQ(sectors.size(), 2U); + EXPECT_EQ(sectors[0].kpoint, + (ModuleRI::SternheimerReducedKPoint{0.0, 0.0, 0.0})); + EXPECT_EQ(sectors[1].kpoint, + (ModuleRI::SternheimerReducedKPoint{0.5, 0.0, 0.0})); + ASSERT_EQ(sectors[0].sector.eigenvalues.size(), 2U); + ASSERT_EQ(sectors[1].sector.eigenvalues.size(), 2U); + EXPECT_NEAR(sectors[0].sector.eigenvalues[0], 1.0, 1.0e-12); + EXPECT_NEAR(sectors[0].sector.eigenvalues[1], 2.0, 1.0e-12); + EXPECT_NEAR(sectors[1].sector.eigenvalues[0], 4.0, 1.0e-12); + EXPECT_NEAR(sectors[1].sector.eigenvalues[1], 7.0, 1.0e-12); + EXPECT_LT(sectors[0].sector.max_orthonormality_error, 1.0e-12); + EXPECT_LT(sectors[1].sector.max_orthonormality_error, 1.0e-12); + EXPECT_LT(sectors[0].sector.max_full_space_residual, 1.0e-12); + EXPECT_LT(sectors[1].sector.max_full_space_residual, 1.0e-12); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_wavefunction_diagnostic_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_wavefunction_diagnostic_test.cpp new file mode 100644 index 00000000000..25b740340db --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_wavefunction_diagnostic_test.cpp @@ -0,0 +1,139 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_wavefunction_diagnostic.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Diagnostic = ModuleRI::SternheimerWavefunctionDiagnostic; +using Complex = Diagnostic::Complex; + +} + +TEST(SternheimerWavefunctionDiagnostic, SelectorMatchesOnlyOneEquation) +{ + Diagnostic::Selector selector; + selector.iq = 2; + selector.ik_full = 3; + selector.ib = 0; + selector.ifrequency = 1; + selector.channel = 4; + + EXPECT_TRUE(selector.matches(2, 3, 0, 1, 4)); + EXPECT_FALSE(selector.matches(1, 3, 0, 1, 4)); + EXPECT_FALSE(selector.matches(2, 2, 0, 1, 4)); + EXPECT_FALSE(selector.matches(2, 3, 1, 1, 4)); + EXPECT_FALSE(selector.matches(2, 3, 0, 2, 4)); + EXPECT_FALSE(selector.matches(2, 3, 0, 1, 5)); +} + +TEST(SternheimerWavefunctionDiagnostic, ParsesCompleteSelectorConfiguration) +{ + const Diagnostic::Configuration config + = Diagnostic::parse_configuration("iq=2,ik=0,ib=0,ifreq=1,channel=4,out=wavefunction.bin"); + EXPECT_TRUE(config.selector.matches(2, 0, 0, 1, 4)); + EXPECT_EQ(config.output_filename, "wavefunction.bin"); + + EXPECT_THROW(Diagnostic::parse_configuration("iq=2,ik=0,ib=0,ifreq=1,channel=4"), + std::invalid_argument); + EXPECT_THROW(Diagnostic::parse_configuration("iq=2,ik=0,ib=0,ifreq=1,channel=4,out=a,iq=3"), + std::invalid_argument); + EXPECT_THROW(Diagnostic::parse_configuration("iq=0,ik=0,ib=0,ifreq=1,channel=4,out=a"), + std::invalid_argument); +} + +TEST(SternheimerWavefunctionDiagnostic, BinaryRecordRoundTripsNamedComplexVectors) +{ + Diagnostic::Record record; + record.metadata.nx = 2; + record.metadata.ny = 1; + record.metadata.nz = 1; + record.metadata.iq = 2; + record.metadata.ik_full = 3; + record.metadata.ib = 0; + record.metadata.ifrequency = 1; + record.metadata.channel = 4; + record.metadata.lattice = {2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0}; + record.metadata.qpoint = {0.25, 0.0, 0.0}; + record.metadata.source_kpoint = {0.0, 0.0, 0.0}; + record.metadata.target_kpoint = {0.25, 0.0, 0.0}; + record.metadata.omega_ha = 0.5; + record.metadata.omega_ry = 1.0; + record.metadata.volume_element = 0.125; + record.metadata.reference_eigenvalue_ry = -0.75; + record.metadata.weighted_occupation = 0.03125; + record.metadata.rhs_norm = 0.625; + record.metadata.solver_relative_residual = 1.0e-9; + record.metadata.equation_relative_residual = 2.0e-9; + record.metadata.diagonal_branch_element = Complex(-0.125, 0.03125); + record.vectors.push_back({"psi0", {Complex(1.0, 2.0), Complex(3.0, 4.0)}}); + record.vectors.push_back({"delta_psi", {Complex(-1.0, 0.5), Complex(0.25, -0.75)}}); + + const std::string filename = "sternheimer_wavefunction_diagnostic_test.bin"; + Diagnostic::write(filename, record); + const Diagnostic::Record reread = Diagnostic::read(filename); + std::remove(filename.c_str()); + + EXPECT_EQ(reread.metadata.nx, record.metadata.nx); + EXPECT_EQ(reread.metadata.ny, record.metadata.ny); + EXPECT_EQ(reread.metadata.nz, record.metadata.nz); + EXPECT_EQ(reread.metadata.iq, record.metadata.iq); + EXPECT_EQ(reread.metadata.ik_full, record.metadata.ik_full); + EXPECT_EQ(reread.metadata.ib, record.metadata.ib); + EXPECT_EQ(reread.metadata.ifrequency, record.metadata.ifrequency); + EXPECT_EQ(reread.metadata.channel, record.metadata.channel); + EXPECT_EQ(reread.metadata.lattice, record.metadata.lattice); + EXPECT_EQ(reread.metadata.qpoint, record.metadata.qpoint); + EXPECT_EQ(reread.metadata.source_kpoint, record.metadata.source_kpoint); + EXPECT_EQ(reread.metadata.target_kpoint, record.metadata.target_kpoint); + EXPECT_DOUBLE_EQ(reread.metadata.omega_ha, record.metadata.omega_ha); + EXPECT_DOUBLE_EQ(reread.metadata.omega_ry, record.metadata.omega_ry); + EXPECT_DOUBLE_EQ(reread.metadata.volume_element, record.metadata.volume_element); + EXPECT_DOUBLE_EQ(reread.metadata.reference_eigenvalue_ry, record.metadata.reference_eigenvalue_ry); + EXPECT_DOUBLE_EQ(reread.metadata.weighted_occupation, record.metadata.weighted_occupation); + EXPECT_DOUBLE_EQ(reread.metadata.rhs_norm, record.metadata.rhs_norm); + EXPECT_DOUBLE_EQ(reread.metadata.solver_relative_residual, record.metadata.solver_relative_residual); + EXPECT_DOUBLE_EQ(reread.metadata.equation_relative_residual, record.metadata.equation_relative_residual); + EXPECT_EQ(reread.metadata.diagonal_branch_element, record.metadata.diagonal_branch_element); + EXPECT_EQ(reread.vectors, record.vectors); +} + +TEST(SternheimerWavefunctionDiagnostic, RejectsVectorWithWrongGridSize) +{ + Diagnostic::Record record; + record.metadata.nx = 2; + record.metadata.ny = 2; + record.metadata.nz = 1; + record.vectors.push_back({"delta_psi", {Complex(1.0, 0.0)}}); + EXPECT_THROW(Diagnostic::write("unused.bin", record), std::invalid_argument); +} + +TEST(SternheimerWavefunctionDiagnostic, RejectsDeclaredVectorSizeBeforeAllocation) +{ + Diagnostic::Record record; + record.metadata.nx = 2; + record.metadata.ny = 1; + record.metadata.nz = 1; + record.vectors.push_back({"psi0", {Complex(1.0, 0.0), Complex(2.0, 0.0)}}); + + const std::string filename = "sternheimer_wavefunction_diagnostic_bad_size_test.bin"; + Diagnostic::write(filename, record); + + std::fstream file(filename.c_str(), std::ios::binary | std::ios::in | std::ios::out); + ASSERT_TRUE(file.good()); + constexpr std::streamoff vector_size_offset + = 2 * sizeof(std::int32_t) + 8 * sizeof(std::int32_t) + 28 * sizeof(double) + + sizeof(std::int32_t) + sizeof(std::int32_t) + 4; + file.seekp(vector_size_offset); + const std::int64_t invalid_size = 3; + file.write(reinterpret_cast(&invalid_size), sizeof(invalid_size)); + file.close(); + + EXPECT_THROW(Diagnostic::read(filename), std::runtime_error); + std::remove(filename.c_str()); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_augmented_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_augmented_test.cpp new file mode 100644 index 00000000000..7f699fe36f9 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_augmented_test.cpp @@ -0,0 +1,875 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using Blocks = ModuleRI::SternheimerWeakAugmented; +using Worker = Blocks::Worker; +using Complex = Blocks::Complex; +using Vector = Blocks::Vector; +using Matrix = std::vector; + +Complex dot(const Vector& a, const Vector& b) +{ + Complex value = 0.0; + for (std::size_t i = 0; i < a.size(); ++i) + { + value += std::conj(a[i]) * b[i]; + } + return value; +} + +Vector multiply(const Matrix& a, const Vector& x) +{ + Vector y(a.size(), 0.0); + for (std::size_t i = 0; i < a.size(); ++i) + { + for (std::size_t j = 0; j < x.size(); ++j) + { + y[i] += a[i][j] * x[j]; + } + } + return y; +} + +// Independent small dense reference, deliberately not the helper's LAPACK path. +Vector direct_solve(Matrix a, Vector b) +{ + for (std::size_t k = 0; k < b.size(); ++k) + { + std::size_t pivot = k; + for (std::size_t i = k + 1; i < b.size(); ++i) + { + if (std::abs(a[i][k]) > std::abs(a[pivot][k])) + { + pivot = i; + } + } + if (std::abs(a[pivot][k]) < 1.0e-14) + { + throw std::runtime_error("singular test reference"); + } + std::swap(a[k], a[pivot]); + std::swap(b[k], b[pivot]); + for (std::size_t i = k + 1; i < b.size(); ++i) + { + const Complex factor = a[i][k] / a[k][k]; + for (std::size_t j = k + 1; j < b.size(); ++j) + { + a[i][j] -= factor * a[k][j]; + } + b[i] -= factor * b[k]; + } + } + for (std::size_t i = b.size(); i-- > 0;) + { + for (std::size_t j = i + 1; j < b.size(); ++j) + { + b[i] -= a[i][j] * b[j]; + } + b[i] /= a[i][i]; + } + return b; +} + +void expect_vector(const Vector& actual, const Vector& expected, const double tolerance = 2.0e-11) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t i = 0; i < actual.size(); ++i) + { + EXPECT_NEAR(std::abs(actual[i] - expected[i]), 0.0, tolerance) << "entry " << i; + } +} + +Vector probe(const std::size_t n, const double offset) +{ + Vector v(n); + for (std::size_t i = 0; i < n; ++i) + { + v[i] = Complex(std::sin(0.8 * i + offset), std::cos(0.4 * i - offset)); + } + return v; +} + +struct Model +{ + static constexpr int nfine = 8; + int nocc = 1; + int nv = 2; + int nc = 4; + Matrix fine_h; + Matrix e; // Columns in the independent fine Euclidean space. + Matrix t; // Columns [F,W]. + Matrix h; + Matrix s; + Matrix ce; + Blocks::Data data; + + Model() + { + fine_h.assign(nfine, Vector(nfine, 0.0)); + for (int i = 0; i < nfine; ++i) + { + fine_h[i][i] = 2.0 + 0.7 * i; + for (int j = 0; j < i; ++j) + { + fine_h[i][j] = Complex(0.09 * std::cos(i + 2 * j), 0.07 * std::sin(2 * i + j)); + fine_h[j][i] = std::conj(fine_h[i][j]); + } + } + for (int col = 0; col < nc; ++col) + { + Vector v(nfine); + for (int i = 0; i < nfine; ++i) + { + v[i] = Complex(0.11 * std::sin((i + 1) * (col + 2)), + 0.13 * std::cos((i + 2) * (col + 1))); + } + v[3 + col] += 1.0; + for (const auto& previous : e) + { + const Complex overlap = dot(previous, v); + for (int i = 0; i < nfine; ++i) + { + v[i] -= overlap * previous[i]; + } + } + const double norm = std::sqrt(dot(v, v).real()); + for (auto& value : v) + { + value /= norm; + } + e.push_back(v); + } + const int nu = nocc + nv; + data.nocc = nocc; + data.nvirtual = nv; + data.ncoarse = nc; + data.hu.resize(nu * nu); + data.l.resize(nu * nc); + data.k.resize(nu * nc); + for (int j = 0; j < nu; ++j) + { + for (int i = 0; i < nu; ++i) + { + data.hu[i + nu * j] = fine_h[i][j]; + } + } + ce.assign(nc, Vector(nc)); + for (int j = 0; j < nc; ++j) + { + const Vector he = multiply(fine_h, e[j]); + for (int i = 0; i < nu; ++i) + { + data.l[i + nu * j] = e[j][i]; + data.k[i + nu * j] = he[i]; + } + for (int i = 0; i < nc; ++i) + { + ce[i][j] = dot(e[i], he); + } + } + for (int i = 0; i < nv; ++i) + { + Vector f(nfine, 0.0); + f[nocc + i] = 1.0; + t.push_back(f); + } + for (const auto& column : e) + { + Vector w = column; + std::fill(w.begin(), w.begin() + nu, Complex(0.0)); + t.push_back(w); + } + h.assign(t.size(), Vector(t.size())); + s = h; + for (std::size_t j = 0; j < t.size(); ++j) + { + const Vector ht = multiply(fine_h, t[j]); + for (std::size_t i = 0; i < t.size(); ++i) + { + h[i][j] = dot(t[i], ht); + s[i][j] = dot(t[i], t[j]); + } + } + } + + Blocks::Apply callback() const + { + const Matrix matrix = ce; + return [matrix](const Vector& x, Vector& y) { y = multiply(matrix, x); }; + } + + Matrix shifted(const double eps, const double omega) const + { + Matrix result = h; + for (std::size_t i = 0; i < h.size(); ++i) + { + for (std::size_t j = 0; j < h.size(); ++j) + { + result[i][j] -= Complex(eps, -omega) * s[i][j]; + } + } + return result; + } +}; + +Blocks::Vertices vertices(const Model& model) +{ + Blocks::Vertices result; + result.f = probe(model.nv, 0.3); + result.w = probe(model.nc, -0.4); + return result; +} + +Vector joined(const Blocks::Vertices& g) +{ + Vector result = g.f; + result.insert(result.end(), g.w.begin(), g.w.end()); + return result; +} + +} // namespace + +TEST(SternheimerWeakAugmented, MetricAndHamiltonianMatchIndependentFineSpace) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + Worker worker(blocks, model.callback(), 0.6, 0.2); + const Vector v = probe(model.nv + model.nc, 0.8); + Vector output; + blocks->apply_metric(v, output); + expect_vector(output, multiply(model.s, v)); + EXPECT_GT(std::abs(model.s[model.nv][model.nv] - 1.0), 0.01); + EXPECT_GT(std::abs(model.s[model.nv][model.nv + 1]), 1.0e-4); + EXPECT_GT(std::abs(model.h[0][model.nv].imag()), 1.0e-4); + worker.apply_hamiltonian(v, output); + expect_vector(output, multiply(model.h, v)); + worker.apply_shifted(v, output); + expect_vector(output, multiply(model.shifted(0.6, 0.2), v)); + output = v; + worker.apply_shifted(output, output); + expect_vector(output, multiply(model.shifted(0.6, 0.2), v)); + output = v; + blocks->apply_metric(output, output); + expect_vector(output, multiply(model.s, v)); +} + +TEST(SternheimerWeakAugmented, ComplementInverseSqrtMatchesNonDiagonalMetricReference) +{ + Blocks::Data data; + data.nocc = 0; + data.nvirtual = 2; + data.ncoarse = 2; + data.hu = {Complex(2.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(3.0, 0.0)}; + data.l = {Complex(0.4, 0.0), Complex(0.2, 0.0), Complex(0.1, 0.0), Complex(0.3, 0.0)}; + data.k.assign(4, Complex(0.0, 0.0)); + + const Blocks blocks(data); + const Vector input = {Complex(0.7, -0.2), Complex(-0.1, 0.6)}; + Vector actual; + blocks.apply_complement_inverse_sqrt(input, actual); + + // The reference evaluates f(G) from the closed-form 2x2 Hermitian + // spectrum, where G=L L* and f(lambda)=((1-lambda)^(-1/2)-1)/lambda. + const double g00 = 0.17; + const double g11 = 0.13; + const double g01 = 0.11; + const double trace = g00 + g11; + const double gap = std::sqrt((g00 - g11) * (g00 - g11) + 4.0 * g01 * g01); + const double low = 0.5 * (trace - gap); + const double high = 0.5 * (trace + gap); + const auto factor = [](const double lambda) { + const double root = std::sqrt(1.0 - lambda); + return 1.0 / (root * (1.0 + root)); + }; + const double flow = factor(low); + const double fhigh = factor(high); + const double slope = (fhigh - flow) / (high - low); + const double intercept = flow - slope * low; + + const Vector lx = {0.4 * input[0] + 0.1 * input[1], 0.2 * input[0] + 0.3 * input[1]}; + const Vector flx = {slope * (g00 * lx[0] + g01 * lx[1]) + intercept * lx[0], + slope * (g01 * lx[0] + g11 * lx[1]) + intercept * lx[1]}; + const Vector expected = {input[0] + 0.4 * flx[0] + 0.2 * flx[1], + input[1] + 0.1 * flx[0] + 0.3 * flx[1]}; + expect_vector(actual, expected, 3.0e-12); +} + +TEST(SternheimerWeakAugmented, HermitianAdjointsAndFrequencySignArePreserved) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + const Vector a = probe(model.h.size(), 0.2); + const Vector b = probe(model.h.size(), -0.6); + Worker positive(blocks, model.callback(), 0.6, 0.7); + Worker negative(blocks, model.callback(), 0.6, -0.7); + Vector ha, hb; + positive.apply_hamiltonian(a, ha); + positive.apply_hamiltonian(b, hb); + EXPECT_NEAR(std::abs(dot(a, hb) - dot(ha, b)), 0.0, 1.0e-11); + positive.apply_shifted(b, hb); + negative.apply_shifted(a, ha); + EXPECT_NEAR(std::abs(dot(a, hb) - dot(ha, b)), 0.0, 1.0e-11); +} + +TEST(SternheimerWeakAugmented, SchurRhsReconstructionAndSolveMatchDenseAtMultipleFrequencies) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + const auto g = vertices(model); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 20; + options.residual_tol = 1.0e-11; + for (const double omega : {0.0, 0.03, 0.7, 3.0}) + { + SCOPED_TRACE(omega); + Worker worker(blocks, model.callback(), 0.6, omega); + const Matrix shifted = model.shifted(0.6, omega); + Matrix av(model.nv, Vector(model.nv)); + for (int i = 0; i < model.nv; ++i) + { + std::copy_n(shifted[i].begin(), model.nv, av[i].begin()); + } + const Vector x = probe(model.nc, 0.1); + Vector bx(model.nv, 0.0); + Vector expected(model.nc, 0.0); + for (int i = 0; i < model.nv; ++i) + { + for (int j = 0; j < model.nc; ++j) + { + bx[i] += shifted[i][model.nv + j] * x[j]; + } + } + const Vector inverse_bx = direct_solve(av, bx); + const Vector inverse_g = direct_solve(av, g.f); + Vector expected_rhs = g.w; + for (int i = 0; i < model.nc; ++i) + { + expected_rhs[i] = -g.w[i]; + for (int j = 0; j < model.nc; ++j) + { + expected[i] += shifted[model.nv + i][model.nv + j] * x[j]; + } + for (int j = 0; j < model.nv; ++j) + { + expected[i] -= shifted[model.nv + i][j] * inverse_bx[j]; + expected_rhs[i] += shifted[model.nv + i][j] * inverse_g[j]; + } + } + Vector actual; + worker.apply_schur(x, actual); + expect_vector(actual, expected); + expect_vector(worker.schur_rhs(g), expected_rhs); + Vector rhs = joined(g); + for (auto& value : rhs) + { + value = -value; + } + const Vector direct = direct_solve(shifted, rhs); + const Vector direct_x(direct.begin() + model.nv, direct.end()); + expect_vector(worker.reconstruct(g, direct_x), direct); + const auto result = worker.solve(g, options, model.nc); + ASSERT_TRUE(result.converged); + EXPECT_TRUE(result.schur.converged); + EXPECT_LE(result.relative_residual, options.residual_tol); + expect_vector(result.coefficients, direct, 2.0e-10); + const Complex response = dot(joined(g), result.coefficients); + EXPECT_LT(response.real(), 0.0); + if (omega > 0.0) + { + EXPECT_GT(response.imag(), 0.0); // -g*(H-eps*S+i*omega*S)^(-1)g. + } + else + { + EXPECT_NEAR(response.imag(), 0.0, 1.0e-11); + } + } +} + +TEST(SternheimerWeakAugmented, SolveInvokesNormalizedPhysicalPreconditioner) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + int calls = 0; + Blocks::Apply precondition = [&calls](const Vector& input, Vector& output) { + ++calls; + output = input; + }; + Worker worker(blocks, + model.callback(), + 0.6, + 0.2, + 512ULL * 1024 * 1024, + std::move(precondition)); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 20; + options.residual_tol = 1.0e-11; + + const auto result = worker.solve(vertices(model), options, model.nc); + + EXPECT_GT(calls, 0); + EXPECT_TRUE(result.converged); +} + +TEST(SternheimerWeakAugmented, PreconditionedSolveMatchesIndependentDenseReference) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + Blocks::Apply precondition = [](const Vector& input, Vector& output) { + output.resize(input.size()); + for (std::size_t i = 0; i < input.size(); ++i) + { + output[i] = input[i] / (1.3 + 0.4 * i); + } + }; + Worker worker(blocks, + model.callback(), + 0.6, + 0.7, + 512ULL * 1024 * 1024, + std::move(precondition)); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 20; + options.residual_tol = 1.0e-11; + const auto g = vertices(model); + Vector rhs = joined(g); + for (auto& value : rhs) + { + value = -value; + } + + const auto result = worker.solve(g, options, model.nc); + + ASSERT_TRUE(result.converged) << result.relative_residual; + expect_vector(result.coefficients, direct_solve(model.shifted(0.6, 0.7), rhs), 2.0e-10); +} + +TEST(SternheimerWeakAugmented, NormalizedPhysicalPreconditionerDoesNotApplyMetricFactors) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + Blocks::Apply diagonal = [](const Vector& input, Vector& output) { + output.resize(input.size()); + for (std::size_t i = 0; i < input.size(); ++i) + { + output[i] = input[i] / (1.7 + 0.3 * i); + } + }; + Worker worker(blocks, + model.callback(), + 0.6, + 0.2, + 512ULL * 1024 * 1024, + diagonal); + const Vector input = probe(model.nc, 0.45); + Vector expected; + diagonal(input, expected); + + Vector actual; + worker.apply_normalized_preconditioner(input, actual); + + expect_vector(actual, expected); +} + +TEST(SternheimerWeakAugmented, RejectsMalformedPreconditionerOutput) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + const auto g = vertices(model); + ModuleRI::SternheimerRPA::SolverOptions options; + Worker wrong_size(blocks, + model.callback(), + 0.6, + 0.2, + 512ULL * 1024 * 1024, + [](const Vector&, Vector& output) { output.assign(1, 0.0); }); + EXPECT_THROW(wrong_size.solve(g, options, model.nc), std::invalid_argument); + + Worker nonfinite(blocks, + model.callback(), + 0.6, + 0.2, + 512ULL * 1024 * 1024, + [](const Vector& input, Vector& output) { + output.assign(input.size(), std::numeric_limits::quiet_NaN()); + }); + EXPECT_THROW(nonfinite.solve(g, options, model.nc), std::invalid_argument); +} + +TEST(SternheimerWeakAugmented, ProjectsBothVerticesAndReconstructsFineResponseWithoutWeights) +{ + const Model model; + const Blocks blocks(model.data); + const Vector perturbation = probe(Model::nfine, 0.6); + const Vector gu(perturbation.begin(), perturbation.begin() + model.nocc + model.nv); + Vector ge; + for (const auto& e : model.e) + { + ge.push_back(dot(e, perturbation)); + } + Vector expected; + for (const auto& t : model.t) + { + expected.push_back(dot(t, perturbation)); + } + expect_vector(joined(blocks.project_vertices(gu, ge)), expected); + const Vector coefficients = probe(model.t.size(), -0.2); + const auto expansion = blocks.expand_coordinates(coefficients); + Vector fine(Model::nfine, 0.0); + Vector direct(Model::nfine, 0.0); + std::copy(expansion.u.begin(), expansion.u.end(), fine.begin()); + for (int i = 0; i < Model::nfine; ++i) + { + for (int j = 0; j < model.nc; ++j) + { + fine[i] += model.e[j][i] * expansion.e[j]; + } + for (std::size_t j = 0; j < model.t.size(); ++j) + { + direct[i] += model.t[j][i] * coefficients[j]; + } + } + expect_vector(fine, direct); + EXPECT_NEAR(std::abs(fine[0]), 0.0, 1.0e-12); +} + +TEST(SternheimerWeakAugmented, SingularIndefiniteAndNearlySingularMetricAreDiagnosed) +{ + Blocks::Data data; + data.nvirtual = 1; + data.ncoarse = 2; + data.hu = {2.0}; + data.k = {0.3, Complex(0.0, 0.2)}; + // Neither column has norm one; their coherent combination is null in W. + data.l = {0.6, Complex(0.0, 0.8)}; + try + { + const Blocks blocks(data); + FAIL() << "singular metric accepted"; + } + catch (const std::domain_error& error) + { + EXPECT_NE(std::string(error.what()).find("metric"), std::string::npos); + EXPECT_NE(std::string(error.what()).find("singular"), std::string::npos); + } + data.l[0] = 0.7; + EXPECT_THROW(Blocks{data}, std::domain_error); + data.l = {std::sqrt(1.0 - 1.0e-13), 0.0}; + EXPECT_THROW(Blocks{data}, std::domain_error); + data.l = {0.6, 0.0}; + const Blocks regular(data); + EXPECT_NEAR(regular.minimum_metric_eigenvalue(), 0.64, 1.0e-13); +} + +TEST(SternheimerWeakAugmented, RejectsSingularVirtualResolventWithoutEnergyShift) +{ + Blocks::Data data; + data.nvirtual = 2; + data.hu = {1.0, Complex(0.0, -1.0), Complex(0.0, 1.0), 1.0}; + const auto blocks = std::make_shared(data); + EXPECT_THROW((Worker(blocks, {}, 0.0, 0.0)), std::domain_error); + Worker nonsingular(blocks, {}, 0.0, 0.2); + const Blocks::Vertices g{{1.0, Complex(0.3, 0.2)}, {}}; + const auto result = nonsingular.solve(g, ModuleRI::SternheimerRPA::SolverOptions{}, 2); + ASSERT_TRUE(result.converged); + expect_vector(result.coefficients, + direct_solve({{Complex(1.0, 0.2), Complex(0.0, 1.0)}, + {Complex(0.0, -1.0), Complex(1.0, 0.2)}}, + {-g.f[0], -g.f[1]})); +} + +TEST(SternheimerWeakAugmented, PureComplementAndZeroRightHandSideAreSupported) +{ + Blocks::Data data; + data.ncoarse = 2; + const auto blocks = std::make_shared(data); + const Matrix ce{{2.0, Complex(0.2, 0.1)}, {Complex(0.2, -0.1), 3.0}}; + Worker worker(blocks, [ce](const Vector& x, Vector& y) { y = multiply(ce, x); }, 0.5, 0.1); + const auto zero = worker.solve({{}, {0.0, 0.0}}, ModuleRI::SternheimerRPA::SolverOptions{}, 2); + EXPECT_TRUE(zero.converged); + expect_vector(zero.coefficients, {0.0, 0.0}); + EXPECT_EQ(zero.absolute_residual, 0.0); + const auto result = worker.solve({{}, {1.0, Complex(0.1, 0.3)}}, + ModuleRI::SternheimerRPA::SolverOptions{}, 2); + ASSERT_TRUE(result.converged); + expect_vector(result.coefficients, + direct_solve({{Complex(1.5, 0.1), Complex(0.2, 0.1)}, + {Complex(0.2, -0.1), Complex(2.5, 0.1)}}, + {-1.0, Complex(-0.1, -0.3)})); +} + +TEST(SternheimerWeakAugmented, NumericallyUnresolvedVirtualResolventIsDiagnosed) +{ + Blocks::Data data; + data.nvirtual = 2; + data.hu = {1.0, 0.0, 0.0, 1.0e-20}; + const auto blocks = std::make_shared(data); + EXPECT_THROW((Worker(blocks, {}, 0.0, 0.0)), std::domain_error); + // A genuinely small but resolved energy is not replaced by a shifted one. + data.hu[3] = 1.0e-8; + const auto resolved = std::make_shared(data); + Worker worker(resolved, {}, 0.0, 0.0); + expect_vector(worker.reconstruct({{0.0, 1.0e-8}, {}}, {}), {0.0, -1.0}); +} + +TEST(SternheimerWeakAugmented, NonconvergenceReturnsActualFullBlockResidual) +{ + const Model model; + const auto blocks = std::make_shared(model.data); + Worker worker(blocks, model.callback(), 0.6, 0.3); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 1; + options.residual_tol = 1.0e-14; + const auto g = vertices(model); + const auto result = worker.solve(g, options, 1); + EXPECT_FALSE(result.converged); + EXPECT_FALSE(result.schur.converged); + Vector residual = multiply(model.shifted(0.6, 0.3), result.coefficients); + const Vector source = joined(g); + for (std::size_t i = 0; i < residual.size(); ++i) + { + residual[i] += source[i]; + } + EXPECT_NEAR(result.absolute_residual, std::sqrt(dot(residual, residual).real()), 1.0e-12); + EXPECT_NEAR(result.relative_residual, + std::sqrt(dot(residual, residual).real() / dot(source, source).real()), 1.0e-12); +} + +TEST(SternheimerWeakAugmented, RejectsMalformedDataVectorsCallbacksAndNonfiniteValues) +{ + const Model model; + auto bad = model.data; + bad.l.pop_back(); + EXPECT_THROW(Blocks{bad}, std::invalid_argument); + bad = model.data; + bad.k[0] = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(Blocks{bad}, std::invalid_argument); + bad = model.data; + bad.hu[1] += Complex(0.0, 0.5); + EXPECT_THROW(Blocks{bad}, std::invalid_argument); + bad = model.data; + bad.nocc = -1; + EXPECT_THROW(Blocks{bad}, std::invalid_argument); + EXPECT_THROW(Blocks{Blocks::Data{}}, std::invalid_argument); + const auto blocks = std::make_shared(model.data); + EXPECT_THROW((Worker(nullptr, model.callback(), 0.6, 0.1)), std::invalid_argument); + EXPECT_THROW((Worker(blocks, {}, 0.6, 0.1)), std::invalid_argument); + EXPECT_THROW((Worker(blocks, model.callback(), std::numeric_limits::infinity(), 0.1)), + std::invalid_argument); + Worker worker(blocks, model.callback(), 0.6, 0.1); + Vector output; + EXPECT_THROW(worker.apply_shifted({}, output), std::invalid_argument); + EXPECT_THROW(worker.schur_rhs({{}, {}}), std::invalid_argument); + EXPECT_THROW(blocks->project_vertices({}, {}), std::invalid_argument); + Vector bad_vector = probe(model.nc, 0.2); + bad_vector[0] = Complex(0.0, std::numeric_limits::infinity()); + EXPECT_THROW(worker.apply_schur(bad_vector, output), std::invalid_argument); + Worker wrong_size(blocks, [](const Vector&, Vector& y) { y.assign(1, 0.0); }, 0.6, 0.1); + EXPECT_THROW(wrong_size.apply_schur(probe(model.nc, 0.2), output), std::invalid_argument); + Worker nonfinite(blocks, [](const Vector& x, Vector& y) { + y.assign(x.size(), std::numeric_limits::quiet_NaN()); + }, 0.6, 0.1); + EXPECT_THROW(nonfinite.apply_schur(probe(model.nc, 0.2), output), std::invalid_argument); + ModuleRI::SternheimerRPA::SolverOptions invalid; + invalid.residual_tol = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(worker.solve(vertices(model), invalid, 3), std::invalid_argument); + EXPECT_THROW(worker.solve(vertices(model), ModuleRI::SternheimerRPA::SolverOptions{}, 0), + std::invalid_argument); +} + +TEST(SternheimerWeakAugmented, EnforcesPayloadBoundsAndOwnsImmutableInputCopies) +{ + Model model; + EXPECT_THROW((Blocks(model.data, 1)), std::length_error); + const auto blocks = std::make_shared(model.data); + EXPECT_THROW((Worker(blocks, model.callback(), 0.6, 0.1, 1)), std::length_error); + Worker first(blocks, model.callback(), 0.6, 0.1); + Worker second(blocks, model.callback(), 0.6, 0.8); + const Vector v = probe(model.h.size(), 0.4); + model.data.hu[0] += 10.0; + model.data.l[0] += 0.3; + Vector a, b; + first.apply_shifted(v, a); + second.apply_shifted(v, b); + expect_vector(a, multiply(model.shifted(0.6, 0.1), v)); + expect_vector(b, multiply(model.shifted(0.6, 0.8), v)); + first.apply_shifted(v, b); + expect_vector(a, b); + const auto required = first.workspace_bytes_required(3); + Worker bounded(blocks, model.callback(), 0.6, 0.1, required); + EXPECT_THROW(bounded.solve(vertices(model), ModuleRI::SternheimerRPA::SolverOptions{}, 1000), + std::length_error); +} + +TEST(SternheimerWeakAugmented, LargeCoarseSpaceDoesNotRequireDenseCoarseMatrices) +{ + Blocks::Data data; + data.nvirtual = 1; + data.ncoarse = 10000; + data.hu = {2.0}; + data.l.assign(data.ncoarse, 0.001); + data.k.assign(data.ncoarse, Complex(0.002, 0.001)); + const auto blocks = std::make_shared(data, 1024 * 1024); + EXPECT_LT(blocks->storage_bytes(), 1024U * 1024); + EXPECT_NEAR(blocks->minimum_metric_eigenvalue(), 0.99, 1.0e-12); + Worker worker(blocks, [](const Vector& x, Vector& y) { + y = x; + for (auto& value : y) + { + value *= 3.0; + } + }, 0.5, 0.2, 8 * 1024 * 1024); + Vector result; + worker.apply_schur(Vector(data.ncoarse, 1.0), result); + ASSERT_EQ(result.size(), static_cast(data.ncoarse)); + const Complex expected = 2.98 - Complex(0.5, -0.2) * 0.99 - 0.01 / Complex(1.5, 0.2); + EXPECT_NEAR(std::abs(result[0] - expected), 0.0, 1.0e-10); + EXPECT_NEAR(std::abs(result.back() - expected), 0.0, 1.0e-10); +} + +TEST(SternheimerWeakAugmented, ResolvesSmallPositiveMetricWithoutDroppingModes) +{ + const int n = 32; + Blocks::Data data; + data.nocc = n; + data.nvirtual = 0; + data.ncoarse = n; + data.hu.assign(n * n, 0.0); + data.k.assign(n * n, 0.0); + data.l.assign(n * n, 0.0); + Vector metric(n), rhs(n), expected(n); + for (int i = 0; i < n; ++i) + { + const double l = std::sqrt(1.0 - std::pow(10.0, -8.0 * i / (n - 1))); + data.l[i + n * i] = l; + metric[i] = 1.0 - l * l; + rhs[i] = std::sqrt(metric[i].real()); + expected[i] = -rhs[i] / (Complex(2.0, 0.2) * metric[i]); + } + auto blocks = std::make_shared(std::move(data)); + Worker worker(blocks, [metric](const Vector& x, Vector& out) { + out.resize(x.size()); + for (std::size_t i = 0; i < x.size(); ++i) out[i] = 2.0 * metric[i] * x[i]; + }, 0.0, 0.2); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 40; + options.residual_tol = 1e-8; + const auto result = worker.solve({{}, rhs}, options, 8); + ASSERT_TRUE(result.converged) << result.relative_residual; + EXPECT_LE(result.relative_residual, options.residual_tol); + EXPECT_LE(result.schur.iterations, 5); + expect_vector(result.coefficients, expected, 1e-4); + EXPECT_GT(blocks->minimum_metric_eigenvalue(), 9e-9); +} + +TEST(SternheimerWeakAugmented, NormalizedStoppingPreservesOriginalResidualTolerance) +{ + Blocks::Data data; + data.nocc = 1; + data.ncoarse = 2; + data.hu = {0.0}; + const double l = std::sqrt(1.0 - 1e-8); + const double s = 1.0 - l * l; + data.l = {l, 0.0}; + data.k = {0.0, 0.0}; + auto blocks = std::make_shared(data); + Worker worker(blocks, [s](const Vector& x, Vector& out) { + out = {s * x[0], 2.0 * x[1]}; + }, 0.0, 0.0); + ModuleRI::SternheimerRPA::SolverOptions options; + options.max_iter = 10; + options.residual_tol = 1e-8; + const auto result = worker.solve({{}, {-1.0, -1e-6}}, options, 2); + ASSERT_TRUE(result.converged) << result.relative_residual; + EXPECT_LE(result.relative_residual, options.residual_tol); + EXPECT_NEAR(std::abs(s * result.coefficients[0] - Complex(1.0)), 0.0, 1e-8); + EXPECT_NEAR(std::abs(result.coefficients[1] - Complex(5e-7)), 0.0, 1e-9); +} + +TEST(SternheimerWeakAugmented, InverseMetricRootNormalizesComplexOverlapAndPreservesAdjoint) +{ + Model model; + Blocks blocks(model.data); + const Vector x = probe(model.nc, 0.6), y = probe(model.nc, 1.1); + Vector tx, ty; + blocks.apply_complement_inverse_sqrt(x, tx); + blocks.apply_complement_inverse_sqrt(y, ty); + Vector augmented(model.nv, 0.0), metric; + augmented.insert(augmented.end(), tx.begin(), tx.end()); + blocks.apply_metric(augmented, metric); + Vector lower(metric.begin() + model.nv, metric.end()), normalized; + blocks.apply_complement_inverse_sqrt(lower, normalized); + expect_vector(normalized, x); + EXPECT_NEAR(std::abs(dot(x, ty) - dot(tx, y)), 0.0, 2e-11); + EXPECT_GT(dot(x, tx).real(), 0.0); + Vector alias = x; + blocks.apply_complement_inverse_sqrt(alias, alias); + expect_vector(alias, tx); + EXPECT_THROW(blocks.apply_complement_inverse_sqrt(Vector(1), normalized), std::invalid_argument); + Vector invalid = x; + invalid[0] = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(blocks.apply_complement_inverse_sqrt(invalid, normalized), std::invalid_argument); +} + +TEST(SternheimerWeakAugmented, ComplementSquareRootFollowedByInverseReturnsInput) +{ + const Model model; + const Blocks blocks(model.data); + const Vector input = probe(model.nc, -0.7); + Vector square_rooted; + blocks.apply_complement_sqrt(input, square_rooted); + + Vector recovered; + blocks.apply_complement_inverse_sqrt(square_rooted, recovered); + + expect_vector(recovered, input); +} + +TEST(SternheimerWeakAugmented, InverseMetricRootHandlesIdentityAndEmptyComplement) +{ + Blocks::Data data; + data.ncoarse = 3; + Blocks identity(data); + Vector x = probe(3, 0.3), result; + identity.apply_complement_inverse_sqrt(x, result); + expect_vector(result, x); + data.ncoarse = 0; + data.nvirtual = 1; + data.hu = {2.0}; + Blocks empty(data); + empty.apply_complement_inverse_sqrt({}, result); + EXPECT_TRUE(result.empty()); +} + +TEST(SternheimerWeakAugmented, PositiveVirtualBlockDoesNotImplyPositiveAugmentedHamiltonian) +{ + Blocks::Data data; + data.nvirtual = 1; + data.ncoarse = 1; + data.hu = {2.0}; + data.l = {0.0}; + data.k = {Complex(3.0, 1.0)}; + const auto blocks = std::make_shared(data); + Worker worker(blocks, [](const Vector& x, Vector& y) { y = x; }, 0.0, 0.2); + const Vector v{1.0, -1.0}; + Vector hv; + worker.apply_hamiltonian(v, hv); + EXPECT_LT(dot(v, hv).real(), 0.0); + const auto result = worker.solve({{1.0}, {0.4}}, ModuleRI::SternheimerRPA::SolverOptions{}, 1); + ASSERT_TRUE(result.converged); + expect_vector(result.coefficients, + direct_solve({{Complex(2.0, 0.2), Complex(3.0, 1.0)}, + {Complex(3.0, -1.0), Complex(1.0, 0.2)}}, {-1.0, -0.4})); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_grid_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_grid_test.cpp new file mode 100644 index 00000000000..930af1f55fa --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_grid_test.cpp @@ -0,0 +1,552 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_grid.h" + +#include +#include +#include +#include +#include + +namespace +{ +using H = ModuleRI::SternheimerFDHamiltonian; +using Operator = ModuleRI::SternheimerWeakGridOperator; +using Function = ModuleRI::SternheimerDeltaGridFunction; +using Complex = H::Complex; +using Vector = H::Vector; +constexpr double pi2 = 6.283185307179586476925286766559; + +H::Grid Grid(int nx, int ny, int nz) +{ + H::Grid grid{nx, ny, nz, 4.0 / nx, std::sqrt(9.25) / ny, std::sqrt(6.5) / nz, true}; + grid.lattice_vectors = {{{4.0, 0.0, 0.0}, {0.5, 3.0, 0.0}, {0.3, 0.4, 2.5}}}; + grid.kpoint = {0.17, -0.13, 0.21}; + return grid; +} + +std::array Position(const H::Grid& grid, int i) +{ + return {{double(i / (grid.ny * grid.nz)) / grid.nx, + double((i / grid.nz) % grid.ny) / grid.ny, double(i % grid.nz) / grid.nz}}; +} + +double DV(const H::Grid& grid) { return 30.0 / grid.size(); } + +Complex Dot(const Vector& a, const Vector& b) +{ + Complex value{}; + for (std::size_t i = 0; i < a.size(); ++i) value += std::conj(a[i]) * b[i]; + return value; +} + +Function PlaneWave(const H::Grid& grid, std::array mode) +{ + const auto dual = ModuleRI::sternheimer_fd_grid_dual_vectors(grid); + std::array momentum{}; + for (int a = 0; a < 3; ++a) + for (int j = 0; j < 3; ++j) momentum[a] += pi2 * (mode[j] + grid.kpoint[j]) * dual[j][a]; + Function result; + result.values.resize(grid.size()); + for (auto& gradient : result.gradients) gradient.resize(grid.size()); + for (int i = 0; i < grid.size(); ++i) + { + const auto r = Position(grid, i); + double phase = 0.0; + for (int a = 0; a < 3; ++a) phase += (mode[a] + grid.kpoint[a]) * r[a]; + result.values[i] = std::polar(1.0 / std::sqrt(30.0), pi2 * phase); + for (int a = 0; a < 3; ++a) result.gradients[a][i] = Complex(0.0, momentum[a]) * result.values[i]; + } + return result; +} + +// Explicit Fourier cardinal functions, independent of the FFT transfer code. +std::vector ExplicitE(const H::Grid& coarse, const H::Grid& fine) +{ + std::vector result(coarse.size()); + const std::array dims{{coarse.nx, coarse.ny, coarse.nz}}; + for (int j = 0; j < coarse.size(); ++j) + { + auto& column = result[j]; + column.values.assign(fine.size(), {}); + for (auto& gradient : column.gradients) gradient.assign(fine.size(), {}); + const auto r = Position(coarse, j); + for (int m = 0; m < coarse.size(); ++m) + { + std::array mode{{m / (coarse.ny * coarse.nz), (m / coarse.nz) % coarse.ny, m % coarse.nz}}; + double phase = 0.0; + for (int a = 0; a < 3; ++a) + { + if (mode[a] > dims[a] / 2) mode[a] -= dims[a]; + phase += (mode[a] + coarse.kpoint[a]) * r[a]; + } + const auto wave = PlaneWave(fine, mode); + const Complex weight = std::polar(1.0 / std::sqrt(double(coarse.size())), -pi2 * phase); + for (int i = 0; i < fine.size(); ++i) + { + column.values[i] += weight * wave.values[i]; + for (int a = 0; a < 3; ++a) column.gradients[a][i] += weight * wave.gradients[a][i]; + } + } + } + return result; +} + +std::shared_ptr MakeH(const H::Grid& grid) +{ + std::vector potential(grid.size()); + for (int i = 0; i < grid.size(); ++i) + { + const auto r = Position(grid, i); + potential[i] = 0.9 + 0.2 * std::cos(pi2 * r[0]) + 0.1 * std::sin(pi2 * (r[1] + r[2])); + } + using P = ModuleRI::SternheimerFDNonlocalProjector; + P::ProjectorBlock block; + block.projectors = {PlaneWave(grid, {0, 0, 0}).values, PlaneWave(grid, {1, 1, 0}).values}; + block.d_matrix = {{Complex(0.4), Complex(0.08, 0.07)}, {Complex(0.08, -0.07), Complex(0.3)}}; + auto projector = std::make_shared(grid.size(), DV(grid), std::vector{block}); + return std::make_shared(grid, potential, 0.7, projector, 8); +} + +void Near(const Vector& a, const Vector& b, double tolerance = 2e-10) +{ + ASSERT_EQ(a.size(), b.size()); + for (std::size_t i = 0; i < a.size(); ++i) EXPECT_LT(std::abs(a[i] - b[i]), tolerance) << i; +} + +TEST(SternheimerWeakGrid, CrossBlocksAndOperatorMatchExplicitFineWeakForm) +{ + const auto coarse = Grid(2, 3, 2), fine = Grid(5, 5, 4); + const auto h = MakeH(fine); + Operator op(h, coarse); + const auto e = ExplicitE(coarse, fine); + // The mode 6 aliases in values on nx=5, but its analytic gradient does not. + std::vector u{PlaneWave(fine, {0, 0, 0}), PlaneWave(fine, {6, 0, 0}), PlaneWave(fine, {2, 1, -1})}; + const auto blocks = op.assemble_blocks(u); + const auto expected_u = ModuleRI::assemble_delta_sternheimer_grid_matrices(*h, u, DV(fine)); + Near(blocks.state_hamiltonian, expected_u.hamiltonian); + ASSERT_EQ(blocks.state_count, u.size()); + ASSERT_EQ(blocks.coarse_count, std::size_t(coarse.size())); + for (int j = 0; j < coarse.size(); ++j) + { + Vector unit(coarse.size()), image, lifted; + unit[j] = 1.0; + op.lift(unit, lifted); + Near(lifted, e[j].values); + op.apply(unit, image); + Vector local, nonlocal; + h->apply_local_potential(e[j].values, local); + h->apply_nonlocal(e[j].values, nonlocal); + for (std::size_t i = 0; i < u.size(); ++i) + { + Complex k = Dot(u[i].values, local) + Dot(u[i].values, nonlocal); + for (int a = 0; a < 3; ++a) k += h->kinetic_prefactor() * Dot(u[i].gradients[a], e[j].gradients[a]); + EXPECT_LT(std::abs(blocks.state_coarse_hamiltonian[i + u.size() * j] - DV(fine) * k), 2e-10); + EXPECT_LT(std::abs(blocks.state_coarse_overlap[i + u.size() * j] - DV(fine) * Dot(u[i].values, e[j].values)), 2e-11); + } + for (int i = 0; i < coarse.size(); ++i) + { + Complex k = Dot(e[i].values, local) + Dot(e[i].values, nonlocal); + for (int a = 0; a < 3; ++a) k += h->kinetic_prefactor() * Dot(e[i].gradients[a], e[j].gradients[a]); + EXPECT_LT(std::abs(image[i] - DV(fine) * k), 2e-10); + } + } +} + +TEST(SternheimerWeakGrid, FineVertexProjectionIsAdjointOfPhysicalLift) +{ + const auto coarse = Grid(2, 3, 2), fine = Grid(5, 5, 4); + Operator op(MakeH(fine), coarse); + Vector x(coarse.size()), g(fine.size()), lifted, projected; + for (int i = 0; i < coarse.size(); ++i) x[i] = Complex(std::sin(0.4 * i), std::cos(0.3 * i)); + for (int i = 0; i < fine.size(); ++i) g[i] = Complex(std::cos(0.17 * i), std::sin(0.31 * i)); + op.lift(x, lifted); + op.project(g, projected); + EXPECT_LT(std::abs(Dot(x, projected) - DV(fine) * Dot(lifted, g)), 2e-11); + op.project(lifted, projected); + Near(x, projected); + Vector expected; + op.apply(x, expected); + op.apply(x, x); + Near(x, expected); +} + +TEST(SternheimerWeakGrid, RejectsInvalidMetricAndInputsWithoutChangingOutput) +{ + const auto coarse = Grid(2, 3, 2), fine = Grid(5, 5, 4); + const auto h = MakeH(fine); + EXPECT_THROW(Operator(nullptr, coarse), std::invalid_argument); + EXPECT_THROW(Operator(h, coarse, 1), std::length_error); + Operator op(h, coarse); + auto u = std::vector{PlaneWave(fine, {0, 0, 0})}; + EXPECT_THROW(op.assemble_blocks(u, 1e-8, 1), std::length_error); + u.push_back(u.front()); + EXPECT_THROW(op.assemble_blocks(u), std::invalid_argument); + u.pop_back(); + u[0].gradients[0].clear(); + EXPECT_THROW(op.assemble_blocks(u), std::invalid_argument); + Vector x(coarse.size()), output{Complex(17.0)}; + x[0] = std::numeric_limits::infinity(); + EXPECT_THROW(op.apply(x, output), std::invalid_argument); + ASSERT_EQ(output.size(), 1u); + EXPECT_EQ(output[0], Complex(17.0)); +} + +TEST(SternheimerWeakGrid, RejectsNonHermitianOrNonfiniteNonlocalData) +{ + const auto coarse = Grid(2, 3, 2), fine = Grid(5, 5, 4); + using P = ModuleRI::SternheimerFDNonlocalProjector; + P::ProjectorBlock block; + block.projectors = {PlaneWave(fine, {0, 0, 0}).values}; + block.d_matrix = {{Complex(0.4, 0.2)}}; + const auto make = [&]() { + auto p = std::make_shared(fine.size(), DV(fine), std::vector{block}); + return std::make_shared(fine, std::vector(fine.size(), 0.0), 0.7, p, 8); + }; + EXPECT_THROW(Operator(make(), coarse), std::invalid_argument); + block.d_matrix[0][0] = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(Operator(make(), coarse), std::invalid_argument); + block.d_matrix[0][0] = 0.4; + block.projectors[0][0] = std::numeric_limits::infinity(); + EXPECT_THROW(Operator(make(), coarse), std::invalid_argument); +} + +using Functions = std::vector; + +void ExpectSameFunctions(const Functions& actual, const Functions& expected) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t i = 0; i < actual.size(); ++i) + { + EXPECT_EQ(actual[i].values, expected[i].values); + for (int a = 0; a < 3; ++a) EXPECT_EQ(actual[i].gradients[a], expected[i].gradients[a]); + } +} + +Functions ReferenceStates(const H::Grid& fine) +{ + return {PlaneWave(fine, {0, 0, 0}), PlaneWave(fine, {6, 0, 0}), PlaneWave(fine, {2, 1, -1})}; +} + +// U=Q*R with known upper R and positive diagonal; the requested Cholesky right +// solve must recover Q, including its supplied (not FFT-reconstructed) gradients. +Functions MixedStates(const Functions& reference) +{ + const std::array, 3> r{{ + {{Complex(1.8), Complex(0.3, 0.2), Complex(-0.15, 0.4)}}, + {{Complex(0.0), Complex(1.4), Complex(-0.2, -0.3)}}, + {{Complex(0.0), Complex(0.0), Complex(0.9)}}}}; + Functions result = reference; + for (std::size_t j = 0; j < result.size(); ++j) + { + std::fill(result[j].values.begin(), result[j].values.end(), Complex(0.0)); + for (auto& gradient : result[j].gradients) std::fill(gradient.begin(), gradient.end(), Complex(0.0)); + for (std::size_t i = 0; i < reference.size(); ++i) + for (std::size_t k = 0; k < result[j].values.size(); ++k) + { + result[j].values[k] += reference[i].values[k] * r[i][j]; + for (int a = 0; a < 3; ++a) + result[j].gradients[a][k] += reference[i].gradients[a][k] * r[i][j]; + } + } + return result; +} + +TEST(SternheimerWeakGrid, InPlaceCholeskyNormalizationPreservesPrefixesAndAnalyticGradients) +{ + const auto fine = Grid(5, 5, 4); + const Functions reference = ReferenceStates(fine); + Functions states = MixedStates(reference); + std::vector> addresses; + for (const auto& state : states) + addresses.push_back({{state.values.data(), state.gradients[0].data(), + state.gradients[1].data(), state.gradients[2].data()}}); + // Far less than a full fine-U copy; forces several tiles and a partial tail. + const double error = ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine), 1e-12, 1024); + EXPECT_LT(error, 2e-12); + ASSERT_EQ(states.size(), reference.size()); + for (std::size_t i = 0; i < states.size(); ++i) + { + EXPECT_EQ(states[i].values.data(), addresses[i][0]); + Near(states[i].values, reference[i].values, 2e-12); + for (int a = 0; a < 3; ++a) + { + EXPECT_EQ(states[i].gradients[a].data(), addresses[i][a + 1]); + Near(states[i].gradients[a], reference[i].gradients[a], 2e-11); + } + for (std::size_t j = 0; j < states.size(); ++j) + EXPECT_LT(std::abs(DV(fine) * Dot(states[i].values, states[j].values) + - Complex(i == j ? 1.0 : 0.0)), 2e-12); + } + for (std::size_t prefix = 1; prefix <= states.size(); ++prefix) + { + Functions independent(reference.begin(), reference.begin() + prefix); + const Functions mixed = MixedStates(reference); + independent.assign(mixed.begin(), mixed.begin() + prefix); + ModuleRI::orthonormalize_sternheimer_weak_states_in_place(independent, DV(fine)); + for (std::size_t i = 0; i < prefix; ++i) + { + Near(independent[i].values, states[i].values, 2e-12); + for (int a = 0; a < 3; ++a) Near(independent[i].gradients[a], states[i].gradients[a], 2e-11); + } + } +} + +TEST(SternheimerWeakGrid, NormalizationIsIndependentOfTileBudgetAndHandlesOneState) +{ + const auto fine = Grid(5, 5, 4); + Functions tiled = MixedStates(ReferenceStates(fine)); + Functions large_tile = tiled; + ModuleRI::orthonormalize_sternheimer_weak_states_in_place(tiled, DV(fine), 1e-12, 1024); + ModuleRI::orthonormalize_sternheimer_weak_states_in_place(large_tile, DV(fine)); + for (std::size_t i = 0; i < tiled.size(); ++i) + { + Near(tiled[i].values, large_tile[i].values, 2e-12); + for (int a = 0; a < 3; ++a) Near(tiled[i].gradients[a], large_tile[i].gradients[a], 2e-11); + } + Functions single{PlaneWave(fine, {6, 0, 0})}; + const Function original = single[0]; + for (auto& value : single[0].values) value *= Complex(0.0, 2.0); + for (auto& gradient : single[0].gradients) + for (auto& value : gradient) value *= Complex(0.0, 2.0); + EXPECT_LT(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(single, DV(fine)), 2e-12); + Vector expected = original.values; + for (auto& value : expected) value *= Complex(0.0, 1.0); + Near(single[0].values, expected, 2e-12); +} + +TEST(SternheimerWeakGrid, NormalizationRejectsSingularAndIllConditionedMetricWithoutMutation) +{ + const auto fine = Grid(5, 5, 4); + const Functions reference = ReferenceStates(fine); + Functions states{reference[0], reference[0]}; + Functions original = states; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine)), std::domain_error); + ExpectSameFunctions(states, original); + for (std::size_t k = 0; k < states[1].values.size(); ++k) + { + states[1].values[k] += 1e-7 * reference[1].values[k]; + for (int a = 0; a < 3; ++a) states[1].gradients[a][k] += 1e-7 * reference[1].gradients[a][k]; + } + original = states; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine)), std::domain_error); + ExpectSameFunctions(states, original); + states = reference; + for (auto& value : states[0].values) value *= 1e8; + for (auto& gradient : states[0].gradients) + for (auto& value : gradient) value *= 1e8; + original = states; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine)), std::domain_error); + ExpectSameFunctions(states, original); +} + +TEST(SternheimerWeakGrid, NormalizationValidatesArgumentsBeforeMutation) +{ + const auto fine = Grid(5, 5, 4); + Functions states = MixedStates(ReferenceStates(fine)); + const Functions original = states; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine), 1e-12, 1), + std::length_error); + for (double dv : {0.0, -0.1, std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, dv), std::invalid_argument); + for (double threshold : {0.0, -0.1, 1.0, std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine), threshold), + std::invalid_argument); + ExpectSameFunctions(states, original); + states.back().gradients[2].pop_back(); + const Functions malformed = states; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine)), std::invalid_argument); + ExpectSameFunctions(states, malformed); + states = original; + states.back().gradients[2].back() = std::numeric_limits::infinity(); + const Functions nonfinite = states; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, DV(fine)), std::invalid_argument); + ExpectSameFunctions(states, nonfinite); + Functions empty; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(empty, DV(fine)), std::invalid_argument); +} + +TEST(SternheimerWeakGrid, NormalizationReportsArithmeticOverflow) +{ + Function state; + state.values = {0.01}; + for (auto& gradient : state.gradients) gradient = {std::numeric_limits::max()}; + Functions states{state}; + EXPECT_THROW(ModuleRI::orthonormalize_sternheimer_weak_states_in_place(states, 1.0), std::overflow_error); +} + +#ifdef ABACUS_STERNHEIMER_WEAK_EXACT_APPLY_CACHE +std::shared_ptr MakeCacheH(const H::Grid& coarse, const H::Grid& fine, bool nonlocal = true) +{ + const std::array c{{coarse.nx, coarse.ny, coarse.nz}}; + const std::array f{{fine.nx, fine.ny, fine.nz}}; + std::vector potential(fine.size(), 0.7); + for (int i = 0; i < fine.size(); ++i) + { + const auto r = Position(fine, i); + // Required difference-window endpoints, just-outside modes and fine Nyquist. + for (int a = 0; a < 3; ++a) + potential[i] += 0.17 * std::sin(pi2 * (c[a] - 1) * r[a]) + + 0.23 * std::cos(pi2 * c[a] * r[a]) + + 0.13 * std::cos(pi2 * (f[a] / 2) * r[a]); + potential[i] += 0.19 * std::sin(pi2 * ((c[0] - 1) * r[0] - (c[1] - 1) * r[1] + + (c[2] - 1) * r[2])); + } + using P = ModuleRI::SternheimerFDNonlocalProjector; + std::shared_ptr projector; + if (nonlocal) + { + P::ProjectorBlock first, second; + first.projectors.assign(2, Vector(fine.size())); + second.projectors.assign(1, Vector(fine.size())); + for (int i = 0; i < fine.size(); ++i) + { + first.projectors[0][i] = Complex(std::sin(0.31 * i), std::cos(0.57 * i)) / std::sqrt(30.0); + first.projectors[1][i] = Complex(std::cos(0.71 * i), std::sin(0.29 * i)) / std::sqrt(30.0); + second.projectors[0][i] = i % 3 ? Complex(0.0) : Complex(0.11, -0.09); + } + first.d_matrix = {{Complex(0.4), Complex(0.08, 0.27)}, {Complex(0.08, -0.27), Complex(-0.3)}}; + second.d_matrix = {{Complex(-0.17)}}; + projector = std::make_shared(fine.size(), DV(fine), std::vector{first, second}); + } + return std::make_shared(fine, potential, 0.7, projector, 8); +} + +void CheckCachedActions(const H::Grid& coarse, const H::Grid& fine, bool nonlocal, bool local) +{ + const auto h = MakeCacheH(coarse, fine); + Operator original(h, coarse), cached(h, coarse); + EXPECT_FALSE(cached.has_exact_nonlocal_cache()); + EXPECT_FALSE(cached.has_exact_local_cache()); + EXPECT_EQ(cached.exact_cache_storage_bytes(), 0u); + cached.enable_exact_apply_cache(nonlocal, local); + EXPECT_EQ(cached.has_exact_nonlocal_cache(), nonlocal); + EXPECT_EQ(cached.has_exact_local_cache(), local); + Vector x(coarse.size()), y(coarse.size()), expected, actual, hy; + for (int i = 0; i < coarse.size(); ++i) + { + x[i] = Complex(std::sin(0.37 * i + 0.2), std::cos(0.53 * i)); + y[i] = Complex(std::cos(0.61 * i), std::sin(0.19 * i + 0.3)); + } + original.apply(x, expected); + cached.apply(x, actual); + Near(actual, expected); + cached.apply(y, hy); + EXPECT_LT(std::abs(Dot(y, actual) - Dot(hy, x)), 2e-9); + cached.apply(x, x); + Near(x, expected); + // Visit every mode, including the asymmetric positive coarse Nyquist. + const std::array dims{{coarse.nx, coarse.ny, coarse.nz}}; + for (int i = 0; i < coarse.size(); ++i) + { + std::array mode{{i / (coarse.ny * coarse.nz), (i / coarse.nz) % coarse.ny, i % coarse.nz}}; + for (int a = 0; a < 3; ++a) if (mode[a] > dims[a] / 2) mode[a] -= dims[a]; + x = PlaneWave(coarse, mode).values; + original.apply(x, expected); + cached.apply(x, actual); + Near(actual, expected); + } +} + +TEST(SternheimerWeakGrid, ExactNonlocalCacheMatchesFineApplyAtSkewNonzeroK) +{ + CheckCachedActions(Grid(2, 3, 2), Grid(7, 7, 6), true, false); + CheckCachedActions(Grid(3, 2, 3), Grid(8, 5, 8), true, false); +} + +TEST(SternheimerWeakGrid, ExactLocalCacheRetainsBoundaryDifferencesAndRejectsAliasedHighModes) +{ + CheckCachedActions(Grid(2, 3, 2), Grid(7, 9, 8), false, true); + CheckCachedActions(Grid(3, 2, 3), Grid(9, 7, 9), false, true); +} + +TEST(SternheimerWeakGrid, ExactCombinedCacheHandlesFineLimitedAxesAndIdentityTransfer) +{ + CheckCachedActions(Grid(3, 4, 2), Grid(4, 7, 3), true, true); + CheckCachedActions(Grid(3, 2, 2), Grid(3, 2, 2), true, true); + CheckCachedActions(Grid(1, 2, 1), Grid(1, 5, 4), true, true); +} + +TEST(SternheimerWeakGrid, ExactCacheSupportsLargeExplicitBlochLabelsAnd144Axis) +{ + auto coarse = Grid(3, 2, 2), fine = Grid(144, 5, 6); + coarse.kpoint = fine.kpoint = {3.17, -2.13, 1.21}; + CheckCachedActions(coarse, fine, true, true); +} + +TEST(SternheimerWeakGrid, ExactCacheLeavesFineBlocksLiftAndProjectUnchanged) +{ + const auto coarse = Grid(2, 3, 2), fine = Grid(5, 7, 6); + Operator op(MakeCacheH(coarse, fine), coarse); + const Functions states{PlaneWave(fine, {0, 0, 0}), PlaneWave(fine, {6, 0, 0})}; + const auto before = op.assemble_blocks(states); + Vector x(coarse.size(), Complex(0.2, -0.1)), lifted, projected, after_lift, after_project; + op.lift(x, lifted); + op.project(lifted, projected); + op.enable_exact_apply_cache(true, true); + const auto after = op.assemble_blocks(states); + Near(before.state_hamiltonian, after.state_hamiltonian); + Near(before.state_coarse_overlap, after.state_coarse_overlap); + Near(before.state_coarse_hamiltonian, after.state_coarse_hamiltonian); + op.lift(x, after_lift); + op.project(lifted, after_project); + Near(lifted, after_lift); + Near(projected, after_project); +} + +TEST(SternheimerWeakGrid, ExactCacheBudgetFailuresAreTransactionalAndCachesCanBeDisabled) +{ + const auto coarse = Grid(2, 3, 2), fine = Grid(7, 9, 8); + const auto h = MakeCacheH(coarse, fine); + Operator op(h, coarse); + const auto budget = Operator::exact_cache_workspace_bytes_required(*h, coarse, true, true); + EXPECT_GT(budget, 0u); + EXPECT_THROW(op.enable_exact_apply_cache(true, true, budget - 1), std::length_error); + EXPECT_EQ(op.exact_cache_storage_bytes(), 0u); + op.enable_exact_apply_cache(true, true, budget); + EXPECT_GT(op.exact_cache_storage_bytes(), 0u); + EXPECT_LE(op.exact_cache_storage_bytes(), budget); + Vector x(coarse.size(), Complex(0.2, 0.7)), expected, actual; + op.apply(x, expected); + EXPECT_THROW(op.enable_exact_apply_cache(true, false, 0), std::length_error); + EXPECT_TRUE(op.has_exact_local_cache()); + EXPECT_TRUE(op.has_exact_nonlocal_cache()); + op.apply(x, actual); + Near(actual, expected); + Vector sentinel{Complex(17.0, -2.0)}; + EXPECT_THROW(op.apply(Vector(1), sentinel), std::invalid_argument); + x[0] = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(op.apply(x, sentinel), std::invalid_argument); + ASSERT_EQ(sentinel.size(), 1u); + EXPECT_EQ(sentinel[0], Complex(17.0, -2.0)); + op.enable_exact_apply_cache(false, false, 0); + EXPECT_EQ(op.exact_cache_storage_bytes(), 0u); + EXPECT_FALSE(op.has_exact_nonlocal_cache()); + EXPECT_FALSE(op.has_exact_local_cache()); +} + +TEST(SternheimerWeakGrid, ExactCacheHandlesAbsentProjectorsAndValidatesGridBeforeAllocation) +{ + const auto coarse = Grid(3, 2, 2), fine = Grid(7, 5, 6); + const auto h = MakeCacheH(coarse, fine, false); + Operator original(h, coarse), cached(h, coarse); + cached.enable_exact_apply_cache(true, true); + Vector x(coarse.size(), Complex(0.7, 0.3)), expected, actual; + original.apply(x, expected); + cached.apply(x, actual); + Near(expected, actual); + auto invalid = coarse; + invalid.nx = 0; + EXPECT_THROW(Operator::exact_cache_workspace_bytes_required(*h, invalid, true, true), std::invalid_argument); + invalid = coarse; + invalid.kpoint[0] += 1; + EXPECT_THROW(Operator::exact_cache_workspace_bytes_required(*h, invalid, true, true), std::invalid_argument); + EXPECT_EQ(Operator::exact_cache_workspace_bytes_required(*h, coarse, false, false), 0u); +} +#else +TEST(SternheimerWeakGrid, ExactApplyCacheRequiresImplementation) +{ + FAIL() << "Missing opt-in exact apply cache: expected test-first RED before implementation"; +} +#endif +} // namespace diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_matrix_audit_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_matrix_audit_test.cpp new file mode 100644 index 00000000000..065a25dd808 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_matrix_audit_test.cpp @@ -0,0 +1,189 @@ +#include + +// Keep the test-only RED handoff runnable before the opt-in helper is added. +#if __has_include("source_lcao/module_rpa/module_delta_st/sternheimer_weak_matrix_audit.h") +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_matrix_audit.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_abfs_perturbation.h" +#include "source_base/module_external/blas_connector.h" + +#include +#include +#include +#include +#include + +using ModuleRI::SternheimerWeakAuditShard; + +TEST(SternheimerWeakMatrixAudit, DefaultsOwnEveryColumn) +{ + const auto shard = SternheimerWeakAuditShard::parse(nullptr, nullptr, 7); + EXPECT_EQ(shard.index, 0); + EXPECT_EQ(shard.count, 1); + EXPECT_EQ(shard.columns(), (std::vector{0, 1, 2, 3, 4, 5, 6})); +} + +TEST(SternheimerWeakMatrixAudit, SixteenShardsPartitionRealAuxiliaryCounts) +{ + for (const int channels : {1113, 928}) + { + std::vector visits(channels, 0); + for (int index = 0; index < 16; ++index) + { + const auto raw_index = std::to_string(index); + const auto shard = SternheimerWeakAuditShard::parse(raw_index.c_str(), "16", channels); + const auto owned = shard.columns(); + ASSERT_FALSE(owned.empty()); + EXPECT_EQ(owned.size(), static_cast((channels - 1 - index) / 16 + 1)); + for (const int column : owned) + { + EXPECT_EQ(column % 16, index); + EXPECT_TRUE(shard.owns(column)); + ++visits.at(column); + } + for (int column = 0; column < channels; ++column) + EXPECT_EQ(shard.owns(column), column % 16 == index); + } + for (const int count : visits) EXPECT_EQ(count, 1); + } +} + +TEST(SternheimerWeakMatrixAudit, OneShardPerChannelAndUnevenLastShard) +{ + EXPECT_EQ(SternheimerWeakAuditShard::parse("4", "5", 5).columns(), (std::vector{4})); + EXPECT_EQ(SternheimerWeakAuditShard::parse("2", "3", 8).columns(), (std::vector{2, 5})); + const auto shard = SternheimerWeakAuditShard::parse("0", "1", 3); + EXPECT_FALSE(shard.owns(-1)); + EXPECT_FALSE(shard.owns(3)); +} + +TEST(SternheimerWeakMatrixAudit, RejectsInvalidShardConfiguration) +{ + for (const char* index : {"-1", "2", "3", "", "x", "1x", " 1", "+1", "1.0", + "9999999999999999999999999999"}) + EXPECT_THROW(SternheimerWeakAuditShard::parse(index, "2", 5), std::invalid_argument); + for (const char* count : {"0", "-1", "6", "", "x", "2x", " 2", "+2", "2.0", + "9999999999999999999999999999"}) + EXPECT_THROW(SternheimerWeakAuditShard::parse("0", count, 5), std::invalid_argument); + EXPECT_THROW(SternheimerWeakAuditShard::parse(nullptr, nullptr, 0), std::invalid_argument); + EXPECT_THROW(SternheimerWeakAuditShard::parse(nullptr, nullptr, -1), std::invalid_argument); +} + +TEST(SternheimerWeakMatrixAudit, SingleChannelSamplingPreservesGlobalMetadataAndValues) +{ + ModuleRI::SternheimerRadialPerturbation s; + s.radial_grid = {0.0, 0.5, 1.5}; + s.radial_values = {1.0, 0.7, 0.0}; + s.label = "toy_s"; + auto p = s; + p.angular_momentum = 1; + p.radial_index = 2; + p.label = "toy_p"; + const std::vector> radials{{s, p}, {s}}; + const std::vector types{0, 1, 0}; + const std::vector> positions{{0.0, 0.0, 0.0}, + {0.3, 0.2, 0.1}, {0.8, 0.6, 0.4}}; + ModuleRI::SternheimerFDHamiltonian::Grid grid; + grid.nx = 3; grid.ny = 2; grid.nz = 2; + grid.hx = 0.7; grid.hy = 0.8; grid.hz = 0.9; + grid.periodic = true; + const ModuleRI::SternheimerReducedKPoint q{0.25, 0.0, 0.0}; + const auto full = ModuleRI::sample_sternheimer_abf_bloch_grid_channels( + radials, types, positions, grid, q, -1); + const auto description = ModuleRI::describe_sternheimer_abf_grid_channels(radials, types, positions, -1); + ASSERT_EQ(full.size(), 9); + ASSERT_EQ(description.size(), full.size()); + EXPECT_EQ(ModuleRI::sample_sternheimer_abf_bloch_grid_channels( + radials, types, positions, grid, q, 2).size(), 2); + for (int j = 0; j < static_cast(full.size()); ++j) + { + auto single = ModuleRI::sample_sternheimer_abf_bloch_grid_channels( + radials, types, positions, grid, q, 1, j); + ASSERT_EQ(single.size(), 1); + const auto& channel = single.front(); + EXPECT_EQ(channel.channel_index, j); + EXPECT_EQ(channel.atom_index, full[j].atom_index); + EXPECT_EQ(channel.atom_local_index, full[j].atom_local_index); + EXPECT_EQ(channel.type_index, full[j].type_index); + EXPECT_EQ(channel.angular_momentum, full[j].angular_momentum); + EXPECT_EQ(channel.magnetic_index, full[j].magnetic_index); + EXPECT_EQ(channel.radial_index, full[j].radial_index); + EXPECT_EQ(channel.label, full[j].label); + EXPECT_EQ(channel.potential_r, full[j].potential_r); + EXPECT_EQ(channel.max_abs, full[j].max_abs); + EXPECT_EQ(description[j].channel_index, channel.channel_index); + EXPECT_EQ(description[j].atom_local_index, channel.atom_local_index); + EXPECT_TRUE(description[j].potential_r.empty()); + auto reference = std::vector{full[j]}; + ModuleRI::solve_sternheimer_abf_periodic_full_coulomb_in_place(single, grid, q, 0.0); + ModuleRI::solve_sternheimer_abf_periodic_full_coulomb_in_place(reference, grid, q, 0.0); + EXPECT_EQ(single.front().potential_r, reference.front().potential_r); + } + EXPECT_TRUE(ModuleRI::sample_sternheimer_abf_bloch_grid_channels( + radials, types, positions, grid, q, 1, 9).empty()); + EXPECT_THROW(ModuleRI::sample_sternheimer_abf_bloch_grid_channels( + radials, types, positions, grid, q, 1, -1), std::invalid_argument); +} + +TEST(SternheimerWeakMatrixAudit, ComplexBlasAllRowsMatchExplicitContractionAndTwoSignSum) +{ + using Complex = std::complex; + using Vector = std::vector; + constexpr int channels = 3; + constexpr int nf = 2; + constexpr int nw = 4; + // Independent logical left vertices; the packed BLAS matrices are 2x3 and 4x3. + const std::array gf{{{{1.0, 0.5}, {-0.2, 0.8}}, + {{-0.7, 0.3}, {1.4, -0.6}}, + {{0.9, -1.2}, {0.4, 0.7}}}}; + const std::array gw{{{{0.3, 0.9}, {-0.8, 0.2}, {1.1, -0.4}, {0.6, 0.7}}, + {{-0.4, 0.5}, {0.2, -1.3}, {0.8, 0.6}, {-0.9, 0.4}}, + {{1.2, -0.8}, {-0.5, -0.1}, {0.7, 0.3}, {0.1, -0.6}}}}; + Vector packed_f(nf * channels), packed_w(nw * channels); + for (int row = 0; row < channels; ++row) + { + for (int u = 0; u < nf; ++u) packed_f[u + nf * row] = gf[row][u]; + for (int u = 0; u < nw; ++u) packed_w[u + nw * row] = gw[row][u]; + } + // Separate complex solutions, not negatives/conjugates that could hide a sum error. + const std::array solutions{{{{0.4, 0.9}, {-0.7, 0.2}, {1.1, -0.5}, + {0.3, 0.8}, {-0.6, -0.4}, {0.9, 0.1}}, + {{-0.2, 0.6}, {0.8, -0.3}, {-0.4, 0.7}, + {1.2, -0.2}, {0.5, 0.9}, {-0.1, -0.8}}}}; + Vector signed_sum(channels, 0.0), explicit_sum(channels, 0.0); + for (int branch = 0; branch < 2; ++branch) + { + SCOPED_TRACE(branch == 0 ? "sign +1" : "sign -1"); + const auto& coefficients = solutions[branch]; + // Nonzero initial output also checks beta=0 for F and beta=1 for W. + Vector block_column(channels, Complex(8.0, -3.0)); + BlasConnector::gemv('C', nf, channels, Complex(0.5), packed_f.data(), nf, + coefficients.data(), 1, Complex(0.0), block_column.data(), 1); + BlasConnector::gemv('C', nw, channels, Complex(0.5), packed_w.data(), nw, + coefficients.data() + nf, 1, Complex(1.0), block_column.data(), 1); + for (int row = 0; row < channels; ++row) + { + SCOPED_TRACE(row); + Complex expected = 0.0; + for (int u = 0; u < nf; ++u) expected += 0.5 * std::conj(gf[row][u]) * coefficients[u]; + for (int u = 0; u < nw; ++u) expected += 0.5 * std::conj(gw[row][u]) * coefficients[nf + u]; + EXPECT_NEAR(block_column[row].real(), expected.real(), 1e-13); + EXPECT_NEAR(block_column[row].imag(), expected.imag(), 1e-13); + signed_sum[row] += block_column[row]; + explicit_sum[row] += expected; + } + } + for (int row = 0; row < channels; ++row) + { + SCOPED_TRACE(row); + EXPECT_NEAR(signed_sum[row].real(), explicit_sum[row].real(), 1e-13); + EXPECT_NEAR(signed_sum[row].imag(), explicit_sum[row].imag(), 1e-13); + EXPECT_GT(std::abs(explicit_sum[row].imag()), 1e-3); + } +} + +#else +TEST(SternheimerWeakMatrixAudit, OptInImplementationMustBePresent) +{ + FAIL() << "Missing sternheimer_weak_matrix_audit.h: expected test-first RED before implementation"; +} +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_preconditioner_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_preconditioner_test.cpp new file mode 100644 index 00000000000..d2b530af283 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_preconditioner_test.cpp @@ -0,0 +1,220 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_preconditioner.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + +using Preconditioner = ModuleRI::SternheimerWeakSpectralPreconditioner; +using Grid = Preconditioner::Grid; +using Vector = Preconditioner::Vector; +using Complex = Preconditioner::Complex; +using Lattice = std::array, 3>; + +constexpr double alpha = 1.0; +constexpr double epsilon = 0.37; +constexpr double omega = 0.29; +constexpr double regularization = 0.02; + +Grid make_grid(const std::array& dimensions, + const Lattice& lattice, + const std::array& kpoint) +{ + Grid grid; + grid.nx = dimensions[0]; + grid.ny = dimensions[1]; + grid.nz = dimensions[2]; + grid.periodic = true; + grid.lattice_vectors = lattice; + grid.kpoint = kpoint; + const auto length = [](const std::array& value) { + return std::sqrt(value[0] * value[0] + value[1] * value[1] + + value[2] * value[2]); + }; + grid.hx = length(lattice[0]) / grid.nx; + grid.hy = length(lattice[1]) / grid.ny; + grid.hz = length(lattice[2]) / grid.nz; + return grid; +} + +Vector mode(const Grid& grid, const std::array& frequencies, + const double phase = 0.0) +{ + Vector result(static_cast(grid.size())); + const double two_pi = 2.0 * std::acos(-1.0); + for (int ix = 0; ix != grid.nx; ++ix) + { + for (int iy = 0; iy != grid.ny; ++iy) + { + for (int iz = 0; iz != grid.nz; ++iz) + { + const double angle + = phase + + two_pi + * ((frequencies[0] + grid.kpoint[0]) * ix / grid.nx + + (frequencies[1] + grid.kpoint[1]) * iy / grid.ny + + (frequencies[2] + grid.kpoint[2]) * iz / grid.nz); + const auto index + = (static_cast(ix) * grid.ny + iy) * grid.nz + iz; + result[index] = std::exp(Complex(0.0, angle)); + } + } + } + return result; +} + +void expect_scaled(const Vector& actual, + const Vector& input, + const Complex factor, + const double tolerance = 3.0e-12) +{ + ASSERT_EQ(actual.size(), input.size()); + for (std::size_t i = 0; i != input.size(); ++i) + { + EXPECT_NEAR(std::abs(actual[i] - factor * input[i]), 0.0, tolerance) + << "index " << i; + } +} + +double squared_norm(const std::array& value) +{ + return value[0] * value[0] + value[1] * value[1] + value[2] * value[2]; +} + +std::array orthogonal_wavevector(const Grid& grid, + const std::array& frequencies) +{ + const double two_pi = 2.0 * std::acos(-1.0); + return {two_pi * (frequencies[0] + grid.kpoint[0]) / 6.0, + two_pi * (frequencies[1] + grid.kpoint[1]) / 7.0, + two_pi * (frequencies[2] + grid.kpoint[2]) / 8.0}; +} + +std::array skew_wavevector(const Grid& grid, + const std::array& frequencies) +{ + const double two_pi = 2.0 * std::acos(-1.0); + const double qx = two_pi * (frequencies[0] + grid.kpoint[0]) / 5.0; + const double qy + = (two_pi * (frequencies[1] + grid.kpoint[1]) + 2.5 * qx) / 4.3; + const double qz + = (two_pi * (frequencies[2] + grid.kpoint[2]) - 0.4 * qx + 0.2 * qy) + / 8.0; + return {qx, qy, qz}; +} + +Complex inverse_denominator(const std::array& wavevector, + const double signed_omega = omega) +{ + return 1.0 + / Complex(alpha * squared_norm(wavevector) - epsilon + regularization, + signed_omega); +} + +} // namespace + +TEST(SternheimerWeakPreconditioner, MatchesOrthogonalBlochFourierMode) +{ + const Lattice lattice{{{6.0, 0.0, 0.0}, + {0.0, 7.0, 0.0}, + {0.0, 0.0, 8.0}}}; + const Grid grid = make_grid({5, 4, 6}, lattice, {0.17, -0.11, 0.08}); + const std::array frequencies{2, -1, 2}; + const Vector input = mode(grid, frequencies, 0.43); + const Preconditioner preconditioner(grid, alpha, epsilon, omega, regularization); + + Vector actual; + preconditioner.apply(input, actual); + + expect_scaled(actual, input, + inverse_denominator(orthogonal_wavevector(grid, frequencies))); +} + +TEST(SternheimerWeakPreconditioner, MatchesSkewBlochFourierMode) +{ + const Lattice lattice{{{5.0, 0.0, 0.0}, + {-2.5, 4.3, 0.0}, + {0.4, -0.2, 8.0}}}; + const Grid grid = make_grid({5, 4, 3}, lattice, {-0.21, 0.13, 0.07}); + const std::array frequencies{-2, 1, -1}; + const Vector input = mode(grid, frequencies, -0.31); + const Preconditioner preconditioner(grid, alpha, epsilon, omega, regularization); + + Vector actual; + preconditioner.apply(input, actual); + + expect_scaled(actual, input, inverse_denominator(skew_wavevector(grid, frequencies))); +} + +TEST(SternheimerWeakPreconditioner, SignedFrequencyConjugacyAndInPlaceApply) +{ + const Lattice lattice{{{6.0, 0.0, 0.0}, + {0.0, 7.0, 0.0}, + {0.0, 0.0, 8.0}}}; + const Grid grid = make_grid({4, 4, 4}, lattice, {0.0, 0.0, 0.0}); + Vector positive = mode(grid, {0, 0, 0}); + Vector negative = positive; + const Preconditioner plus(grid, alpha, -0.3, omega, 0.0); + const Preconditioner minus(grid, alpha, -0.3, -omega, 0.0); + + plus.apply(positive, positive); + minus.apply(negative, negative); + + ASSERT_EQ(positive.size(), negative.size()); + for (std::size_t i = 0; i != positive.size(); ++i) + { + EXPECT_NEAR(std::abs(negative[i] - std::conj(positive[i])), 0.0, 2.0e-12); + } +} + +TEST(SternheimerWeakPreconditioner, RejectsInvalidConstruction) +{ + const Lattice lattice{{{6.0, 0.0, 0.0}, + {0.0, 7.0, 0.0}, + {0.0, 0.0, 8.0}}}; + Grid grid = make_grid({4, 4, 4}, lattice, {0.0, 0.0, 0.0}); + EXPECT_THROW(Preconditioner(grid, alpha, 0.0, 0.0, 0.0), std::domain_error); + EXPECT_THROW(Preconditioner(grid, 0.0, epsilon, omega, regularization), + std::invalid_argument); + EXPECT_THROW(Preconditioner(grid, alpha, + std::numeric_limits::quiet_NaN(), omega, + regularization), + std::invalid_argument); + EXPECT_THROW(Preconditioner(grid, alpha, epsilon, + std::numeric_limits::infinity(), + regularization), + std::invalid_argument); + EXPECT_THROW(Preconditioner(grid, alpha, epsilon, omega, -0.1), + std::invalid_argument); + grid.periodic = false; + EXPECT_THROW(Preconditioner(grid, alpha, epsilon, omega, regularization), + std::invalid_argument); + grid.periodic = true; + grid.nx = 0; + EXPECT_THROW(Preconditioner(grid, alpha, epsilon, omega, regularization), + std::invalid_argument); +} + +TEST(SternheimerWeakPreconditioner, RejectsInvalidInputWithoutChangingOutput) +{ + const Lattice lattice{{{6.0, 0.0, 0.0}, + {0.0, 7.0, 0.0}, + {0.0, 0.0, 8.0}}}; + const Grid grid = make_grid({4, 4, 4}, lattice, {0.0, 0.0, 0.0}); + const Preconditioner preconditioner(grid, alpha, epsilon, omega, regularization); + Vector output{Complex(7.0, -3.0)}; + EXPECT_THROW(preconditioner.apply(Vector(3, 0.0), output), + std::invalid_argument); + EXPECT_EQ(output, Vector({Complex(7.0, -3.0)})); + Vector input(static_cast(grid.size()), 0.0); + input[7] = Complex(std::numeric_limits::quiet_NaN(), 0.0); + EXPECT_THROW(preconditioner.apply(input, output), std::invalid_argument); + EXPECT_EQ(output, Vector({Complex(7.0, -3.0)})); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_dispatch_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_dispatch_test.cpp new file mode 100644 index 00000000000..371570c8740 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_dispatch_test.cpp @@ -0,0 +1,338 @@ +#include + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_dispatch.h" + +#include +#include +#include +#include +#include +#include + +using ModuleRI::SternheimerWeakQLayout; +using ModuleRI::run_sternheimer_weak_q_tasks; + +namespace +{ +int thread_index() +{ +#ifdef _OPENMP + return omp_get_thread_num(); +#else + return 0; +#endif +} + +struct Worker +{ + explicit Worker(int index) : index(index) {} + Worker(const Worker&) = delete; + Worker& operator=(const Worker&) = delete; + int index; + std::size_t calls = 0; +}; + +void check_coverage(const SternheimerWeakQLayout& layout, int task_count) +{ + std::vector> visits(task_count); + for (auto& count : visits) count.store(0); + std::vector factories(layout.workers, 0); + std::vector team_sizes(layout.workers, 0); + std::vector owners(task_count, -1); + std::vector observed(layout.workers, 0); + const auto counts = run_sternheimer_weak_q_tasks(task_count, layout, + [&](int index) { + EXPECT_EQ(thread_index(), index); + ++factories[index]; +#ifdef _OPENMP + team_sizes[index] = omp_get_num_threads(); + EXPECT_EQ(team_sizes[index], layout.workers); + EXPECT_FALSE(omp_get_dynamic()); + EXPECT_EQ(omp_get_max_threads(), layout.inner_threads); +#else + team_sizes[index] = 1; +#endif +#ifdef __MKL + EXPECT_EQ(mkl_get_max_threads(), layout.inner_threads); + EXPECT_EQ(mkl_set_num_threads_local(layout.inner_threads), layout.inner_threads); +#endif + return std::unique_ptr(new Worker(index)); + }, + [&](std::int64_t task, std::unique_ptr& worker, int index) { + EXPECT_EQ(thread_index(), index); + EXPECT_EQ(worker->index, index); +#ifdef _OPENMP + EXPECT_FALSE(omp_get_dynamic()); +#endif +#ifdef __MKL + EXPECT_EQ(mkl_get_max_threads(), layout.inner_threads); + EXPECT_EQ(mkl_set_num_threads_local(layout.inner_threads), layout.inner_threads); +#endif + ++worker->calls; + observed[index] = worker->calls; + visits[task].fetch_add(1); + owners[task] = index; + }); + ASSERT_EQ(counts.size(), static_cast(layout.workers)); + ASSERT_EQ(team_sizes[0], layout.workers); + EXPECT_EQ(std::accumulate(factories.begin(), factories.end(), 0), team_sizes[0]); + EXPECT_EQ(std::accumulate(counts.begin(), counts.end(), std::size_t(0)), + static_cast(task_count)); + for (int i = 0; i < task_count; ++i) + { + EXPECT_EQ(visits[i].load(), 1); + EXPECT_GE(owners[i], 0); + EXPECT_LT(owners[i], layout.workers); + } + for (int i = 0; i < layout.workers; ++i) + { + EXPECT_EQ(factories[i], 1); + EXPECT_EQ(counts[i], observed[i]); + if (counts[i] != 0) EXPECT_EQ(factories[i], 1); + } +} +} // namespace + +TEST(SternheimerWeakQDispatch, ValidatesAll48CpuCandidatesWithoutOverflow) +{ + for (int workers : {1, 4, 8, 16, 24, 48}) + EXPECT_NO_THROW((SternheimerWeakQLayout{workers, 48 / workers, 48}.validate())); + EXPECT_NO_THROW((SternheimerWeakQLayout{3, 5, 48}.validate())); + for (const auto& layout : std::array{{ + {0, 1, 48}, {-1, 1, 48}, {1, 0, 48}, {1, -1, 48}, + {1, 1, 0}, {1, 1, -1}, {49, 1, 48}, {8, 7, 48}, + {std::numeric_limits::max(), 2, std::numeric_limits::max()}}}) + EXPECT_THROW(layout.validate(), std::invalid_argument); +} + +TEST(SternheimerWeakQDispatch, RejectsInvalidInputBeforeCreatingWorkers) +{ + int created = 0; + auto factory = [&](int) { ++created; return 0; }; + auto callback = [](std::int64_t, int&, int) {}; + EXPECT_THROW(run_sternheimer_weak_q_tasks(-1, {1, 1, 1}, factory, callback), std::invalid_argument); + EXPECT_THROW(run_sternheimer_weak_q_tasks(0, {0, 1, 1}, factory, callback), std::invalid_argument); + EXPECT_THROW(run_sternheimer_weak_q_tasks(10, {2, 2, 3}, factory, callback), std::invalid_argument); + EXPECT_EQ(created, 0); +} + +TEST(SternheimerWeakQDispatch, EmptyTasksDoNotCreateWorkers) +{ + int created = 0; + const auto counts = run_sternheimer_weak_q_tasks(0, {1, 1, 1}, + [&](int) { ++created; return 0; }, [](std::int64_t, int&, int) { FAIL(); }); + EXPECT_EQ(created, 0); + EXPECT_EQ(counts, std::vector{0}); +} + +TEST(SternheimerWeakQDispatch, ExactCoverageForMoveOnlyThreadOwnedWorkers) +{ +#ifdef _OPENMP + for (int workers : {1, 4, 8, 16, 24, 48}) + { + SCOPED_TRACE(workers); + check_coverage({workers, 48 / workers, 48}, 197); + } + check_coverage({8, 1, 8}, 3); +#else + check_coverage({1, 1, 1}, 197); + EXPECT_THROW(run_sternheimer_weak_q_tasks(1, {2, 1, 2}, + [](int) { return 0; }, [](std::int64_t, int&, int) {}), std::invalid_argument); +#endif +} + +TEST(SternheimerWeakQDispatch, CallbackFailureStillAttemptsEveryTaskOnceAndRethrowsOutsideTeam) +{ + std::array, 37> visits; + for (auto& count : visits) count.store(0); + std::atomic alive{0}; + const int workers = +#ifdef _OPENMP + 4; +#else + 1; +#endif + try + { + run_sternheimer_weak_q_tasks(37, {workers, 1, workers}, + [&](int index) { + ++alive; + return std::shared_ptr(new Worker(index), [&](Worker* worker) { + --alive; + delete worker; + }); + }, + [&](std::int64_t task, std::shared_ptr&, int) { + ++visits[task]; + if (task == 3 || task == 17) throw std::runtime_error("callback failure"); + }); + FAIL() << "Callback exception was lost"; + } + catch (const std::runtime_error& error) + { + EXPECT_EQ(std::string(error.what()), "callback failure"); +#ifdef _OPENMP + EXPECT_FALSE(omp_in_parallel()); +#endif + } + EXPECT_EQ(alive.load(), 0); + for (const auto& count : visits) EXPECT_EQ(count.load(), 1); +} + +TEST(SternheimerWeakQDispatch, FactoryFailurePreventsAllCallbacksAndDoesNotDeadlock) +{ + std::atomic callbacks{0}; + std::atomic alive{0}; + const int workers = +#ifdef _OPENMP + 4; +#else + 1; +#endif + EXPECT_THROW(run_sternheimer_weak_q_tasks(37, {workers, 1, workers}, + [&](int index) { + if (index == 0) throw std::logic_error("factory failure"); + ++alive; + return std::shared_ptr(new Worker(index), [&](Worker* worker) { + --alive; + delete worker; + }); + }, [&](std::int64_t, std::shared_ptr&, int) { ++callbacks; }), std::logic_error); + EXPECT_EQ(callbacks.load(), 0); + EXPECT_EQ(alive.load(), 0); +} + +TEST(SternheimerWeakQDispatch, PreservesNonStandardExceptionType) +{ + EXPECT_THROW(run_sternheimer_weak_q_tasks(2, {1, 1, 1}, + [](int) -> int { throw 17; }, [](std::int64_t, int&, int) {}), int); + EXPECT_THROW(run_sternheimer_weak_q_tasks(2, {1, 1, 1}, + [](int) { return 0; }, [](std::int64_t, int&, int) { throw 23; }), int); +} + +TEST(SternheimerWeakQDispatch, RestoresThreadSettingsOnSuccessAndBothExceptionPaths) +{ +#ifdef _OPENMP + const int previous_threads = omp_get_max_threads(); + const int previous_levels = omp_get_max_active_levels(); + const int previous_dynamic = omp_get_dynamic(); + omp_set_dynamic(1); +#endif +#ifdef __MKL + const int previous_mkl = mkl_set_num_threads_local(3); +#endif + for (int failure : {0, 1, 2}) + { + try + { + run_sternheimer_weak_q_tasks(3, {1, 2, 2}, + [&](int) { +#ifdef _OPENMP + EXPECT_EQ(omp_get_max_threads(), 2); + EXPECT_FALSE(omp_get_dynamic()); +#endif +#ifdef __MKL + EXPECT_EQ(mkl_get_max_threads(), 2); + EXPECT_EQ(mkl_set_num_threads_local(2), 2); +#endif + if (failure == 1) throw std::runtime_error("factory"); + return 0; + }, [&](std::int64_t, int&, int) { +#ifdef _OPENMP + EXPECT_FALSE(omp_get_dynamic()); +#endif +#ifdef __MKL + EXPECT_EQ(mkl_get_max_threads(), 2); + EXPECT_EQ(mkl_set_num_threads_local(2), 2); +#endif + if (failure == 2) throw std::runtime_error("callback"); + }); + EXPECT_EQ(failure, 0); + } + catch (const std::runtime_error&) { EXPECT_NE(failure, 0); } +#ifdef _OPENMP + EXPECT_EQ(omp_get_max_threads(), previous_threads); + EXPECT_EQ(omp_get_max_active_levels(), previous_levels); + EXPECT_TRUE(omp_get_dynamic()); +#endif +#ifdef __MKL + EXPECT_EQ(mkl_get_max_threads(), 3); + EXPECT_EQ(mkl_set_num_threads_local(3), 3); +#endif + } +#ifdef __MKL + EXPECT_EQ(mkl_set_num_threads_local(previous_mkl), 3); +#endif +#ifdef _OPENMP + omp_set_dynamic(previous_dynamic); +#endif +} + +#ifdef _OPENMP +TEST(SternheimerWeakQDispatch, RuntimeSerializedTeamFailsBeforeCreatingWorkers) +{ + const int previous_levels = omp_get_max_active_levels(); + const int previous_dynamic = omp_get_dynamic(); + omp_set_max_active_levels(0); + omp_set_dynamic(1); + std::atomic factories{0}; + std::atomic callbacks{0}; + EXPECT_THROW(run_sternheimer_weak_q_tasks(37, {4, 1, 4}, + [&](int) { ++factories; return 0; }, + [&](std::int64_t, int&, int) { ++callbacks; }), std::runtime_error); + EXPECT_EQ(factories.load(), 0); + EXPECT_EQ(callbacks.load(), 0); + EXPECT_EQ(omp_get_max_active_levels(), 0); + EXPECT_TRUE(omp_get_dynamic()); + omp_set_dynamic(previous_dynamic); + omp_set_max_active_levels(previous_levels); +} + +TEST(SternheimerWeakQDispatch, InnerOpenMPTeamUsesRequestedThreadCount) +{ + const int previous_dynamic = omp_get_dynamic(); + omp_set_dynamic(1); + run_sternheimer_weak_q_tasks(8, {4, 2, 8}, [](int) { return 0; }, + [](std::int64_t, int&, int) { + EXPECT_EQ(omp_get_num_threads(), 4); + EXPECT_FALSE(omp_get_dynamic()); + int inner_size = 0; +#pragma omp parallel shared(inner_size) + { +#pragma omp single + inner_size = omp_get_num_threads(); + } + EXPECT_EQ(inner_size, 2); + }); + EXPECT_TRUE(omp_get_dynamic()); + omp_set_dynamic(previous_dynamic); +} +#endif + +#ifdef __MKL +TEST(SternheimerWeakQDispatch, RestoresMklGlobalDefaultSentinel) +{ + const int previous_mkl = mkl_set_num_threads_local(0); + for (int failure : {0, 1, 2}) + { + try + { + run_sternheimer_weak_q_tasks(1, {1, 2, 2}, + [&](int) { + EXPECT_EQ(mkl_get_max_threads(), 2); + EXPECT_EQ(mkl_set_num_threads_local(2), 2); + if (failure == 1) throw std::runtime_error("factory"); + return 0; + }, [&](std::int64_t, int&, int) { + EXPECT_EQ(mkl_get_max_threads(), 2); + EXPECT_EQ(mkl_set_num_threads_local(2), 2); + if (failure == 2) throw std::runtime_error("callback"); + }); + EXPECT_EQ(failure, 0); + } + catch (const std::runtime_error&) { EXPECT_NE(failure, 0); } + EXPECT_EQ(mkl_set_num_threads_local(0), 0); + } + EXPECT_EQ(mkl_set_num_threads_local(previous_mkl), 0); +} +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_reference_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_reference_test.cpp new file mode 100644 index 00000000000..fb58d44a1f8 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_reference_test.cpp @@ -0,0 +1,405 @@ +#include + +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_reference.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using namespace ModuleRI; +using Bytes = std::vector; + +class SternheimerWeakQReferenceTest : public ::testing::Test +{ + protected: + std::string directory; + std::string path; + const std::string contract = std::string(64, 'a'); + SternheimerWeakQReferenceDimensions dimensions{3, 2, 2}; + std::vector potential{1.25, -0.0, -2.5}; + std::vector records; + + void SetUp() override + { + char pattern[] = "/tmp/st-weak-q-reference-XXXXXX"; + const char* made = ::mkdtemp(pattern); + ASSERT_NE(made, nullptr); + directory = made; + path = directory + "/reference.bin"; + SternheimerLCAOOccupiedKPoint r; + r.local_k_index = -1; + r.global_k_index = 4; + r.zero_order_k_index = 2; + r.symmetry_spatial_isym = 7; + r.symmetry_time_reversal = true; + r.spin_index = 1; + r.kpoint = {0.25, -0.5, -0.0}; + r.has_grid_kpoint_override = true; + r.grid_kpoint = {-0.75, 0.5, 1.0}; + r.kweight = 0.125; + r.eigenvalues = {-1.25, -0.5}; + r.occupations = {2.0, 0.25}; + r.coefficients = {{{0.25, -0.0}, {1.5, -2}}, {{-3, 4}, {5, 6}}}; + r.unoccupied_eigenvalues = {0.25}; + r.unoccupied_coefficients = {{{7, 8}, {-9, -10}}}; + records.push_back(r); + r.local_k_index = 3; + r.global_k_index = 5; + r.symmetry_time_reversal = false; + r.has_grid_kpoint_override = false; + r.unoccupied_eigenvalues.clear(); + r.unoccupied_coefficients.clear(); + records.push_back(r); + } + + void TearDown() override + { + std::remove(path.c_str()); + std::remove((directory + "/copy.bin").c_str()); + if (!directory.empty()) EXPECT_EQ(::rmdir(directory.c_str()), 0); + } + + void write() + { + write_sternheimer_weak_q_reference(path, contract, dimensions, potential, records); + } + + SternheimerWeakQReference read() + { + return read_sternheimer_weak_q_reference(path, contract, dimensions); + } + + Bytes bytes() const + { + std::ifstream in(path, std::ios::binary); + return Bytes(std::istreambuf_iterator(in), std::istreambuf_iterator()); + } + + void replace(const Bytes& data) + { + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(reinterpret_cast(data.data()), data.size()); + ASSERT_TRUE(out.good()); + } + + static void put64(Bytes& data, const std::size_t offset, const std::uint64_t value) + { + for (unsigned i = 0; i < 8; ++i) data.at(offset + i) = (value >> (8 * i)) & 255; + } + + static void reseal(Bytes& data) + { + ASSERT_GE(data.size(), 64U); + module_ri::sternheimer_siab::Sha256 sha; + sha.update(data.data(), data.size() - 64); + const auto digest = sha.finish(); + std::copy(digest.begin(), digest.end(), data.end() - 64); + } + + std::size_t dimension_offset() const { return 8 + 4 + 8 + contract.size(); } + std::size_t potential_offset() const { return dimension_offset() + 24; } + std::size_t record_offset() const { return potential_offset() + 8 * potential.size(); } +}; + +void expect_bits(const double actual, const double expected) +{ + EXPECT_EQ(std::memcmp(&actual, &expected, sizeof(double)), 0); +} + +void expect_reals(const std::vector& actual, const std::vector& expected) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t i = 0; i < actual.size(); ++i) expect_bits(actual[i], expected[i]); +} + +void expect_coefficients(const std::vector>>& actual, + const std::vector>>& expected) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t i = 0; i < actual.size(); ++i) + { + ASSERT_EQ(actual[i].size(), expected[i].size()); + for (std::size_t j = 0; j < actual[i].size(); ++j) + { + expect_bits(actual[i][j].real(), expected[i][j].real()); + expect_bits(actual[i][j].imag(), expected[i][j].imag()); + } + } +} + +TEST_F(SternheimerWeakQReferenceTest, RoundTripsEveryActualFieldBitExactly) +{ + write(); + const auto restored = read(); + expect_reals(restored.fine_potential, potential); + ASSERT_EQ(restored.full_records.size(), records.size()); + for (std::size_t i = 0; i < records.size(); ++i) + { + const auto& a = restored.full_records[i]; + const auto& b = records[i]; + EXPECT_EQ(a.local_k_index, b.local_k_index); + EXPECT_EQ(a.global_k_index, b.global_k_index); + EXPECT_EQ(a.zero_order_k_index, b.zero_order_k_index); + EXPECT_EQ(a.symmetry_spatial_isym, b.symmetry_spatial_isym); + EXPECT_EQ(a.symmetry_time_reversal, b.symmetry_time_reversal); + EXPECT_EQ(a.spin_index, b.spin_index); + EXPECT_EQ(a.has_grid_kpoint_override, b.has_grid_kpoint_override); + for (std::size_t j = 0; j < 3; ++j) + { + expect_bits(a.kpoint[j], b.kpoint[j]); + expect_bits(a.grid_kpoint[j], b.grid_kpoint[j]); + } + expect_bits(a.kweight, b.kweight); + expect_reals(a.eigenvalues, b.eigenvalues); + expect_reals(a.occupations, b.occupations); + expect_coefficients(a.coefficients, b.coefficients); + expect_reals(a.unoccupied_eigenvalues, b.unoccupied_eigenvalues); + expect_coefficients(a.unoccupied_coefficients, b.unoccupied_coefficients); + } + const auto original = bytes(); + write_sternheimer_weak_q_reference(directory + "/copy.bin", contract, dimensions, + restored.fine_potential, restored.full_records); + std::ifstream copy(directory + "/copy.bin", std::ios::binary); + EXPECT_EQ(original, Bytes(std::istreambuf_iterator(copy), std::istreambuf_iterator())); +} + +TEST_F(SternheimerWeakQReferenceTest, RejectsWrongContractAndEachDimension) +{ + write(); + EXPECT_THROW(read_sternheimer_weak_q_reference(path, std::string(64, 'b'), dimensions), std::runtime_error); + for (const auto d : {SternheimerWeakQReferenceDimensions{4, 2, 2}, + SternheimerWeakQReferenceDimensions{3, 3, 2}, + SternheimerWeakQReferenceDimensions{3, 2, 3}}) + EXPECT_THROW(read_sternheimer_weak_q_reference(path, contract, d), std::runtime_error); +} + +TEST_F(SternheimerWeakQReferenceTest, CallerReplacesBothIndependentSCFCopiesBeforeHashing) +{ + write(); + auto independent_potential = potential; + auto independent_records = records; + independent_potential[0] = std::nextafter(potential[0], 2.0); + independent_records[0].eigenvalues[0] = std::nextafter(records[0].eigenvalues[0], 0.0); + independent_records[0].coefficients[0][0] = {0.25, 1e-16}; + auto reference = read(); + independent_potential.swap(reference.fine_potential); + independent_records.swap(reference.full_records); + expect_reals(independent_potential, potential); + expect_reals(independent_records[0].eigenvalues, records[0].eigenvalues); + expect_coefficients(independent_records[0].coefficients, records[0].coefficients); + // A caller's previous reference-hash calculation belongs after both swaps. + write_sternheimer_weak_q_reference(directory + "/copy.bin", contract, dimensions, + independent_potential, independent_records); + EXPECT_EQ(module_ri::sternheimer_siab::sha256_file(path), + module_ri::sternheimer_siab::sha256_file(directory + "/copy.bin")); +} + +TEST_F(SternheimerWeakQReferenceTest, RejectsEmptyContractsZeroDimensionsAndNonRegularFiles) +{ + EXPECT_THROW(write_sternheimer_weak_q_reference(path, "", dimensions, potential, records), std::runtime_error); + EXPECT_THROW(write_sternheimer_weak_q_reference(path, std::string(65537, 'a'), dimensions, + potential, records), std::runtime_error); + for (const auto d : {SternheimerWeakQReferenceDimensions{0, 2, 2}, + SternheimerWeakQReferenceDimensions{3, 0, 2}, + SternheimerWeakQReferenceDimensions{3, 2, 0}}) + EXPECT_THROW(write_sternheimer_weak_q_reference(path, contract, d, potential, records), std::runtime_error); + EXPECT_THROW(read(), std::runtime_error); + ASSERT_EQ(::mkfifo(path.c_str(), 0600), 0); + EXPECT_THROW(read(), std::runtime_error); + EXPECT_THROW(read_sternheimer_weak_q_reference(directory, contract, dimensions), std::runtime_error); +} + +TEST_F(SternheimerWeakQReferenceTest, PreservesExtremeIndicesAndEmptyBandLists) +{ + records[0].local_k_index = std::numeric_limits::min(); + records[0].global_k_index = std::numeric_limits::max(); + records[1].eigenvalues.clear(); + records[1].occupations.clear(); + records[1].coefficients.clear(); + write(); + const auto restored = read(); + EXPECT_EQ(restored.full_records[0].local_k_index, std::numeric_limits::min()); + EXPECT_EQ(restored.full_records[0].global_k_index, std::numeric_limits::max()); + EXPECT_TRUE(restored.full_records[1].eigenvalues.empty()); + EXPECT_TRUE(restored.full_records[1].occupations.empty()); + EXPECT_TRUE(restored.full_records[1].coefficients.empty()); +} + +TEST_F(SternheimerWeakQReferenceTest, RejectsEveryTruncationCorruptionAndTrailingBytes) +{ + write(); + const auto original = bytes(); + for (std::size_t n = 0; n < original.size(); ++n) + { + SCOPED_TRACE(n); + replace(Bytes(original.begin(), original.begin() + n)); + EXPECT_THROW(read(), std::runtime_error); + } + for (std::size_t i = 0; i < original.size(); ++i) + { + SCOPED_TRACE(i); + auto changed = original; + changed[i] ^= 1; + replace(changed); + EXPECT_THROW(read(), std::runtime_error); + } + auto trailing = original; + trailing.push_back(0); + replace(trailing); + EXPECT_THROW(read(), std::runtime_error); + trailing = original; + trailing.insert(trailing.end() - 64, 0); + reseal(trailing); + replace(trailing); + EXPECT_THROW(read(), std::runtime_error); +} + +TEST_F(SternheimerWeakQReferenceTest, RejectsResealedUnknownSchemaBooleanCountsAndNonfiniteValues) +{ + write(); + const auto original = bytes(); + for (const auto offset : {std::size_t(0), std::size_t(8), record_offset() + 20, record_offset() + 21}) + { + auto changed = original; + changed[offset] = 42; + reseal(changed); + replace(changed); + EXPECT_THROW(read(), std::runtime_error); + } + for (const auto offset : {std::size_t(12), dimension_offset(), dimension_offset() + 8, + dimension_offset() + 16, record_offset() + 78, record_offset() + 86}) + { + auto changed = original; + put64(changed, offset, std::numeric_limits::max()); + reseal(changed); + replace(changed); + EXPECT_THROW(read(), std::runtime_error); + } + // Potential, both k vectors, weight, eigenvalue, occupation, complex parts, virtual eigenvalue. + for (const auto offset : {potential_offset(), record_offset() + 22, record_offset() + 46, + record_offset() + 70, record_offset() + 94, record_offset() + 110, + record_offset() + 126, record_offset() + 134, record_offset() + 190, + record_offset() + 198, record_offset() + 206}) + { + for (const auto bits : {UINT64_C(0x7ff0000000000000), UINT64_C(0x7ff8000000000001)}) + { + auto changed = original; + put64(changed, offset, bits); + reseal(changed); + replace(changed); + EXPECT_THROW(read(), std::runtime_error); + } + } +} + +TEST_F(SternheimerWeakQReferenceTest, RejectsHugeDimensionsEvenWhenCallerExpectsThem) +{ + write(); + const auto original = bytes(); + const auto huge = UINT64_C(1) << 40; + for (int field = 0; field < 3; ++field) + { + auto changed = original; + auto expected = dimensions; + if (field == 0) expected.fine_grid_size = huge; + if (field == 1) expected.full_record_count = huge; + if (field == 2) expected.orbital_count = huge; + put64(changed, dimension_offset() + 8 * field, huge); + reseal(changed); + replace(changed); + EXPECT_THROW(read_sternheimer_weak_q_reference(path, contract, expected), std::runtime_error); + } + // Below the default band limit: rejection must also use available file bytes. + auto changed = original; + put64(changed, record_offset() + 78, 65536); + reseal(changed); + replace(changed); + EXPECT_THROW(read(), std::runtime_error); +} + +TEST_F(SternheimerWeakQReferenceTest, EnforcesReadAndWriteBudgetsWithoutPublishingPartialFiles) +{ + write(); + const auto original = bytes(); + for (int mode = 0; mode < 3; ++mode) + { + SternheimerWeakQReferenceLimits limits; + if (mode == 0) limits.max_file_bytes = original.size() - 1; + if (mode == 1) limits.max_allocation_bytes = sizeof(SternheimerLCAOOccupiedKPoint); + if (mode == 2) limits.max_bands_per_record = 1; + EXPECT_THROW(read_sternheimer_weak_q_reference(path, contract, dimensions, limits), std::runtime_error); + EXPECT_THROW(write_sternheimer_weak_q_reference(directory + "/copy.bin", contract, dimensions, + potential, records, limits), std::runtime_error); + EXPECT_NE(::access((directory + "/copy.bin").c_str(), F_OK), 0); + } + EXPECT_EQ(bytes(), original); +} + +TEST_F(SternheimerWeakQReferenceTest, RejectsInvalidExporterData) +{ + records[0].occupations.pop_back(); + EXPECT_THROW(write(), std::runtime_error); + records[0].occupations.push_back(0.25); + records[0].coefficients[0].pop_back(); + EXPECT_THROW(write(), std::runtime_error); + records[0].coefficients[0].push_back({1.5, -2}); + records[0].unoccupied_coefficients.clear(); + EXPECT_THROW(write(), std::runtime_error); + records[0].unoccupied_coefficients = {{{7, 8}, {-9, -10}}}; + records[0].grid_kpoint[2] = std::numeric_limits::infinity(); + EXPECT_THROW(write(), std::runtime_error); + records[0].grid_kpoint[2] = 1.0; + potential[0] = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(write(), std::runtime_error); + potential[0] = 1.25; + records[0].coefficients[0][0] = {0, std::numeric_limits::infinity()}; + EXPECT_THROW(write(), std::runtime_error); + EXPECT_NE(::access(path.c_str(), F_OK), 0); +} + +TEST_F(SternheimerWeakQReferenceTest, NeverOverwritesExistingFileOrSymlink) +{ + write(); + const auto original = bytes(); + potential[0] = 99; + EXPECT_THROW(write(), std::runtime_error); + EXPECT_EQ(bytes(), original); + ASSERT_EQ(::symlink("missing-target", (directory + "/copy.bin").c_str()), 0); + EXPECT_THROW(write_sternheimer_weak_q_reference(directory + "/copy.bin", contract, dimensions, + potential, records), std::runtime_error); + struct stat st; + ASSERT_EQ(::lstat((directory + "/copy.bin").c_str(), &st), 0); + EXPECT_TRUE(S_ISLNK(st.st_mode)); +} + +TEST_F(SternheimerWeakQReferenceTest, ConcurrentPublishHasExactlyOneWinner) +{ + const pid_t child = ::fork(); + ASSERT_GE(child, 0); + if (child == 0) + { + try { write(); ::_exit(0); } + catch (...) { ::_exit(1); } + } + int parent_success = 0; + try { write(); parent_success = 1; } + catch (const std::runtime_error&) {} + int status = 0; + ASSERT_EQ(::waitpid(child, &status, 0), child); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(parent_success + (WEXITSTATUS(status) == 0 ? 1 : 0), 1); + EXPECT_NO_THROW(read()); +} +} // namespace diff --git a/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_unit_test.cpp b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_unit_test.cpp new file mode 100644 index 00000000000..2cf11943655 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/sternheimer_weak_q_unit_test.cpp @@ -0,0 +1,212 @@ +#include + +#if __has_include("source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_unit.h") +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_q_unit.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_weak_augmented.h" + +#include + +using ModuleRI::SternheimerWeakQUnit; + +TEST(SternheimerWeakQUnit, DefaultsCoverSelectedFullKAllBandsAndFrequencies) +{ + SternheimerWeakQUnit::Raw raw; + raw.source_k = "2"; + const auto unit = SternheimerWeakQUnit::parse(raw, {20, 18, 20}, 6, 1113); + EXPECT_EQ(unit.source_k, 2); + EXPECT_EQ(unit.band_begin, 1); + EXPECT_EQ(unit.band_end, 18); + EXPECT_EQ(unit.frequency_begin, 1); + EXPECT_EQ(unit.frequency_end, 6); + EXPECT_TRUE(unit.all_source_bands(18)); + EXPECT_TRUE(unit.all_frequencies(6)); + EXPECT_EQ(unit.columns().size(), 1113U); + EXPECT_EQ(unit.expected_equations(), 2ULL * 18 * 6 * 1113); +} + +TEST(SternheimerWeakQUnit, OneBandPilotAndUnevenColumnShards) +{ + SternheimerWeakQUnit::Raw raw; + raw.source_k = "16"; + raw.band_begin = "20"; + raw.band_end = "20"; + raw.frequency_begin = "2"; + raw.frequency_end = "3"; + raw.shard_index = "15"; + raw.shard_count = "16"; + const auto unit = SternheimerWeakQUnit::parse(raw, std::vector(16, 20), 6, 1113); + EXPECT_FALSE(unit.all_source_bands(20)); + EXPECT_FALSE(unit.all_frequencies(6)); + EXPECT_EQ(unit.columns().front(), 15); + EXPECT_EQ(unit.columns().back(), 1103); + EXPECT_EQ(unit.columns().size(), 69U); + EXPECT_EQ(unit.expected_equations(), 276U); +} + +TEST(SternheimerWeakQUnit, RejectsMalformedAndOutOfBoundsSelections) +{ + for (const char* value : {"", "0", "-1", "+1", " 1", "1x", "2147483648", "3"}) + { + SternheimerWeakQUnit::Raw raw; + raw.source_k = value; + EXPECT_THROW(SternheimerWeakQUnit::parse(raw, {2, 3}, 6, 5), std::invalid_argument); + } + SternheimerWeakQUnit::Raw raw; + raw.band_begin = "3"; + EXPECT_THROW(SternheimerWeakQUnit::parse(raw, {2}, 6, 5), std::invalid_argument); + raw.band_begin = "2"; + raw.band_end = "1"; + EXPECT_THROW(SternheimerWeakQUnit::parse(raw, {2}, 6, 5), std::invalid_argument); + raw = {}; + raw.frequency_end = "7"; + EXPECT_THROW(SternheimerWeakQUnit::parse(raw, {2}, 6, 5), std::invalid_argument); + raw.frequency_end = "0"; + EXPECT_THROW(SternheimerWeakQUnit::parse(raw, {2}, 6, 5), std::invalid_argument); + raw = {}; + raw.shard_index = "2"; + raw.shard_count = "2"; + EXPECT_THROW(SternheimerWeakQUnit::parse(raw, {2}, 6, 5), std::invalid_argument); + EXPECT_THROW(SternheimerWeakQUnit::parse({}, {}, 6, 5), std::invalid_argument); + EXPECT_THROW(SternheimerWeakQUnit::parse({}, {0}, 6, 5), std::invalid_argument); + EXPECT_THROW(SternheimerWeakQUnit::parse({}, {2}, 0, 5), std::invalid_argument); +} + +TEST(SternheimerWeakQUnit, CanonicalTargetAdmitsIntegerFoldWithoutChangingFields) +{ + using Point = std::array; + using Shift = std::array; + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_weak_q_fold( + Point{0.25, -0.5, 0.0}, Point{0.25, -0.25, 0.0}, Point{-0.5, 0.25, 0.0}, Shift{1, -1, 0})); + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_weak_q_fold( + Point{0, 0, 0}, Point{0.25, 0, 0}, Point{0.25, 0, 0}, Shift{0, 0, 0})); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_fold( + Point{0.25, 0, 0}, Point{0.25, 0, 0}, Point{-0.5, 0, 0}, Shift{0, 0, 0}), std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_fold( + Point{0.25, 0, 0}, Point{0.25, 0, 0}, Point{-0.49, 0, 0}, Shift{1, 0, 0}), std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_fold( + Point{std::numeric_limits::quiet_NaN(), 0, 0}, Point{}, Point{}, Shift{}), std::invalid_argument); +} + +TEST(SternheimerWeakQUnit, HaRyAndSpinWeightedColumnMajorSignedSum) +{ + using Complex = std::complex; + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_weak_q_omega_ry(0.25), 0.5); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_weak_q_left_vertex_factor(), 0.5); + const double weight = ModuleRI::sternheimer_weak_q_weight(2.0 / 16, 1.0); + EXPECT_DOUBLE_EQ(weight, 0.125); + std::vector result(6, Complex(0.0)); + const std::vector plus{{1, 2}, {3, -4}, {5, 6}}; + const std::vector minus{{7, 8}, {9, 10}, {11, -12}}; + ModuleRI::accumulate_sternheimer_weak_q_column(result, plus, 3, 1, weight); + ModuleRI::accumulate_sternheimer_weak_q_column(result, minus, 3, 1, weight); + for (int row = 0; row < 3; ++row) + { + EXPECT_EQ(result[row], Complex(0)); + EXPECT_EQ(result[row + 3], weight * (plus[row] + minus[row])); + } + EXPECT_THROW(ModuleRI::sternheimer_weak_q_weight(-0.1, 1), std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_weak_q_weight(0.1, 0.5), std::invalid_argument); + EXPECT_THROW(ModuleRI::sternheimer_weak_q_omega_ry(0), std::invalid_argument); + EXPECT_THROW(ModuleRI::accumulate_sternheimer_weak_q_column(result, plus, 3, 2, weight), std::invalid_argument); + auto bad = plus; + bad[1] = {std::numeric_limits::infinity(), 0}; + EXPECT_THROW(ModuleRI::accumulate_sternheimer_weak_q_column(result, bad, 3, 1, weight), std::invalid_argument); +} + +TEST(SternheimerWeakQUnit, OriginalResidualMustBeFiniteAndWithinTolerance) +{ + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_weak_q_residual(true, 1e-8, 2e-8, 1e-8)); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_residual(false, 0, 0, 1e-8), std::runtime_error); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_residual(true, 1.01e-8, 1e-9, 1e-8), + std::runtime_error); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_residual( + true, std::numeric_limits::quiet_NaN(), 0, 1e-8), std::runtime_error); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_residual(true, 0, -1, 1e-8), std::runtime_error); +} + +TEST(SternheimerWeakQUnit, OriginalResidualUsesRequestedTolerance) +{ + EXPECT_NO_THROW(ModuleRI::validate_sternheimer_weak_q_residual(true, 5e-7, 2e-8, 1e-6)); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_residual(true, 1.01e-6, 1e-9, 1e-6), + std::runtime_error); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_residual( + true, 1e-8, 1e-9, std::numeric_limits::quiet_NaN()), + std::invalid_argument); + EXPECT_THROW(ModuleRI::validate_sternheimer_weak_q_residual(true, 1e-8, 1e-9, 0), + std::invalid_argument); +} + +TEST(SternheimerWeakQUnit, ActualCompactDensitySupportIncludesLastInterpolationInterval) +{ + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_weak_q_radial_support({0, 1, 2, 3, 4}, {1, 0.2, 0, 0, 0}), 2.0); + EXPECT_DOUBLE_EQ(ModuleRI::sternheimer_weak_q_radial_support({0, 1, 2}, {1, 0.2, 0.1}), 2.0); + EXPECT_TRUE(ModuleRI::sternheimer_weak_q_z_support_contained(10, 2, 20)); + EXPECT_FALSE(ModuleRI::sternheimer_weak_q_z_support_contained(1, 2, 20)); + EXPECT_FALSE(ModuleRI::sternheimer_weak_q_z_support_contained(19, 2, 20)); + EXPECT_FALSE(ModuleRI::sternheimer_weak_q_z_support_contained(-1, 0.5, 20)); + EXPECT_THROW(ModuleRI::sternheimer_weak_q_radial_support({0, 2, 1}, {1, 0, 0}), std::invalid_argument); +} + +TEST(SternheimerWeakQUnit, BothSignedSolvesMatchDirectResolventWithUnrotatedSourceEnergies) +{ + using Blocks = ModuleRI::SternheimerWeakAugmented; + using Complex = Blocks::Complex; + Blocks::Data data; + data.nocc = 1; + data.nvirtual = 2; + data.ncoarse = 0; + data.hu = {-2.0, 0.0, 0.0, 0.0, 1.5, 0.0, 0.0, 0.0, 3.0}; + auto blocks = std::make_shared(std::move(data)); + const std::array source_energies{-0.7, -1.2}; // Deliberately not energy-sorted. + const std::array target_energies{1.5, 3.0}; + const double omega_ha = 0.3; + const double omega_ry = ModuleRI::sternheimer_weak_q_omega_ry(omega_ha); + const double weight = ModuleRI::sternheimer_weak_q_weight(2.0 / 16, 1.0); + Blocks::Vector actual(4, 0.0), expected(4, 0.0); + for (int band = 0; band != 2; ++band) + { + const double amplitude = band + 1.0; + const std::array vertices{{ + {amplitude * Complex(0.3, 0.7), Complex(-0.8, 0.2)}, + {Complex(0.6, -0.5), amplitude * Complex(0.4, 0.9)}}}; + for (int sign : {1, -1}) + { + Blocks::Worker worker(blocks, [](const auto&, auto& y) { y.clear(); }, + source_energies[band], sign * omega_ry); + for (int j = 0; j != 2; ++j) + { + ModuleRI::SternheimerRPA::SolverOptions options; + options.residual_tol = 1e-8; + const auto solved = worker.solve({vertices[j], {}}, options); + ASSERT_TRUE(solved.converged); + ModuleRI::validate_sternheimer_weak_q_residual( + solved.converged, solved.relative_residual, solved.absolute_residual, options.residual_tol); + Blocks::Vector column(2, 0.0); + for (int i = 0; i != 2; ++i) + for (int a = 0; a != 2; ++a) + column[i] += ModuleRI::sternheimer_weak_q_left_vertex_factor() + * std::conj(vertices[i][a]) * solved.coefficients[a]; + ModuleRI::accumulate_sternheimer_weak_q_column(actual, column, 2, j, weight); + } + } + for (int j = 0; j != 2; ++j) + for (int i = 0; i != 2; ++i) + for (int a = 0; a != 2; ++a) + { + const double gap = target_energies[a] - source_energies[band]; + expected[i + 2 * j] -= weight * gap / (gap * gap + omega_ry * omega_ry) + * std::conj(vertices[i][a]) * vertices[j][a]; + } + } + for (int i = 0; i != 4; ++i) EXPECT_NEAR(std::abs(actual[i] - expected[i]), 0.0, 1e-12); + EXPECT_GT(std::abs(expected[1].imag()), 1e-3); + EXPECT_LT(actual[0].real(), 0.0); + // A second B+B^dagger would fail this direct normalization test by a factor of two. + EXPECT_GT(std::abs(2.0 * actual[0] - expected[0]), 1e-3); +} +#else +TEST(SternheimerWeakQUnit, MissingImplementationIsRed) +{ + FAIL() << "Missing sternheimer_weak_q_unit.h: test-first remote RED snapshot"; +} +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_mpi.cpp b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_mpi.cpp new file mode 100644 index 00000000000..1acd86d8939 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_mpi.cpp @@ -0,0 +1,379 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_chi0_mpi.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_grid_diagnostics.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_mpi.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_rpa.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __MPI +#include +#endif + +namespace +{ + +namespace siab = module_ri::sternheimer_siab; +using Complex = std::complex; + +std::string read_text(const std::string& path) +{ + std::ifstream input(path.c_str(), std::ios::binary); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); +} + +siab::Provenance test_provenance() +{ + siab::Provenance provenance; + provenance.abacus_commit = "19ab21e01d02cc805604ed77a6e269af698fdd1d"; + provenance.auxiliary_basis_sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + provenance.cell_bohr = {8.0, 0.0, 0.0, 0.0, 8.0, 0.0, 0.0, 0.0, 8.0}; + provenance.ecut_ry = 25.0; + provenance.kernel = "full_coulomb"; + provenance.orbital_sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + provenance.pseudopotential_sha256 = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + provenance.spin_convention = "occupation_in_metadata"; + return provenance; +} + +siab::ReferenceRow make_row(const int frequency_index, + const double frequency_ha, + const std::vector& y, + const std::vector>& primitives) +{ + siab::ReferenceRow row; + row.occupied_state = 0; + row.auxiliary_channel = 0; + row.frequency_index = frequency_index; + row.frequency_ha = frequency_ha; + row.occupation = 2.0; + row.frequency_weight = 0.25 + 0.5 * frequency_index; + row.norm = siab::norm(y, 0.5); + row.q = siab::overlap_q(y, primitives, 0.5); + return row; +} + +siab::ReferenceRow make_global_row(const int occupied_state, + const int frequency_index, + const int auxiliary_channel) +{ + const double tag = 100.0 * occupied_state + 10.0 * frequency_index + auxiliary_channel; + siab::ReferenceRow row; + row.occupied_state = occupied_state; + row.auxiliary_channel = auxiliary_channel; + row.frequency_index = frequency_index; + row.frequency_ha = 0.2 + 0.6 * frequency_index; + row.occupation = 2.0; + row.frequency_weight = 0.25 + 0.5 * frequency_index; + row.norm = 1.0 + tag; + row.q = {Complex(tag + 1.0, -tag), Complex(0.5 * tag, tag + 2.0)}; + return row; +} + +TEST(SternheimerSIABMPI, ReassemblesPWSlabsInFullGridOrder) +{ + siab::PrimitiveSlab rank0; + rank0.startz = 0; + rank0.nplane = 1; + rank0.values = {{Complex(1.0, 0.5), Complex(3.0, 1.5)}, {Complex(10.0, -0.5), Complex(30.0, -1.5)}}; + + siab::PrimitiveSlab rank1; + rank1.startz = 1; + rank1.nplane = 1; + rank1.values = {{Complex(2.0, 1.0), Complex(4.0, 2.0)}, {Complex(20.0, -1.0), Complex(40.0, -2.0)}}; + + const auto full = siab::assemble_full_primitive_grids({rank1, rank0}, 2, 2); + ASSERT_EQ(full.size(), 2); + EXPECT_EQ(full[0], (std::vector{{1.0, 0.5}, {2.0, 1.0}, {3.0, 1.5}, {4.0, 2.0}})); + EXPECT_EQ(full[1], (std::vector{{10.0, -0.5}, {20.0, -1.0}, {30.0, -1.5}, {40.0, -2.0}})); +} + +TEST(SternheimerSIABMPI, RejectsDuplicateOrMissingPWPlanes) +{ + siab::PrimitiveSlab first; + first.startz = 0; + first.nplane = 1; + first.values = {{Complex(1.0, 0.0)}}; + + siab::PrimitiveSlab duplicate = first; + EXPECT_THROW(siab::assemble_full_primitive_grids({first, duplicate}, 1, 2), std::invalid_argument); + EXPECT_THROW(siab::assemble_full_primitive_grids({first}, 1, 2), std::invalid_argument); +} + +#ifdef __MPI +TEST(SternheimerSIABMPI, FullPrimitiveAndVariableRowsMatchSerialWithoutDoubleCounting) +{ + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + if (size != 2) + { + GTEST_SKIP() << "This regression requires exactly two MPI ranks."; + } + + // PW local ordering is [ixy][local iz]. Each rank owns one z plane. + const int startz = rank; + const int nplane = 1; + std::vector> local_primitives(2, std::vector(2)); + local_primitives[0][0] = Complex(1.0 + rank, 0.5 * (rank + 1)); + local_primitives[0][1] = Complex(3.0 + rank, 1.5 + 0.5 * rank); + local_primitives[1][0] = Complex(10.0 * (rank + 1), -0.5 * (rank + 1)); + local_primitives[1][1] = Complex(30.0 + 10.0 * rank, -1.5 - 0.5 * rank); + + const auto full_primitives + = siab::allgather_full_primitive_grids(local_primitives, 2, 2, startz, nplane, MPI_COMM_WORLD); + ASSERT_EQ(full_primitives.size(), 2); + ASSERT_EQ(full_primitives[0].size(), 4); + + const std::vector y0{{1.0, 1.0}, {2.0, -1.0}, {0.5, 0.25}, {-1.0, 0.75}}; + const std::vector y1{{-0.5, 0.5}, {1.5, 0.25}, {2.0, -0.5}, {0.25, 1.0}}; + + // Deliberately reverse owner/frequency order. Y is replicated and complete; + // only the owning rank forms its row, with no overlap Allreduce. + std::vector local_rows; + if (rank == 0) + { + local_rows.push_back(make_row(1, 0.8, y1, full_primitives)); + } + else + { + local_rows.push_back(make_row(0, 0.2, y0, full_primitives)); + } + + const auto gathered = siab::gather_reference_rows_to_root(local_rows, 2, 0, MPI_COMM_WORLD); + if (rank == 0) + { + ASSERT_EQ(gathered.size(), 2); + EXPECT_EQ(gathered[0].frequency_index, 0); + EXPECT_EQ(gathered[1].frequency_index, 1); + + const auto expected0 = make_row(0, 0.2, y0, full_primitives); + const auto expected1 = make_row(1, 0.8, y1, full_primitives); + EXPECT_NEAR(gathered[0].norm, expected0.norm, 1.0e-12); + EXPECT_NEAR(gathered[1].norm, expected1.norm, 1.0e-12); + for (std::size_t ie = 0; ie != 2; ++ie) + { + EXPECT_NEAR(std::abs(gathered[0].q[ie] - expected0.q[ie]), 0.0, 1.0e-12); + EXPECT_NEAR(std::abs(gathered[1].q[ie] - expected1.q[ie]), 0.0, 1.0e-12); + } + + const std::vector blocks{{"H", 0, 0, 0, 2, 0}}; + const std::vector overlap_s = siab::overlap_s(full_primitives, 0.5); + const std::string mpi_path = "sternheimer_siab_mpi_rows.dat"; + const std::string serial_path = "sternheimer_siab_serial_rows.dat"; + siab::write_v1(mpi_path, 0.5, blocks, gathered, overlap_s, test_provenance()); + siab::write_v1(serial_path, 0.5, blocks, {expected1, expected0}, overlap_s, test_provenance()); + EXPECT_EQ(read_text(mpi_path), read_text(serial_path)); + std::remove(mpi_path.c_str()); + std::remove(serial_path.c_str()); + } + else + { + EXPECT_TRUE(gathered.empty()); + } +} + +TEST(SternheimerSIABMPI, SupportsRankWithNoLocalReferenceRows) +{ + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + if (size != 2) + { + GTEST_SKIP() << "This regression requires exactly two MPI ranks."; + } + + std::vector local_rows; + if (rank == 1) + { + const std::vector> primitive{{Complex(1.0, 0.0)}}; + local_rows.push_back(make_row(1, 0.8, {Complex(2.0, 0.0)}, primitive)); + local_rows.push_back(make_row(0, 0.2, {Complex(1.0, 0.0)}, primitive)); + } + + const auto gathered = siab::gather_reference_rows_to_root(local_rows, 1, 0, MPI_COMM_WORLD); + if (rank == 0) + { + ASSERT_EQ(gathered.size(), 2); + EXPECT_EQ(gathered[0].frequency_index, 0); + EXPECT_EQ(gathered[1].frequency_index, 1); + } +} + +TEST(SternheimerSIABMPI, GlobalEquationOwnershipWritesCanonicalSerialFile) +{ + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + if (size != 2) + { + GTEST_SKIP() << "This regression requires exactly two MPI ranks."; + } + + constexpr int occupied_count = 2; + constexpr int frequency_count = 2; + constexpr int channel_count = 4; + std::vector local_rows; + std::vector serial_rows; + for (int occupied = occupied_count - 1; occupied >= 0; --occupied) + { + for (int frequency = frequency_count - 1; frequency >= 0; --frequency) + { + for (int channel = channel_count - 1; channel >= 0; --channel) + { + const siab::ReferenceRow row = make_global_row(occupied, frequency, channel); + serial_rows.push_back(row); + const int owner = ModuleRI::SternheimerRPA::global_equation_owner(occupied, + frequency, + channel, + frequency_count, + channel_count, + size, + 1); + if (owner == rank) + { + local_rows.push_back(row); + } + } + } + } + ASSERT_EQ(local_rows.size(), 8); + + const auto gathered = siab::gather_reference_rows_to_root(local_rows, 2, 0, MPI_COMM_WORLD); + if (rank == 0) + { + ASSERT_EQ(gathered.size(), 16); + const std::vector blocks{{"H", 0, 0, 0, 2, 0}}; + const std::vector overlap_s{Complex(1.0, 0.0), + Complex(0.0, 0.0), + Complex(0.0, 0.0), + Complex(1.0, 0.0)}; + const std::string mpi_path = "sternheimer_siab_global_mpi_rows.dat"; + const std::string serial_path = "sternheimer_siab_global_serial_rows.dat"; + siab::write_v1(mpi_path, 0.5, blocks, gathered, overlap_s, test_provenance()); + siab::write_v1(serial_path, 0.5, blocks, serial_rows, overlap_s, test_provenance()); + EXPECT_EQ(read_text(mpi_path), read_text(serial_path)); + std::remove(mpi_path.c_str()); + std::remove(serial_path.c_str()); + } + else + { + EXPECT_TRUE(gathered.empty()); + } +} + +TEST(SternheimerSIABMPI, GlobalEquationChi0ReductionMatchesSerialBranch) +{ + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + if (size != 2) + { + GTEST_SKIP() << "This regression requires exactly two MPI ranks."; + } + + std::vector local_branch(4, Complex(0.0, 0.0)); + if (rank == 0) + { + local_branch[0] = Complex(1.0, 0.5); + local_branch[2] = Complex(3.0, -2.0); + } + else + { + local_branch[1] = Complex(2.0, 1.0); + local_branch[3] = Complex(4.0, -0.25); + } + + module_ri::sternheimer_chi0::reduce_branch_to_root(local_branch, 0, MPI_COMM_WORLD); + if (rank == 0) + { + const std::vector serial_branch{Complex(1.0, 0.5), + Complex(2.0, 1.0), + Complex(3.0, -2.0), + Complex(4.0, -0.25)}; + EXPECT_EQ(local_branch, serial_branch); + EXPECT_EQ(ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(local_branch, 2), + ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(serial_branch, 2)); + } + else + { + EXPECT_TRUE(local_branch.empty()); + } +} + +TEST(SternheimerSIABMPI, DeltaResponseComponentsRemainCompleteAfterMPIReduction) +{ + int rank = 0; + int size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + if (size != 2) + { + GTEST_SKIP() << "This regression requires exactly two MPI ranks."; + } + + std::vector sos(4, Complex(0.0, 0.0)); + std::vector pulay(4, Complex(0.0, 0.0)); + std::vector qspace(4, Complex(0.0, 0.0)); + std::vector total(4, Complex(0.0, 0.0)); + for (std::size_t index = static_cast(rank); index < total.size(); index += 2) + { + sos[index] = Complex(1.0 + index, 0.25 * index); + pulay[index] = Complex(-0.1 * index, 0.5 + index); + qspace[index] = Complex(0.2, -0.3 * index); + total[index] = sos[index] + pulay[index] + qspace[index]; + } + + module_ri::sternheimer_chi0::reduce_branch_to_root(total, 0, MPI_COMM_WORLD); + module_ri::sternheimer_chi0::reduce_branch_to_root(sos, 0, MPI_COMM_WORLD); + module_ri::sternheimer_chi0::reduce_branch_to_root(pulay, 0, MPI_COMM_WORLD); + module_ri::sternheimer_chi0::reduce_branch_to_root(qspace, 0, MPI_COMM_WORLD); + + if (rank == 0) + { + EXPECT_NEAR(ModuleRI::relative_component_reconstruction_error(total, sos, pulay, qspace), 0.0, 1.0e-15); + const auto total_symmetric = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(total, 2); + const auto sos_symmetric = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(sos, 2); + const auto pulay_symmetric = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(pulay, 2); + const auto qspace_symmetric = ModuleRI::SternheimerRPA::symmetrize_chi0_imaginary_frequency(qspace, 2); + EXPECT_NEAR(ModuleRI::relative_component_reconstruction_error(total_symmetric, + sos_symmetric, + pulay_symmetric, + qspace_symmetric), + 0.0, + 1.0e-15); + } + else + { + EXPECT_TRUE(total.empty()); + EXPECT_TRUE(sos.empty()); + EXPECT_TRUE(pulay.empty()); + EXPECT_TRUE(qspace.empty()); + } +} + +#endif + +} // namespace + +#ifdef __MPI +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + ::testing::InitGoogleTest(&argc, argv); + const int result = RUN_ALL_TESTS(); + MPI_Finalize(); + return result; +} +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_overlap.cpp b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_overlap.cpp new file mode 100644 index 00000000000..b264fe1f8f2 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_overlap.cpp @@ -0,0 +1,223 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_data.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_overlap.h" + +#include +#include +#include +#include +#include + +namespace +{ + +using Complex = std::complex; +using GridVector = std::vector; +using PrimitiveGrid = std::vector; +namespace siab = module_ri::sternheimer_siab; + +const GridVector reference_wavefunction = { + {1.0, 1.0}, + {2.0, 0.0}, + {0.0, -1.0}, + {1.0, 0.0}, +}; + +const PrimitiveGrid primitives = { + {{1.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}, + {{0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}, {1.0, 0.0}}, +}; + +} // namespace + +TEST(SternheimerSIABData, ExposesVersionOneFields) +{ + const siab::PrimitiveBlock block{"H", 0, 1, -1, 3, 6}; + EXPECT_EQ(block.element, "H"); + EXPECT_EQ(block.atom_index, 0); + EXPECT_EQ(block.l, 1); + EXPECT_EQ(block.m, -1); + EXPECT_EQ(block.n_primitive, 3); + EXPECT_EQ(block.offset, 6); + + const siab::ReferenceRow row{2, 4, 0.5, 1.0, 0.25, 1.5, {{0.75, -0.5}}}; + EXPECT_EQ(row.occupied_state, 2); + EXPECT_EQ(row.auxiliary_channel, 4); + EXPECT_DOUBLE_EQ(row.frequency_ha, 0.5); + EXPECT_DOUBLE_EQ(row.occupation, 1.0); + EXPECT_DOUBLE_EQ(row.frequency_weight, 0.25); + EXPECT_DOUBLE_EQ(row.norm, 1.5); + ASSERT_EQ(row.q.size(), 1); + EXPECT_EQ(row.q[0], Complex(0.75, -0.5)); +} + +TEST(SternheimerSIABOverlap, MatchesPlanFourPointFixture) +{ + constexpr double delta_omega = 0.25; + + EXPECT_NEAR(siab::norm(reference_wavefunction, delta_omega), 2.0, 1.0e-14); + + const auto q = siab::overlap_q(reference_wavefunction, primitives, delta_omega); + ASSERT_EQ(q.size(), 2); + EXPECT_NEAR(q[0].real(), 0.25, 1.0e-14); + EXPECT_NEAR(q[0].imag(), 0.00, 1.0e-14); + EXPECT_NEAR(q[1].real(), 0.75, 1.0e-14); + EXPECT_NEAR(q[1].imag(), 0.00, 1.0e-14); +} + +TEST(SternheimerSIABOverlap, ConjugatesReferenceWavefunctionInQ) +{ + const GridVector complex_reference = {{1.0, 1.0}, {0.0, 0.0}}; + const PrimitiveGrid real_primitive = {{{1.0, 0.0}, {0.0, 0.0}}}; + + const auto q = siab::overlap_q(complex_reference, real_primitive, 0.5); + + ASSERT_EQ(q.size(), 1); + EXPECT_NEAR(q[0].real(), 0.5, 1.0e-14); + EXPECT_NEAR(q[0].imag(), -0.5, 1.0e-14); +} + +TEST(SternheimerSIABOverlap, FlattensPrimitiveOverlapInRowMajorOrder) +{ + const auto s = siab::overlap_s(primitives, 0.25); + + ASSERT_EQ(s.size(), 4); + EXPECT_NEAR(s[0].real(), 0.5, 1.0e-14); + EXPECT_NEAR(s[0].imag(), 0.0, 1.0e-14); + EXPECT_NEAR(s[1].real(), 0.0, 1.0e-14); + EXPECT_NEAR(s[1].imag(), 0.0, 1.0e-14); + EXPECT_NEAR(s[2].real(), 0.0, 1.0e-14); + EXPECT_NEAR(s[2].imag(), 0.0, 1.0e-14); + EXPECT_NEAR(s[3].real(), 0.5, 1.0e-14); + EXPECT_NEAR(s[3].imag(), 0.0, 1.0e-14); + + constexpr std::size_t nprimitive = 2; + for (std::size_t i = 0; i != nprimitive; ++i) + { + for (std::size_t j = 0; j != nprimitive; ++j) + { + EXPECT_NEAR(s[i * nprimitive + j].real(), s[j * nprimitive + i].real(), 1.0e-14); + EXPECT_NEAR(s[i * nprimitive + j].imag(), -s[j * nprimitive + i].imag(), 1.0e-14); + } + } +} + +TEST(SternheimerSIABOverlap, ProducesComplexHermitianPrimitiveOverlap) +{ + const PrimitiveGrid complex_primitives = { + {{1.0, 1.0}, {0.0, 0.0}}, + {{1.0, 0.0}, {0.0, 0.0}}, + }; + + const auto s = siab::overlap_s(complex_primitives, 0.5); + + ASSERT_EQ(s.size(), 4); + EXPECT_NEAR(s[0].real(), 1.0, 1.0e-14); + EXPECT_NEAR(s[0].imag(), 0.0, 1.0e-14); + EXPECT_NEAR(s[1].real(), 0.5, 1.0e-14); + EXPECT_NEAR(s[1].imag(), -0.5, 1.0e-14); + EXPECT_NEAR(s[2].real(), 0.5, 1.0e-14); + EXPECT_NEAR(s[2].imag(), 0.5, 1.0e-14); + EXPECT_NEAR(s[3].real(), 0.5, 1.0e-14); + EXPECT_NEAR(s[3].imag(), 0.0, 1.0e-14); +} + +TEST(SternheimerSIABOverlap, ContractsPhysicallyNormalizedReciprocalCoefficients) +{ + const std::vector response = {{1.0, 1.0}, {2.0, -1.0}, {-0.5, 0.25}}; + const PrimitiveGrid reciprocal_primitives = { + {{1.0, 0.0}, {0.5, 0.5}, {0.0, -1.0}}, + {{0.0, 1.0}, {1.0, 0.0}, {0.25, 0.0}}, + }; + + const auto q = siab::overlap_q_reciprocal(response, reciprocal_primitives); + ASSERT_EQ(q.size(), 2); + Complex expected_q0(0.0, 0.0); + Complex expected_q1(0.0, 0.0); + for (std::size_t ig = 0; ig != response.size(); ++ig) + { + expected_q0 += std::conj(response[ig]) * reciprocal_primitives[0][ig]; + expected_q1 += std::conj(response[ig]) * reciprocal_primitives[1][ig]; + } + EXPECT_NEAR(q[0].real(), expected_q0.real(), 1.0e-14); + EXPECT_NEAR(q[0].imag(), expected_q0.imag(), 1.0e-14); + EXPECT_NEAR(q[1].real(), expected_q1.real(), 1.0e-14); + EXPECT_NEAR(q[1].imag(), expected_q1.imag(), 1.0e-14); + + const auto s = siab::overlap_s_reciprocal(reciprocal_primitives); + ASSERT_EQ(s.size(), 4); + EXPECT_NEAR(s[1].real(), std::conj(s[2]).real(), 1.0e-14); + EXPECT_NEAR(s[1].imag(), std::conj(s[2]).imag(), 1.0e-14); +} + +TEST(SternheimerSIABOverlap, ContiguousReciprocalContractionPreservesBraConjugation) +{ + const std::vector response = {{1.0, 1.0}, {2.0, -1.0}, {-0.5, 0.25}}; + const PrimitiveGrid primitives = { + {{1.0, 0.0}, {0.5, 0.5}, {0.0, -1.0}}, + {{0.0, 1.0}, {1.0, 0.0}, {0.25, 0.0}}, + }; + std::vector contiguous; + for (const auto& primitive: primitives) + { + contiguous.insert(contiguous.end(), primitive.begin(), primitive.end()); + } + + const auto expected = siab::overlap_q_reciprocal(response, primitives); + const auto actual = siab::overlap_q_reciprocal_contiguous(response, contiguous, 2, 3); + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t index = 0; index != actual.size(); ++index) + { + EXPECT_NEAR(actual[index].real(), expected[index].real(), 1.0e-14); + EXPECT_NEAR(actual[index].imag(), expected[index].imag(), 1.0e-14); + } + + EXPECT_THROW(siab::overlap_q_reciprocal_contiguous(response, contiguous, 3, 2), + std::invalid_argument); +} + +TEST(SternheimerSIABOverlap, RejectsInvalidGridVolume) +{ + EXPECT_THROW(siab::norm(reference_wavefunction, 0.0), std::invalid_argument); + EXPECT_THROW(siab::overlap_q(reference_wavefunction, primitives, -0.25), std::invalid_argument); + EXPECT_THROW(siab::overlap_s(primitives, std::numeric_limits::infinity()), std::invalid_argument); + EXPECT_THROW(siab::norm(reference_wavefunction, std::numeric_limits::quiet_NaN()), + std::invalid_argument); +} + +TEST(SternheimerSIABOverlap, EmptyLocalSlabContributesZero) +{ + const GridVector empty_grid; + const PrimitiveGrid empty_primitives(2); + + EXPECT_DOUBLE_EQ(siab::norm(empty_grid, 0.25), 0.0); + + const auto q = siab::overlap_q(empty_grid, empty_primitives, 0.25); + ASSERT_EQ(q.size(), 2); + EXPECT_EQ(q[0], Complex(0.0, 0.0)); + EXPECT_EQ(q[1], Complex(0.0, 0.0)); + + const auto s = siab::overlap_s(empty_primitives, 0.25); + ASSERT_EQ(s.size(), 4); + for (const Complex& value : s) + { + EXPECT_EQ(value, Complex(0.0, 0.0)); + } +} + +TEST(SternheimerSIABOverlap, RejectsMissingBasisAndMismatchedGrids) +{ + EXPECT_THROW(siab::overlap_q(reference_wavefunction, {}, 0.25), std::invalid_argument); + EXPECT_THROW(siab::overlap_s({}, 0.25), std::invalid_argument); + + PrimitiveGrid mismatched = primitives; + mismatched[1].pop_back(); + EXPECT_THROW(siab::overlap_q(reference_wavefunction, mismatched, 0.25), std::invalid_argument); + EXPECT_THROW(siab::overlap_s(mismatched, 0.25), std::invalid_argument); + + const PrimitiveGrid wrong_reference_size = {{{1.0, 0.0}, {0.0, 0.0}}}; + EXPECT_THROW(siab::overlap_q(reference_wavefunction, wrong_reference_size, 0.25), std::invalid_argument); + + const PrimitiveGrid mixed_empty_and_nonempty = {{}, {{1.0, 0.0}}}; + EXPECT_THROW(siab::overlap_q({}, mixed_empty_and_nonempty, 0.25), std::invalid_argument); + EXPECT_THROW(siab::overlap_s(mixed_empty_and_nonempty, 0.25), std::invalid_argument); +} diff --git a/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_primitives.cpp b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_primitives.cpp new file mode 100644 index 00000000000..e9e3b216062 --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_primitives.cpp @@ -0,0 +1,669 @@ +#include "source_base/constants.h" +#include "source_base/math_ylmreal.h" +#include "source_base/matrix.h" +#include "source_base/matrix3.h" +#include "source_basis/module_pw/pw_basis_k.h" +#include "source_cell/stru_fac.h" +#include "source_cell/unitcell.h" +#include "source_io/module_bessel/bessel_basis.h" +#include "source_io/module_bessel/numerical_basis.h" +#include "source_io/module_parameter/parameter.h" + +#include +#include +#include +#include +#include + +#ifdef __MPI +#include +#endif + +namespace +{ + +using Complex = std::complex; + +constexpr double ecut_ry = 25.0; +constexpr double rcut_bohr = 2.0; +constexpr double tolerance = 1.0e-12; +constexpr int ik = 0; +const ModuleBase::Vector3 atom_tau(0.137, 0.211, 0.173); + +TEST(SternheimerSIABPrimitives, DefaultPrimitiveEcutInheritsWavefunctionCutoff) +{ + Numerical_Basis::SIABPrimitiveInputs settings; + settings.rcuts = {8.0}; + settings.ecut_spec = "default"; + settings.ecutwfc = 37.5; + settings.smooth = false; + settings.sigma = 0.0; + settings.tolerance = 1.0e-10; + + const auto parameters = Numerical_Basis::siab_parameters_from_input(settings, 0, 2); + EXPECT_DOUBLE_EQ(parameters.ecut_ry, 37.5); + EXPECT_DOUBLE_EQ(parameters.rcut_bohr, 8.0); + EXPECT_EQ(parameters.lmax, 2); +} + +void expect_complex_near(const Complex& actual, const Complex& expected) +{ + const double scale = std::max(std::abs(actual), std::abs(expected)); + const double threshold = 1.0e-10 + 1.0e-10 * scale; + EXPECT_NEAR(actual.real(), expected.real(), threshold); + EXPECT_NEAR(actual.imag(), expected.imag(), threshold); +} + +Complex inner_product(const std::vector& left, const std::vector& right, const double weight) +{ + EXPECT_EQ(left.size(), right.size()); + Complex local_result(0.0, 0.0); + for (std::size_t i = 0; i < left.size(); ++i) + { + local_result += std::conj(left[i]) * right[i] * weight; + } +#ifdef __MPI + double local_values[2] = {local_result.real(), local_result.imag()}; + double global_values[2] = {0.0, 0.0}; + MPI_Allreduce(local_values, global_values, 2, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD); + return Complex(global_values[0], global_values[1]); +#else + return local_result; +#endif +} + +void fill_h_cell(UnitCell& ucell) +{ + ucell.lat0 = 8.0; + ucell.latvec = ModuleBase::Matrix3(1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); + ucell.omega = 512.0; + ucell.tpiba = ModuleBase::TWO_PI / ucell.lat0; + ucell.ntype = 1; + ucell.nat = 1; + ucell.lmax = 1; + ucell.lmaxmax = 1; + ucell.nmax = 1; + ucell.atoms = new Atom[1]; + ucell.atoms[0].type = 0; + ucell.atoms[0].label = "H"; + ucell.atoms[0].na = 1; + ucell.atoms[0].nwl = 1; + ucell.atoms[0].l_nchi = {1, 1}; + ucell.atoms[0].tau = {atom_tau}; +} + +std::vector reciprocal_primitive(const Bessel_Basis& bessel, + const ModulePW::PW_Basis_K& pw, + const UnitCell& ucell, + const int l, + const int conventional_m, + const int ie) +{ + const int npw = pw.npwk[ik]; + std::vector> gk(npw); + for (int ig = 0; ig < npw; ++ig) + { + gk[ig] = pw.getgpluskcar(ik, ig) * ucell.tpiba; + } + + const int target_lmax = std::max(ucell.lmax, l); + const int total_lm = (target_lmax + 1) * (target_lmax + 1); + ModuleBase::matrix ylm(total_lm, npw); + ModuleBase::YlmReal::Ylm_Real(total_lm, npw, gk.data(), ylm); + + const int abacus_m = Numerical_Basis::siab_abacus_m(conventional_m); + const int lm = l * l + abacus_m; + const Complex lphase = (4.0 * ModuleBase::PI / std::sqrt(ucell.omega)) + * std::pow(ModuleBase::IMAG_UNIT, -l); + + std::vector values(npw); + for (int ig = 0; ig < npw; ++ig) + { + const ModuleBase::Vector3 gdirect = pw.getgdirect(ik, ig); + const Complex sk = std::exp(Complex(0.0, -ModuleBase::TWO_PI * (gdirect * atom_tau))); + values[ig] = lphase * sk * ylm(lm, ig) + * bessel.Polynomial_Interpolation2(l, ie, gk[ig].norm()); + } + return values; +} + +std::vector physical_complex_grid(const ModulePW::PW_Basis_K& pw, + const UnitCell& ucell, + const std::vector& reciprocal) +{ + EXPECT_FALSE(pw.gamma_only); + std::vector grid(std::max(1, pw.nrxx), Complex(0.0, 0.0)); + pw.recip2real(reciprocal.data(), grid.data(), ik); + grid.resize(pw.nrxx); + const double scale = 1.0 / std::sqrt(ucell.omega); + for (Complex& value : grid) + { + value *= scale; + } + return grid; +} + +std::vector deterministic_complex_pw(const ModulePW::PW_Basis_K& pw) +{ + std::vector values(pw.npwk[ik]); + for (int ig = 0; ig < pw.npwk[ik]; ++ig) + { + const ModuleBase::Vector3 g = pw.getgdirect(ik, ig); + const double denominator = 1.0 + g.norm2(); + values[ig] = Complex((1.0 + 0.17 * g.x - 0.11 * g.z) / denominator, + (0.23 + 0.13 * g.y + 0.07 * g.z) / denominator); + } + return values; +} + +void initialize_reference_bessel(Bessel_Basis& bessel, + const UnitCell& ucell, + const Numerical_Basis::SIABPrimitiveParameters& parameters) +{ + const int target_lmax = parameters.lmax < 0 ? ucell.lmax : parameters.lmax; + bessel.init(false, + parameters.ecut_ry, + ucell.ntype, + target_lmax, + parameters.smooth, + parameters.sigma, + parameters.rcut_bohr, + parameters.tolerance, + ucell); +} + +class SternheimerSIABPrimitivesTest : public ::testing::Test +{ + protected: + UnitCell ucell; + ModulePW::PW_Basis_K gamma_pw; + ModulePW::PW_Basis_K full_pw; + Structure_Factor structure_factor; + + void SetUp() override + { + fill_h_cell(ucell); + const ModuleBase::Vector3 gamma(0.0, 0.0, 0.0); +#ifdef __MPI + int rank = 0; + int size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + gamma_pw.initmpi(size, rank, MPI_COMM_WORLD); + full_pw.initmpi(size, rank, MPI_COMM_WORLD); +#endif + gamma_pw.initgrids(ucell.lat0, ucell.latvec, 4.0 * ecut_ry); + gamma_pw.initparameters(true, ecut_ry, 1, &gamma, 2, true); + gamma_pw.setuptransform(); + gamma_pw.collect_local_pw(); + + full_pw.initgrids(ucell.lat0, ucell.latvec, gamma_pw.nx, gamma_pw.ny, gamma_pw.nz); + full_pw.initparameters(false, ecut_ry, 1, &gamma, 2, true); + full_pw.setuptransform(); + full_pw.collect_local_pw(); + + ASSERT_TRUE(gamma_pw.gamma_only); + ASSERT_FALSE(full_pw.gamma_only); + ASSERT_EQ(gamma_pw.nx, full_pw.nx); + ASSERT_EQ(gamma_pw.ny, full_pw.ny); + ASSERT_EQ(gamma_pw.nz, full_pw.nz); + ASSERT_EQ(gamma_pw.nxyz, full_pw.nxyz); + ASSERT_EQ(gamma_pw.nplane, full_pw.nplane); + ASSERT_EQ(gamma_pw.startz_current, full_pw.startz_current); + ASSERT_EQ(gamma_pw.nrxx, full_pw.nrxx); + } + + void TearDown() override + { + delete[] ucell.atoms; + ucell.atoms = nullptr; + } +}; + +} // namespace + +TEST(SternheimerSIABPrimitiveMapping, ConvertsConventionalMToABACUSOrdering) +{ + EXPECT_EQ(Numerical_Basis::siab_abacus_m(0), 0); + EXPECT_EQ(Numerical_Basis::siab_abacus_m(1), 1); + EXPECT_EQ(Numerical_Basis::siab_abacus_m(-1), 2); + EXPECT_EQ(Numerical_Basis::siab_abacus_m(2), 3); + EXPECT_EQ(Numerical_Basis::siab_abacus_m(-2), 4); +} + +TEST_F(SternheimerSIABPrimitivesTest, OrdersBlocksAndPreservesFullComplexPWInnerProductsOnGrid) +{ + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + }; + Numerical_Basis numerical_basis; + const auto blocks + = numerical_basis.siab_primitive_grid_values(ik, &full_pw, structure_factor, ucell, parameters); + + const int nprimitive = static_cast(std::sqrt(ecut_ry) * rcut_bohr / ModuleBase::PI); + ASSERT_EQ(nprimitive, 3); + ASSERT_EQ(blocks.size(), 4); + + EXPECT_EQ(blocks[0].type_index, 0); + EXPECT_EQ(blocks[0].element, "H"); + EXPECT_EQ(blocks[0].atom_index, 0); + EXPECT_EQ(blocks[0].l, 0); + EXPECT_EQ(blocks[0].m, 0); + EXPECT_EQ(blocks[0].n_primitive, nprimitive); + EXPECT_EQ(blocks[0].offset, 0); + + EXPECT_EQ(blocks[1].l, 1); + EXPECT_EQ(blocks[1].m, -1); + EXPECT_EQ(blocks[1].offset, nprimitive); + EXPECT_EQ(blocks[2].l, 1); + EXPECT_EQ(blocks[2].m, 0); + EXPECT_EQ(blocks[2].offset, 2 * nprimitive); + EXPECT_EQ(blocks[3].l, 1); + EXPECT_EQ(blocks[3].m, 1); + EXPECT_EQ(blocks[3].offset, 3 * nprimitive); + + Bessel_Basis bessel; + initialize_reference_bessel(bessel, ucell, parameters); + + const double delta_omega = ucell.omega / static_cast(full_pw.nxyz); + for (const auto& block : blocks) + { + ASSERT_EQ(block.n_primitive, nprimitive); + ASSERT_EQ(block.values.size(), static_cast(nprimitive)); + for (const auto& values : block.values) + { + EXPECT_EQ(values.size(), static_cast(full_pw.nrxx)); + } + + for (int ie_left = 0; ie_left < nprimitive; ++ie_left) + { + const auto reciprocal_left + = reciprocal_primitive(bessel, full_pw, ucell, block.l, block.m, ie_left); + for (int ie_right = 0; ie_right < nprimitive; ++ie_right) + { + const auto reciprocal_right + = reciprocal_primitive(bessel, full_pw, ucell, block.l, block.m, ie_right); + const Complex reciprocal_overlap = inner_product(reciprocal_left, reciprocal_right, 1.0); + const Complex grid_overlap + = inner_product(block.values[ie_left], block.values[ie_right], delta_omega); + expect_complex_near(grid_overlap, reciprocal_overlap); + } + } + } +} + +TEST_F(SternheimerSIABPrimitivesTest, ExposesTheSamePhysicallyNormalizedReciprocalPrimitives) +{ + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + 1, + }; + Numerical_Basis numerical_basis; + const auto blocks + = numerical_basis.siab_primitive_reciprocal_values(ik, &full_pw, structure_factor, ucell, parameters); + Bessel_Basis bessel; + initialize_reference_bessel(bessel, ucell, parameters); + const int nprimitive = static_cast(std::sqrt(ecut_ry) * rcut_bohr / ModuleBase::PI); + ASSERT_EQ(blocks.size(), 4); + for (const auto& block: blocks) + { + ASSERT_EQ(block.values.size(), static_cast(nprimitive)); + for (int ie = 0; ie != nprimitive; ++ie) + { + const auto expected = reciprocal_primitive(bessel, + full_pw, + ucell, + block.l, + block.m, + ie); + ASSERT_EQ(block.values[static_cast(ie)].size(), expected.size()); + for (std::size_t ig = 0; ig != expected.size(); ++ig) + { + expect_complex_near(block.values[static_cast(ie)][ig], expected[ig]); + } + } + } +} + +TEST_F(SternheimerSIABPrimitivesTest, RecoversNormalizedCoefficientsFromPhysicalGridValues) +{ + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + 1, + }; + Bessel_Basis bessel; + initialize_reference_bessel(bessel, ucell, parameters); + const auto expected = reciprocal_primitive(bessel, full_pw, ucell, 1, -1, 0); + const auto physical_grid = physical_complex_grid(full_pw, ucell, expected); + std::vector recovered(static_cast(full_pw.npwk[ik]), Complex(0.0, 0.0)); + full_pw.real2recip(physical_grid.data(), recovered.data(), ik); + for (Complex& value: recovered) + { + value *= std::sqrt(ucell.omega); + } + ASSERT_EQ(recovered.size(), expected.size()); + for (std::size_t ig = 0; ig != expected.size(); ++ig) + { + expect_complex_near(recovered[ig], expected[ig]); + } +} + +TEST_F(SternheimerSIABPrimitivesTest, UsesGlobalAtomIndicesAcrossSpecies) +{ + delete[] ucell.atoms; + ucell.ntype = 2; + ucell.nat = 2; + ucell.lmax = 0; + ucell.lmaxmax = 0; + ucell.nmax = 1; + ucell.atoms = new Atom[2]; + for (int type = 0; type != 2; ++type) + { + ucell.atoms[type].type = type; + ucell.atoms[type].label = type == 0 ? "H" : "H_empty"; + ucell.atoms[type].na = 1; + ucell.atoms[type].nwl = 0; + ucell.atoms[type].l_nchi = {1}; + ucell.atoms[type].tau = {atom_tau}; + } + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + 0, + }; + Numerical_Basis numerical_basis; + const auto blocks + = numerical_basis.siab_primitive_reciprocal_values(ik, &full_pw, structure_factor, ucell, parameters); + ASSERT_EQ(blocks.size(), 2); + EXPECT_EQ(blocks[0].element, "H"); + EXPECT_EQ(blocks[0].atom_index, 0); + EXPECT_EQ(blocks[1].element, "H_empty"); + EXPECT_EQ(blocks[1].atom_index, 1); +} + +TEST_F(SternheimerSIABPrimitivesTest, ExplicitLmaxExtendsPrimitivesBeyondInputOrbitalChannels) +{ + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + 2, + }; + ASSERT_EQ(ucell.atoms[0].nwl, 1); + + Numerical_Basis numerical_basis; + const auto blocks + = numerical_basis.siab_primitive_grid_values(ik, &full_pw, structure_factor, ucell, parameters); + + const int nprimitive = static_cast(std::sqrt(ecut_ry) * rcut_bohr / ModuleBase::PI); + ASSERT_EQ(blocks.size(), 9); + for (int magnetic_offset = 0; magnetic_offset != 5; ++magnetic_offset) + { + const auto& block = blocks[static_cast(4 + magnetic_offset)]; + EXPECT_EQ(block.l, 2); + EXPECT_EQ(block.m, magnetic_offset - 2); + EXPECT_EQ(block.n_primitive, nprimitive); + EXPECT_EQ(block.offset, (4 + magnetic_offset) * nprimitive); + } + + Bessel_Basis bessel; + initialize_reference_bessel(bessel, ucell, parameters); + const auto& d_block = blocks.back(); + const auto reciprocal_reference + = reciprocal_primitive(bessel, full_pw, ucell, d_block.l, d_block.m, 0); + const auto grid_reference = physical_complex_grid(full_pw, ucell, reciprocal_reference); + ASSERT_EQ(d_block.values[0].size(), grid_reference.size()); + for (std::size_t ir = 0; ir != grid_reference.size(); ++ir) + { + expect_complex_near(d_block.values[0][ir], grid_reference[ir]); + } +} + +TEST_F(SternheimerSIABPrimitivesTest, GammaCompressedOddLMatchesIndependentFullComplexGrid) +{ + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + }; + Numerical_Basis numerical_basis; + const auto gamma_blocks + = numerical_basis.siab_primitive_grid_values(ik, &gamma_pw, structure_factor, ucell, parameters); + + ASSERT_EQ(gamma_blocks.size(), 4); + const auto& odd_l_block = gamma_blocks[3]; + ASSERT_EQ(odd_l_block.l, 1); + ASSERT_EQ(odd_l_block.m, 1); + ASSERT_FALSE(odd_l_block.values.empty()); + + Bessel_Basis bessel; + initialize_reference_bessel(bessel, ucell, parameters); + const auto reciprocal_reference + = reciprocal_primitive(bessel, full_pw, ucell, odd_l_block.l, odd_l_block.m, 0); + const auto grid_reference = physical_complex_grid(full_pw, ucell, reciprocal_reference); + + ASSERT_EQ(odd_l_block.values[0].size(), grid_reference.size()); + for (std::size_t ir = 0; ir < grid_reference.size(); ++ir) + { + expect_complex_near(odd_l_block.values[0][ir], grid_reference[ir]); + } +} + +TEST_F(SternheimerSIABPrimitivesTest, FullComplexReciprocalBpsiMatchesGridOverlap) +{ + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + }; + Numerical_Basis numerical_basis; + const auto blocks + = numerical_basis.siab_primitive_grid_values(ik, &full_pw, structure_factor, ucell, parameters); + + ASSERT_EQ(blocks.size(), 4); + const auto& odd_l_block = blocks[3]; + ASSERT_EQ(odd_l_block.l, 1); + ASSERT_EQ(odd_l_block.m, 1); + + Bessel_Basis bessel; + initialize_reference_bessel(bessel, ucell, parameters); + const auto reciprocal_b + = reciprocal_primitive(bessel, full_pw, ucell, odd_l_block.l, odd_l_block.m, 0); + const auto reciprocal_psi = deterministic_complex_pw(full_pw); + const auto grid_psi = physical_complex_grid(full_pw, ucell, reciprocal_psi); + + const Complex reciprocal_overlap = inner_product(reciprocal_b, reciprocal_psi, 1.0); + const double delta_omega = ucell.omega / static_cast(full_pw.nxyz); + const Complex grid_overlap = inner_product(odd_l_block.values[0], grid_psi, delta_omega); + expect_complex_near(grid_overlap, reciprocal_overlap); +} + +TEST_F(SternheimerSIABPrimitivesTest, ReinitializesWhenParametersOrCellBasisShapeChange) +{ + const Numerical_Basis::SIABPrimitiveParameters first_parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + }; + const Numerical_Basis::SIABPrimitiveParameters second_parameters{ + 36.0, + 3.0, + false, + 0.1, + tolerance, + }; + + Numerical_Basis numerical_basis; + const auto first_blocks + = numerical_basis.siab_primitive_grid_values(ik, &full_pw, structure_factor, ucell, first_parameters); + ASSERT_EQ(first_blocks.size(), 4); + EXPECT_EQ(first_blocks[0].n_primitive, 3); + + const auto second_blocks + = numerical_basis.siab_primitive_grid_values(ik, &full_pw, structure_factor, ucell, second_parameters); + ASSERT_EQ(second_blocks.size(), 4); + EXPECT_EQ(second_blocks[0].n_primitive, 5); + EXPECT_EQ(second_blocks[3].offset, 15); + + ucell.lmax = 2; + ucell.lmaxmax = 2; + ucell.atoms[0].nwl = 2; + ucell.atoms[0].l_nchi = {1, 1, 1}; + const auto reshaped_blocks + = numerical_basis.siab_primitive_grid_values(ik, &full_pw, structure_factor, ucell, second_parameters); + ASSERT_EQ(reshaped_blocks.size(), 9); + EXPECT_EQ(reshaped_blocks[4].l, 2); + EXPECT_EQ(reshaped_blocks[4].m, -2); + EXPECT_EQ(reshaped_blocks[8].l, 2); + EXPECT_EQ(reshaped_blocks[8].m, 2); + EXPECT_EQ(reshaped_blocks[8].n_primitive, 5); + EXPECT_EQ(reshaped_blocks[8].offset, 40); +} + +TEST(SternheimerSIABPrimitivesMPI, EmptyLocalPWStillParticipatesInGridTransform) +{ +#ifndef __MPI + GTEST_SKIP(); +#else + int rank = 0; + int size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + if (size != 2) + { + GTEST_SKIP(); + } + + UnitCell ucell; + fill_h_cell(ucell); + const ModuleBase::Vector3 gamma(0.0, 0.0, 0.0); + ModulePW::PW_Basis_K tiny_pw; + tiny_pw.initmpi(size, rank, MPI_COMM_WORLD); + tiny_pw.initgrids(ucell.lat0, ucell.latvec, 4.0 * ecut_ry); + tiny_pw.initparameters(false, 4.0, 1, &gamma, 2, true); + tiny_pw.setuptransform(); + tiny_pw.collect_local_pw(); + + ASSERT_GT(tiny_pw.npwk[ik], 0); + if (rank == size - 1) + { + // setupIndGk rejects empty ranks, so retain its FFT plan and emulate a tighter local cutoff. + tiny_pw.npwk[ik] = 0; + } + const int local_empty = tiny_pw.npwk[ik] == 0 ? 1 : 0; + int empty_ranks = 0; + MPI_Allreduce(&local_empty, &empty_ranks, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD); + ASSERT_EQ(empty_ranks, 1); + if (local_empty) + { + ASSERT_GT(tiny_pw.nrxx, 0); + } + + const Numerical_Basis::SIABPrimitiveParameters parameters{ + ecut_ry, + rcut_bohr, + false, + 0.1, + tolerance, + }; + Structure_Factor structure_factor; + Numerical_Basis numerical_basis; + const auto blocks + = numerical_basis.siab_primitive_grid_values(ik, &tiny_pw, structure_factor, ucell, parameters); + + ASSERT_EQ(blocks.size(), 4); + for (const auto& block : blocks) + { + ASSERT_EQ(block.values.size(), 3); + for (const auto& values : block.values) + { + EXPECT_EQ(values.size(), static_cast(tiny_pw.nrxx)); + } + } + if (local_empty) + { + double local_grid_norm = 0.0; + for (const Complex& value : blocks[0].values[0]) + { + local_grid_norm += std::norm(value); + } + EXPECT_GT(local_grid_norm, 0.0); + } + + delete[] ucell.atoms; + ucell.atoms = nullptr; +#endif +} + +Structure_Factor::Structure_Factor() = default; +Structure_Factor::~Structure_Factor() = default; + +std::complex* Structure_Factor::get_sk(const int ik_in, + const int it, + const int ia, + const ModulePW::PW_Basis_K* wfc_basis) const +{ + EXPECT_GE(it, 0); + EXPECT_GE(ia, 0); + const int npw = wfc_basis->npwk[ik_in]; + std::complex* sk = new std::complex[npw]; + for (int ig = 0; ig < npw; ++ig) + { + const ModuleBase::Vector3 gdirect = wfc_basis->getgdirect(ik_in, ig); + const ModuleBase::Vector3 shifted_tau + = atom_tau + ModuleBase::Vector3(0.013 * it, 0.017 * ia, 0.0); + sk[ig] = std::exp(Complex(0.0, -ModuleBase::TWO_PI * (gdirect * shifted_tau))); + } + return sk; +} + +Atom::Atom() = default; +Atom::~Atom() = default; +Atom_pseudo::Atom_pseudo() = default; +Atom_pseudo::~Atom_pseudo() = default; +pseudo::pseudo() = default; +pseudo::~pseudo() = default; +UnitCell::UnitCell() = default; +UnitCell::~UnitCell() = default; +Magnetism::Magnetism() = default; +Magnetism::~Magnetism() = default; +SepPot::SepPot() = default; +SepPot::~SepPot() = default; +Sep_Cell::Sep_Cell() noexcept = default; +Sep_Cell::~Sep_Cell() noexcept = default; + +#ifdef __MPI +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + ::testing::InitGoogleTest(&argc, argv); + const int result = RUN_ALL_TESTS(); + MPI_Finalize(); + return result; +} +#endif diff --git a/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_provenance.cpp b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_provenance.cpp new file mode 100644 index 00000000000..0a14fddf52b --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_provenance.cpp @@ -0,0 +1,105 @@ +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_provenance.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +namespace siab = module_ri::sternheimer_siab; + +class TemporaryFile +{ + public: + TemporaryFile(const std::string& path, const std::string& contents) : path_(path) + { + std::ofstream out(path_.c_str(), std::ios::binary | std::ios::trunc); + out.write(contents.data(), static_cast(contents.size())); + } + + ~TemporaryFile() + { + std::remove(path_.c_str()); + } + + const std::string& path() const + { + return path_; + } + + private: + std::string path_; +}; + +TEST(SternheimerSIABProvenance, HashesKnownBytesAndFile) +{ + const std::string expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; + const std::vector bytes{'a', 'b', 'c'}; + EXPECT_EQ(siab::sha256_bytes(bytes), expected); + + const TemporaryFile file("sternheimer_siab_sha256_abc.tmp", "abc"); + EXPECT_EQ(siab::sha256_file(file.path()), expected); + EXPECT_EQ(siab::sha256_file_manifest({file.path()}), expected); + + siab::Sha256 chunked; + const unsigned char a = 'a'; + const unsigned char bc[2] = {'b', 'c'}; + chunked.update(&a, 1); + chunked.update(bc, 2); + EXPECT_EQ(chunked.finish(), expected); +} + +TEST(SternheimerSIABProvenance, FileManifestIsOrderedAndRejectsMissingInput) +{ + const TemporaryFile first("sternheimer_siab_sha256_first.tmp", "first"); + const TemporaryFile second("sternheimer_siab_sha256_second.tmp", "second"); + EXPECT_NE(siab::sha256_file_manifest({first.path(), second.path()}), + siab::sha256_file_manifest({second.path(), first.path()})); + EXPECT_THROW(siab::sha256_file_manifest({}), std::invalid_argument); + EXPECT_THROW(siab::sha256_file("sternheimer_siab_missing_file.tmp"), std::runtime_error); +} + +TEST(SternheimerSIABProvenance, UniqueFileManifestIgnoresRepeatedAuxiliaryBasisContents) +{ + const TemporaryFile first("sternheimer_siab_sha256_unique_first.tmp", "same auxiliary basis"); + const TemporaryFile duplicate("sternheimer_siab_sha256_unique_duplicate.tmp", "same auxiliary basis"); + const TemporaryFile second("sternheimer_siab_sha256_unique_second.tmp", "different auxiliary basis"); + + const std::string single_hash = siab::sha256_file(first.path()); + EXPECT_EQ(siab::sha256_unique_file_manifest({first.path(), first.path()}), single_hash); + EXPECT_EQ(siab::sha256_unique_file_manifest({first.path(), duplicate.path()}), single_hash); + EXPECT_EQ(siab::sha256_unique_file_manifest({first.path(), duplicate.path(), second.path()}), + siab::sha256_file_manifest({first.path(), second.path()})); + EXPECT_THROW(siab::sha256_unique_file_manifest({}), std::invalid_argument); +} + +TEST(SternheimerSIABProvenance, ResolvesExplicitFilesWithoutPlaceholders) +{ + const TemporaryFile file("sternheimer_siab_resolve.tmp", "x"); + EXPECT_EQ(siab::resolve_required_input_files("", {file.path()}, "orbital"), + (std::vector{file.path()})); + EXPECT_THROW(siab::resolve_required_input_files("", {}, "ABFS"), std::runtime_error); + EXPECT_THROW(siab::resolve_required_input_files("", {"auto"}, "pseudopotential"), std::runtime_error); +} + +TEST(SternheimerSIABProvenance, RequiresExactlyOneExplicitPrimitiveRcut) +{ + EXPECT_DOUBLE_EQ(siab::require_single_primitive_rcut({8.0}), 8.0); + EXPECT_THROW(siab::require_single_primitive_rcut({}), std::invalid_argument); + EXPECT_THROW(siab::require_single_primitive_rcut({8.0, 10.0}), std::invalid_argument); + EXPECT_THROW(siab::require_single_primitive_rcut({0.0}), std::invalid_argument); +} + +TEST(SternheimerSIABProvenance, RequiresFullCompiledCommitMetadata) +{ + const std::string commit = "c407b55f9dc4ae83377b4c37b4f3656fa91f447a"; + EXPECT_EQ(siab::require_source_commit(commit + " (2026-07-18)"), commit); + EXPECT_THROW(siab::require_source_commit("c407b55f9"), std::runtime_error); + EXPECT_THROW(siab::require_source_commit("unknown"), std::runtime_error); +} + +} // namespace diff --git a/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_writer.cpp b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_writer.cpp new file mode 100644 index 00000000000..06d65c739ef --- /dev/null +++ b/source/source_lcao/module_rpa/module_delta_st/test/test_sternheimer_siab_writer.cpp @@ -0,0 +1,681 @@ +#include "source_io/module_parameter/input_parameter.h" +#include "source_io/module_parameter/read_input.h" +#include "source_lcao/module_rpa/module_delta_st/sternheimer_siab_writer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +namespace siab = module_ri::sternheimer_siab; +using Complex = std::complex; + +const char* canonical_fixture = R"fixture( +format_version 1 +n_reference 2 +n_primitive 4 +n_blocks 2 +grid_volume_bohr3 0.125 + + +# element atom_index l m n_primitive offset +H 0 0 0 2 0 +H 0 1 0 2 2 + + +# occupied_state auxiliary_channel frequency_ha occupation frequency_weight norm +0 0 0.5 2.0 0.3 1.2 +0 0 1.5 2.0 0.7 0.8 + + +# row-major , real imag +0.8 0.0 +0.1 0.0 +0.2 0.0 +0.0 0.0 +0.4 0.0 +0.3 0.0 +0.0 0.0 +0.1 0.0 + + +# row-major , real imag +1.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +1.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +1.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +1.0 0.0 + + +{"abacus_commit":"19ab21e01d02cc805604ed77a6e269af698fdd1d","auxiliary_basis_sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","cell_bohr":[20.0,0.0,0.0,0.0,20.0,0.0,0.0,0.0,20.0],"ecut_ry":25.0,"kernel":"full_coulomb","orbital_sha256":"7e398340398306a6baf1c61ea68944d81ed43667473fbcc290d6541c4a661d1c","pseudopotential_sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","spin_convention":"occupation_in_metadata"} + +)fixture"; + +std::string test_path(const std::string& suffix) +{ + return ::testing::TempDir() + "/sternheimer_siab_writer_" + suffix + ".dat"; +} + +std::string read_text(const std::string& path) +{ + std::ifstream input(path.c_str(), std::ios::binary); + std::ostringstream text; + text << input.rdbuf(); + return text.str(); +} + +void write_text(const std::string& path, const std::string& text) +{ + std::ofstream output(path.c_str(), std::ios::binary | std::ios::trunc); + output << text; +} + +std::size_t count_occurrences(const std::string& text, const std::string& needle) +{ + std::size_t count = 0; + std::size_t position = 0; + while ((position = text.find(needle, position)) != std::string::npos) + { + ++count; + position += needle.size(); + } + return count; +} + +std::string section_body(const std::string& text, const std::string& section) +{ + const std::string opening = "<" + section + ">\n"; + const std::string closing = ""; + const std::size_t begin = text.find(opening); + const std::size_t end = text.find(closing); + EXPECT_NE(begin, std::string::npos); + EXPECT_NE(end, std::string::npos); + if (begin == std::string::npos || end == std::string::npos) + { + return ""; + } + return text.substr(begin + opening.size(), end - begin - opening.size()); +} + +std::size_t data_line_count(const std::string& body) +{ + std::istringstream input(body); + std::string line; + std::size_t count = 0; + while (std::getline(input, line)) + { + if (!line.empty() && line[0] != '#') + { + ++count; + } + } + return count; +} + +std::vector canonical_blocks() +{ + return {siab::PrimitiveBlock{"H", 0, 0, 0, 2, 0}, siab::PrimitiveBlock{"H", 0, 1, 0, 2, 2}}; +} + +std::vector canonical_rows_reversed() +{ + siab::ReferenceRow high; + high.occupied_state = 0; + high.auxiliary_channel = 0; + high.frequency_ha = 1.5; + high.occupation = 2.0; + high.frequency_weight = 0.7; + high.norm = 0.8; + high.q = {{0.4, 0.0}, {0.3, 0.0}, {0.0, 0.0}, {0.1, 0.0}}; + high.frequency_index = 1; + + siab::ReferenceRow low; + low.occupied_state = 0; + low.auxiliary_channel = 0; + low.frequency_ha = 0.5; + low.occupation = 2.0; + low.frequency_weight = 0.3; + low.norm = 1.2; + low.q = {{0.8, 0.0}, {0.1, 0.0}, {0.2, 0.0}, {0.0, 0.0}}; + low.frequency_index = 0; + return {high, low}; +} + +std::vector canonical_overlap_s() +{ + std::vector overlap_s(16, Complex(0.0, 0.0)); + for (std::size_t i = 0; i != 4; ++i) + { + overlap_s[i * 4 + i] = Complex(1.0, 0.0); + } + return overlap_s; +} + +siab::Provenance canonical_provenance() +{ + siab::Provenance provenance; + provenance.abacus_commit = "19ab21e01d02cc805604ed77a6e269af698fdd1d"; + provenance.auxiliary_basis_sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + provenance.cell_bohr = {20.0, 0.0, 0.0, 0.0, 20.0, 0.0, 0.0, 0.0, 20.0}; + provenance.ecut_ry = 25.0; + provenance.kernel = "full_coulomb"; + provenance.orbital_sha256 = "7e398340398306a6baf1c61ea68944d81ed43667473fbcc290d6541c4a661d1c"; + provenance.pseudopotential_sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + provenance.spin_convention = "occupation_in_metadata"; + return provenance; +} + +void write_canonical(const std::string& path, + const std::vector& rows = canonical_rows_reversed(), + const std::vector& overlap_s = canonical_overlap_s(), + const siab::Provenance& provenance = canonical_provenance()) +{ + siab::write_v1(path, 0.125, canonical_blocks(), rows, overlap_s, provenance); +} + +std::string formatted_q_real(const double value, const std::string& suffix) +{ + const std::string path = test_path("float_" + suffix); + std::vector rows = canonical_rows_reversed(); + rows[1].q[0] = Complex(value, 0.0); + write_canonical(path, rows); + + std::istringstream input(section_body(read_text(path), "OVERLAP_Q")); + std::string line; + std::string real; + while (std::getline(input, line)) + { + if (!line.empty() && line[0] != '#') + { + std::istringstream values(line); + values >> real; + break; + } + } + std::remove(path.c_str()); + return real; +} + +double parse_classic_double(const std::string& text) +{ + std::istringstream input(text); + input.imbue(std::locale::classic()); + double value = 0.0; + input >> value; + const bool exact_subnormal = value != 0.0 && std::abs(value) < std::numeric_limits::min(); + EXPECT_TRUE(!input.fail() || exact_subnormal); + EXPECT_EQ(input.rdbuf()->in_avail(), 0); + return value; +} + +} // namespace + +TEST(SternheimerSIABWriter, MatchesCanonicalFixtureAndSortsRowsWithQ) +{ + const std::string path = test_path("canonical"); + std::remove(path.c_str()); + std::remove((path + ".tmp").c_str()); + + write_canonical(path); + const std::string text = read_text(path); + EXPECT_EQ(text, canonical_fixture); + + const std::vector sections = {"STERNHEIMER_SIAB_HEADER", + "PRIMITIVE_BLOCKS", + "REFERENCE_METADATA", + "OVERLAP_Q", + "OVERLAP_S", + "PROVENANCE_JSON"}; + std::size_t previous = 0; + for (const std::string& section: sections) + { + const std::string opening = "<" + section + ">"; + const std::string closing = ""; + EXPECT_EQ(count_occurrences(text, opening), 1); + EXPECT_EQ(count_occurrences(text, closing), 1); + const std::size_t position = text.find(opening); + EXPECT_NE(position, std::string::npos); + EXPECT_GE(position, previous); + previous = position; + } + + EXPECT_EQ(data_line_count(section_body(text, "PRIMITIVE_BLOCKS")), 2); + EXPECT_EQ(data_line_count(section_body(text, "REFERENCE_METADATA")), 2); + EXPECT_EQ(data_line_count(section_body(text, "OVERLAP_Q")), 8); + EXPECT_EQ(data_line_count(section_body(text, "OVERLAP_S")), 16); + + const std::string expected_json + = "{\"abacus_commit\":\"19ab21e01d02cc805604ed77a6e269af698fdd1d\"," + "\"auxiliary_basis_sha256\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"," + "\"cell_bohr\":[20.0,0.0,0.0,0.0,20.0,0.0,0.0,0.0,20.0],\"ecut_ry\":25.0," + "\"kernel\":\"full_coulomb\"," + "\"orbital_sha256\":\"7e398340398306a6baf1c61ea68944d81ed43667473fbcc290d6541c4a661d1c\"," + "\"pseudopotential_sha256\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"," + "\"spin_convention\":\"occupation_in_metadata\"}\n"; + EXPECT_EQ(section_body(text, "PROVENANCE_JSON"), expected_json); + + std::remove(path.c_str()); +} + +TEST(SternheimerSIABWriter, PreservesShortestExactDoubleRoundTrip) +{ + const double adjacent = std::nextafter(1.0, 2.0); + const std::vector> values = { + {adjacent, "adjacent"}, + {std::numeric_limits::denorm_min(), "denorm_min"}, + {std::numeric_limits::min(), "min"}, + {std::numeric_limits::max(), "max"}, + {-0.0, "negative_zero"}, + }; + for (const std::pair& value: values) + { + const std::string formatted = formatted_q_real(value.first, value.second); + const double parsed = parse_classic_double(formatted); + EXPECT_EQ(parsed, value.first); + if (value.first == 0.0) + { + EXPECT_EQ(std::signbit(parsed), std::signbit(value.first)); + } + } + + EXPECT_EQ(formatted_q_real(adjacent, "adjacent_exact"), "1.0000000000000002"); + EXPECT_EQ(formatted_q_real(123.0, "one_hundred_twenty_three"), "123.0"); + EXPECT_EQ(formatted_q_real(10000.0, "ten_thousand"), "1e4"); + EXPECT_EQ(formatted_q_real(1.0e-5, "one_e_minus_five"), "1e-5"); + EXPECT_EQ(formatted_q_real(-0.0, "negative_zero_exact"), "-0.0"); +} + +TEST(SternheimerSIABWriter, EscapesJsonStringsWithoutChangingUtf8Bytes) +{ + const std::string path = test_path("json_escape"); + siab::Provenance provenance = canonical_provenance(); + const std::string utf8_two = "\xC2\xA2"; + const std::string utf8_three = "\xE6\xA0\xB8"; + const std::string utf8_four = "\xF0\x9F\x98\x80"; + const std::string utf8 = utf8_two + "_" + utf8_three + "_" + utf8_four; + provenance.kernel = std::string("full_\"\\\b\f\n\r\t") + static_cast(0x01) + "_" + utf8; + + write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance); + const std::string json = section_body(read_text(path), "PROVENANCE_JSON"); + const std::string expected_kernel = std::string(R"json("kernel":"full_\"\\\b\f\n\r\t\u0001_)json") + utf8 + "\""; + EXPECT_NE(json.find(expected_kernel), std::string::npos); + EXPECT_EQ(json.find(static_cast(0x01)), std::string::npos); + + std::remove(path.c_str()); +} + +TEST(SternheimerSIABWriter, RejectsMalformedUtf8BeforeTouchingTmp) +{ + const std::string path = test_path("invalid_utf8"); + const std::string tmp = path + ".tmp"; + const std::vector malformed = { + "\x80", // lone continuation byte + "\xE2\x28\xA1", // non-continuation byte inside a sequence + "\xE2\x82", // truncated three-byte sequence + "\xC0\xAF", // overlong slash + "\xED\xA0\x80", // UTF-8 encoded surrogate U+D800 + "\xF4\x90\x80\x80", // code point above U+10FFFF + }; + + write_text(tmp, "sentinel"); + for (const std::string& invalid: malformed) + { + siab::Provenance provenance = canonical_provenance(); + provenance.kernel = invalid; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + EXPECT_EQ(read_text(tmp), "sentinel"); + } + + using StringMember = std::string siab::Provenance::*; + const std::vector json_string_fields = { + &siab::Provenance::abacus_commit, + &siab::Provenance::auxiliary_basis_sha256, + &siab::Provenance::kernel, + &siab::Provenance::orbital_sha256, + &siab::Provenance::pseudopotential_sha256, + &siab::Provenance::spin_convention, + }; + for (const StringMember field: json_string_fields) + { + siab::Provenance provenance = canonical_provenance(); + provenance.*field = "\x80"; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + EXPECT_EQ(read_text(tmp), "sentinel"); + } + + std::remove(tmp.c_str()); +} + +TEST(SternheimerSIABWriter, AcceptsCompleteNonorthogonalCellWithNegativeComponents) +{ + const std::string path = test_path("nonorthogonal_cell"); + siab::Provenance provenance = canonical_provenance(); + provenance.cell_bohr = {20.0, 1.0, -2.0, 0.0, 19.0, 3.0, -1.0, 0.0, 18.0}; + + write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance); + const std::string json = section_body(read_text(path), "PROVENANCE_JSON"); + EXPECT_NE(json.find("\"cell_bohr\":[20.0,1.0,-2.0,0.0,19.0,3.0,-1.0,0.0,18.0]"), std::string::npos); + + std::remove(path.c_str()); +} + +TEST(SternheimerSIABWriter, RejectsIncompleteSingularAndNonfiniteCells) +{ + const std::string path = test_path("invalid_cell"); + siab::Provenance provenance = canonical_provenance(); + provenance.cell_bohr = {20.0, 20.0, 20.0}; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + + provenance = canonical_provenance(); + provenance.cell_bohr = {1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 1.0}; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + + provenance = canonical_provenance(); + provenance.cell_bohr[4] = std::numeric_limits::infinity(); + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); +} + +TEST(SternheimerSIABWriter, RejectsBadDimensionsAndDuplicateRowKeys) +{ + const std::string path = test_path("dimensions"); + std::remove(path.c_str()); + std::remove((path + ".tmp").c_str()); + std::vector rows = canonical_rows_reversed(); + rows[0].q.pop_back(); + EXPECT_THROW(write_canonical(path, rows), std::invalid_argument); + + rows = canonical_rows_reversed(); + rows[0].frequency_index = rows[1].frequency_index; + EXPECT_THROW(write_canonical(path, rows), std::invalid_argument); + + std::vector overlap_s = canonical_overlap_s(); + overlap_s.pop_back(); + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), overlap_s), std::invalid_argument); + EXPECT_FALSE(std::ifstream(path.c_str()).good()); +} + +TEST(SternheimerSIABWriter, RejectsNonHermitianAndNonfiniteValues) +{ + const std::string path = test_path("values"); + std::vector overlap_s = canonical_overlap_s(); + overlap_s[1] = Complex(0.25, 0.5); + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), overlap_s), std::invalid_argument); + + std::vector rows = canonical_rows_reversed(); + rows[0].frequency_weight = std::numeric_limits::quiet_NaN(); + EXPECT_THROW(write_canonical(path, rows), std::invalid_argument); + + rows = canonical_rows_reversed(); + rows[0].q[0] = Complex(std::numeric_limits::infinity(), 0.0); + EXPECT_THROW(write_canonical(path, rows), std::invalid_argument); + + overlap_s = canonical_overlap_s(); + overlap_s[0] = Complex(std::numeric_limits::quiet_NaN(), 0.0); + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), overlap_s), std::invalid_argument); +} + +TEST(SternheimerSIABWriter, AcceptsAsciiPrimitiveElementTokens) +{ + const std::string path = test_path("element_tokens"); + const std::vector accepted = {"H", "H1", "Si_aux", "H+", "H-", "A+B-C_1"}; + for (const std::string& element: accepted) + { + std::vector blocks = canonical_blocks(); + blocks[0].element = element; + siab::write_v1(path, 0.125, blocks, canonical_rows_reversed(), canonical_overlap_s(), canonical_provenance()); + EXPECT_NE(read_text(path).find(element + " 0 0 0 2 0\n"), std::string::npos); + } + std::remove(path.c_str()); +} + +TEST(SternheimerSIABWriter, RejectsUnsafePrimitiveElementTokensBeforeTouchingTmp) +{ + const std::string path = test_path("invalid_element_tokens"); + const std::string tmp = path + ".tmp"; + const std::vector rejected = {"", " H", "H H", "H\nX", "#H", "", ">H", "+H", "-H", "\x80", "H\x80"}; + write_text(tmp, "sentinel"); + for (const std::string& element: rejected) + { + std::vector blocks = canonical_blocks(); + blocks[0].element = element; + EXPECT_THROW(siab::write_v1(path, + 0.125, + blocks, + canonical_rows_reversed(), + canonical_overlap_s(), + canonical_provenance()), + std::invalid_argument); + EXPECT_EQ(read_text(tmp), "sentinel"); + } + std::remove(tmp.c_str()); +} + +TEST(SternheimerSIABWriter, RejectsInvalidBlocksAndHashesBeforeTouchingTmp) +{ + const std::string path = test_path("validation"); + const std::string tmp = path + ".tmp"; + write_text(tmp, "sentinel"); + + std::vector blocks = canonical_blocks(); + blocks[1].offset = 3; + EXPECT_THROW( + siab::write_v1(path, 0.125, blocks, canonical_rows_reversed(), canonical_overlap_s(), canonical_provenance()), + std::invalid_argument); + EXPECT_EQ(read_text(tmp), "sentinel"); + + siab::Provenance provenance = canonical_provenance(); + provenance.orbital_sha256 = "not-a-sha256"; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + EXPECT_EQ(read_text(tmp), "sentinel"); + + std::remove(tmp.c_str()); +} + +TEST(SternheimerSIABWriter, AtomicallyReplacesExistingPathAndRemovesTmp) +{ + const std::string path = test_path("atomic"); + const std::string tmp = path + ".tmp"; + write_text(path, "old contents"); + std::remove(tmp.c_str()); + + write_canonical(path); + EXPECT_EQ(read_text(path), canonical_fixture); + EXPECT_FALSE(std::ifstream(tmp.c_str()).good()); + + std::remove(path.c_str()); +} + +TEST(SternheimerSIABWriter, AppendsCompleteTask4ProvenanceDeterministically) +{ + const std::string path = test_path("task4_provenance"); + siab::Provenance provenance = canonical_provenance(); + provenance.executable_sha256 = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + provenance.exx_pca_thr = 1.0e-6; + provenance.sternheimer_nfreq = 2; + provenance.frequency_ha = {0.2, 0.8}; + provenance.frequency_weights_ha = {0.3, 0.7}; + provenance.mpi_ranks = 2; + provenance.omp_threads = 32; + + write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance); + const std::string json = section_body(read_text(path), "PROVENANCE_JSON"); + EXPECT_NE(json.find( + "\"spin_convention\":\"occupation_in_metadata\"," + "\"executable_sha256\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"," + "\"exx_pca_thr\":1e-6,\"frequency_ha\":[0.2,0.8]," + "\"frequency_weights_ha\":[0.3,0.7],\"mpi_ranks\":2,\"omp_threads\":32," + "\"sternheimer_nfreq\":2}"), + std::string::npos); + std::remove(path.c_str()); +} + +TEST(SternheimerSIABWriter, RejectsPartialTask4ProvenanceBeforeTouchingTmp) +{ + const std::string path = test_path("partial_task4_provenance"); + const std::string tmp = path + ".tmp"; + write_text(tmp, "sentinel"); + + siab::Provenance provenance = canonical_provenance(); + provenance.executable_sha256 = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + EXPECT_EQ(read_text(tmp), "sentinel"); + + provenance.exx_pca_thr = 1.0e-6; + provenance.sternheimer_nfreq = 1; + provenance.frequency_ha = {0.2}; + provenance.frequency_weights_ha = {}; + provenance.mpi_ranks = 1; + provenance.omp_threads = 1; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + EXPECT_EQ(read_text(tmp), "sentinel"); + std::remove(tmp.c_str()); +} + +TEST(SternheimerSIABWriter, AppendsCompleteCoulombWhiteningProvenance) +{ + const std::string path = test_path("coulomb_whitening_provenance"); + siab::Provenance provenance = canonical_provenance(); + provenance.auxiliary_whitening = "global_full_coulomb_v1"; + provenance.raw_auxiliary_dimension = 2; + provenance.whitened_auxiliary_rank = 1; + provenance.discarded_auxiliary_rank = 1; + provenance.coulomb_relative_threshold = 1.0e-10; + provenance.coulomb_eigenvalues = {1.0e-12, 4.0}; + provenance.coulomb_max_orthonormality_error = 2.0e-14; + provenance.coulomb_transform_sha256 + = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; + + write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance); + const std::string json = section_body(read_text(path), "PROVENANCE_JSON"); + EXPECT_NE(json.find( + "\"auxiliary_whitening\":\"global_full_coulomb_v1\"," + "\"coulomb_eigenvalues\":[1e-12,4.0]," + "\"coulomb_max_orthonormality_error\":2e-14," + "\"coulomb_relative_threshold\":1e-10," + "\"coulomb_transform_sha256\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"," + "\"discarded_auxiliary_rank\":1,\"raw_auxiliary_dimension\":2," + "\"whitened_auxiliary_rank\":1}"), + std::string::npos); + std::remove(path.c_str()); +} + +TEST(SternheimerSIABWriter, RejectsPartialCoulombWhiteningProvenance) +{ + const std::string path = test_path("partial_coulomb_whitening_provenance"); + siab::Provenance provenance = canonical_provenance(); + provenance.auxiliary_whitening = "global_full_coulomb_v1"; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + + provenance.raw_auxiliary_dimension = 2; + provenance.whitened_auxiliary_rank = 2; + provenance.discarded_auxiliary_rank = 0; + provenance.coulomb_relative_threshold = 1.0e-10; + provenance.coulomb_eigenvalues = {1.0, 2.0}; + provenance.coulomb_max_orthonormality_error = 1.0e-14; + provenance.coulomb_transform_sha256 + = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; + write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance); + + provenance.whitened_auxiliary_rank = 1; + EXPECT_THROW(write_canonical(path, canonical_rows_reversed(), canonical_overlap_s(), provenance), + std::invalid_argument); + std::remove(path.c_str()); +} + +TEST(SternheimerSIABInput, RegisteredCheckEnforcesLcaoDeltaCombination) +{ + ModuleIO::ReadInput read_input(0); + const std::vector>& items = read_input.get_input_lists(); + const auto found = std::find_if(items.begin(), items.end(), [](const auto& item) { + return item.first == "out_sternheimer_siab"; + }); + ASSERT_NE(found, items.end()); + const ModuleIO::Input_Item& item = found->second; + ASSERT_TRUE(static_cast(item.check_value)); + EXPECT_NE(item.description.find("basis_type=lcao"), std::string::npos); + EXPECT_EQ(item.get_availability(), "basis_type==lcao"); + + Parameter parameter; + Input_para& input = const_cast(parameter.inp); + EXPECT_FALSE(input.out_sternheimer_siab); + item.check_value(item, parameter); + + input.out_sternheimer_siab = true; + input.basis_type = "lcao"; + input.out_sternheimer_librpa = false; + input.sternheimer_delta = true; + input.bessel_nao_rcuts = {8.0}; + item.check_value(item, parameter); + + input.bessel_nao_rcuts.clear(); + EXPECT_EXIT(item.check_value(item, parameter), ::testing::ExitedWithCode(1), ""); + input.bessel_nao_rcuts = {8.0, 10.0}; + EXPECT_EXIT(item.check_value(item, parameter), ::testing::ExitedWithCode(1), ""); + input.bessel_nao_rcuts = {8.0}; + + input.basis_type = "pw"; + EXPECT_EXIT(item.check_value(item, parameter), ::testing::ExitedWithCode(1), ""); + + input.basis_type = "lcao"; + input.out_sternheimer_librpa = true; + EXPECT_EXIT(item.check_value(item, parameter), ::testing::ExitedWithCode(1), ""); + + input.out_sternheimer_librpa = false; + input.sternheimer_delta = false; + EXPECT_EXIT(item.check_value(item, parameter), ::testing::ExitedWithCode(1), ""); +} + +TEST(SternheimerSIABInput, CoulombWhiteningThresholdIsExplicitAndPositive) +{ + ModuleIO::ReadInput read_input(0); + const std::vector>& items = read_input.get_input_lists(); + const auto found = std::find_if(items.begin(), items.end(), [](const auto& item) { + return item.first == "sternheimer_siab_vthr"; + }); + ASSERT_NE(found, items.end()); + const ModuleIO::Input_Item& item = found->second; + ASSERT_TRUE(static_cast(item.check_value)); + + Parameter parameter; + Input_para& input = const_cast(parameter.inp); + EXPECT_DOUBLE_EQ(input.sternheimer_siab_vthr, 1.0e-10); + item.check_value(item, parameter); + + input.sternheimer_siab_vthr = 1.0e-8; + item.check_value(item, parameter); + input.sternheimer_siab_vthr = 0.0; + EXPECT_EXIT(item.check_value(item, parameter), ::testing::ExitedWithCode(1), ""); + input.sternheimer_siab_vthr = 1.0; + EXPECT_EXIT(item.check_value(item, parameter), ::testing::ExitedWithCode(1), ""); +}