207 const std::vector<PolarSegment>& foreground)
const {
213 constexpr double kTol = 1
e-4;
215 const Eigen::Vector3d centroid = Centroid(seg);
218 if (entry.first == seg.getId() &&
219 (centroid - entry.second).norm() <= kTol) {
225 std::stringstream message;
226 message <<
"EwaldRegion: asked about segment " << seg.getId()
227 <<
", which is not part of the foreground these sums were "
228 "built for. The suppression list and the real-space "
229 "neighbour cache belong to that foreground, so answering "
230 "would drop the wrong background copies and subtract erf "
231 "corrections for copies that were never dropped -- a wrong "
232 "energy with no symptom. JobTopology declares the whole "
233 "foreground with RegisterForeground before any region is "
234 "evaluated; if this fires, that declaration is missing or "
236 throw std::runtime_error(message.str());
242 std::vector<std::pair<Index, Eigen::Vector3d>> source =
244 if (source.empty()) {
246 source.push_back({seg.getId(), Centroid(seg)});
252 for (
const auto& entry : source) {
253 const Index seg_id = entry.first;
255 throw std::runtime_error(
256 "EwaldRegion: the foreground contains segment " +
257 std::to_string(seg_id) +
258 ", which is absent from the periodic background. The foreground "
259 "must be carved out of the same system the background was "
286 const Eigen::Vector3d bg_centroid = Centroid(bg_copy);
287 const Eigen::Vector3d delta = entry.second - bg_centroid;
288 const Eigen::Vector3d fractional =
params_.box.inverse() * delta;
289 const Eigen::Vector3d image =
290 params_.box * fractional.array().round().matrix();
291 const Eigen::Vector3d residual = delta - image;
297 constexpr double kResidualWarn = 5.0;
298 if (residual.norm() > kResidualWarn) {
300 <<
TimeStamp() <<
" WARNING: foreground segment " << seg_id
301 <<
" sits " << residual.norm()
302 <<
" bohr from the nearest periodic image of its background "
303 "copy. That is too far to be geometry relaxation, so the "
304 "copy this code is about to suppress may not be the one the "
305 "foreground actually displaces."
312 real_sum_ = std::make_unique<EwaldRealSpaceSum>(
316 recip_sum_ = std::make_unique<EwaldReciprocalSpaceSum>(
318 shape_ = std::make_unique<EwaldShapeCorrection>(
params_.box.determinant(),
325 const std::vector<Eigen::Vector3d>& points)
const {
327 NotInitialized(
"PotentialAt");
330 throw std::runtime_error(
331 "EwaldRegion::PotentialAt: no foreground has been declared. "
332 "ApplyFieldTo can fall back on the segments it is handed, but a "
333 "list of points carries no segment identity, so the copies to "
334 "suppress cannot be inferred. JobTopology declares the foreground "
335 "with RegisterForeground before any region is evaluated.");
347 using Clock = std::chrono::steady_clock;
348 auto seconds_since = [](Clock::time_point t) {
349 return std::chrono::duration<double>(Clock::now() - t).count();
351 auto t_start = Clock::now();
352 Eigen::VectorXd phi =
real_sum_->PotentialAtMany(probe_segment_id, points,
354 const double t_real = seconds_since(t_start);
355 auto t_recip_start = Clock::now();
357 const double t_recip = seconds_since(t_recip_start);
358 auto t_corr_start = Clock::now();
374 std::vector<ProbeSource> erf_sources;
378 const Eigen::Vector3d shift = copy.second - Centroid(bg);
380 erf_sources.push_back(
381 {source.getPos() + shift, source.getCharge(),
382 source.getStaticDipole() + source.getInducedDipole()});
385#pragma omp parallel for schedule(static)
386 for (
Index p = 0; p < n_points; ++p) {
387 const Eigen::Vector3d& point = points[std::size_t(p)];
389 for (
const ProbeSource& source : erf_sources) {
391 interactor_->ErfPotential(source.q, source.mu, point - source.pos);
396 <<
TimeStamp() <<
" Ewald potential at " << n_points
397 <<
" points: real space " << std::setprecision(4) << t_real
398 <<
" s, reciprocal " << t_recip <<
" s, shape + erf correction "
400 <<
" foreground copies)" << std::flush;
406 NotInitialized(
"ApplyFieldTo");
418 std::vector<PolarSite*> targets;
419 std::vector<Index> target_segment_ids;
422 targets.push_back(&site);
423 target_segment_ids.push_back(seg.getId());
445 std::vector<Eigen::Vector3d> v_before;
446 v_before.reserve(targets.size());
447 for (
const PolarSite* target : targets) {
448 v_before.push_back(target->V());
458 const Index n_targets =
Index(targets.size());
460 std::vector<std::pair<Index, PolarSite*>> real_targets;
461 real_targets.reserve(targets.size());
462 for (
Index i = 0; i < n_targets; ++i) {
463 real_targets.push_back(
464 {target_segment_ids[std::size_t(i)], targets[std::size_t(i)]});
481#pragma omp parallel for schedule(dynamic, 16) reduction(+ : e_real)
482 for (
Index i = 0; i < n_targets; ++i) {
483 e_real +=
real_sum_->CalcStaticEnergyAt(*targets[std::size_t(i)],
486 double energy = e_real;
498 double e_real_pu = 0.0;
499#pragma omp parallel for schedule(dynamic, 16) reduction(+ : e_real_pu)
500 for (
Index i = 0; i < n_targets; ++i) {
501 e_real_pu +=
real_sum_->CalcInducedSourceEnergyAt(
511 PolarSite probe(-1,
"X", Eigen::Vector3d::Zero());
513 const Eigen::Vector3d shape_field = probe.
V();
515 target->V() += shape_field;
523#pragma omp parallel for schedule(dynamic, 16)
524 for (
Index i = 0; i < n_targets; ++i) {
525 PolarSite& target = *targets[std::size_t(i)];
529 const Eigen::Vector3d shift = copy.second - Centroid(bg);
532 source, target, shift);
564 double e_recip = 0.0;
565 double e_shape = 0.0;
568 std::vector<std::pair<const PolarSite*, Eigen::Vector3d>> fg_sites;
569 fg_sites.reserve(targets.size());
570 for (std::size_t n = 0; n < targets.size(); ++n) {
571 fg_sites.push_back({targets[n], targets[n]->getPos()});
573 const std::vector<const PolarSite*> no_exclusions;
575 e_recip =
recip_sum_->CalcStaticEnergyBetween(fg_sites, no_exclusions,
577 e_shape =
shape_->CalcStaticEnergyBetween(fg_sites, no_exclusions,
585 const Eigen::Vector3d shift = copy.second - Centroid(bg_copy);
586 for (
const PolarSite& source : bg_copy) {
587 for (
const auto& entry : fg_sites) {
589 source, *entry.first, shift);
593 energy += e_recip + e_shape - e_erf;
598 double e_recip_pu =
recip_sum_->CalcInducedSourceEnergyBetween(
600 double e_shape_pu =
shape_->CalcInducedSourceEnergyBetween(
602 double e_erf_pu = 0.0;
606 const Eigen::Vector3d shift = copy.second - Centroid(bg_copy);
607 for (
const PolarSite& source : bg_copy) {
608 for (
const auto& entry : fg_sites) {
609 e_erf_pu +=
interactor_->CalcErfInducedSourceEnergy(
610 source, *entry.first, shift);
614 energy += e_recip_pu + e_shape_pu - e_erf_pu;
628 <<
" Ewald energy [eV], permanent x permanent: real = " << e_real * h2ev
629 <<
" recip = " << e_recip * h2ev <<
" shape = " << e_shape * h2ev
630 <<
" erf = " << e_erf * h2ev << std::flush;
633 <<
" Ewald energy [eV], fg permanent x bg induced: real = "
634 << e_real_pu * h2ev <<
" recip = " << e_recip_pu * h2ev
635 <<
" shape = " << e_shape_pu * h2ev <<
" erf = " << e_erf_pu * h2ev
638 <<
TimeStamp() <<
" Ewald energy [eV], total = " << energy * h2ev
642 <<
" 1/bohr (" <<
params_.alpha * 18.8972612 <<
" 1/nm)"
643 <<
", k_max = " <<
params_.k_max <<
" 1/bohr ("
644 <<
params_.k_max * 18.8972612 <<
" 1/nm)"
645 <<
", r_min = " <<
params_.r_min
646 <<
" bohr, V = " <<
params_.box.determinant()
647 <<
" bohr^3, thole_a = " <<
params_.thole_a << std::flush;
650 <<
" segments, " << targets.size() <<
" sites, carved from "
651 <<
registry_.AllIds().size() <<
" registered" << std::flush;
659 const Index expected =
663 <<
" of " << expected
664 << ((stats.
foreground == expected) ?
" (ok)" :
" <-- MISMATCH")
670 for (std::size_t i = 0; i < targets.size(); ++i) {
671 const Eigen::Vector3d ewald_contribution = targets[i]->V() - v_before[i];
672 targets[i]->V() = v_before[i] - ewald_contribution;
679 <<
" NOTE: the background's own internal energy is deliberately "
680 "absent -- it is a constant that cancels in any charge-state "
681 "difference. Everything else is here; see ApplyFieldTo's "
682 "declaration for the full accounting."