176 const std::vector<std::pair<Index, Eigen::Vector3d>>& foreground) {
181 for (std::size_t i = 0; i < foreground.size(); ++i) {
182 for (std::size_t j = i + 1; j < foreground.size(); ++j) {
183 if (foreground[i].first == foreground[j].first) {
184 std::stringstream message;
185 message <<
"EwaldRegion::RegisterForeground: segment "
186 << foreground[i].first
187 <<
" was declared twice. The foreground is the disjoint "
188 "union of the regions that own segments.";
189 throw std::runtime_error(message.str());
205 const std::vector<PolarSegment>& foreground)
const {
211 constexpr double kTol = 1
e-4;
213 const Eigen::Vector3d centroid = Centroid(seg);
216 if (entry.first == seg.getId() &&
217 (centroid - entry.second).norm() <= kTol) {
223 std::stringstream message;
224 message <<
"EwaldRegion: asked about segment " << seg.getId()
225 <<
", which is not part of the foreground these sums were "
226 "built for. The suppression list and the real-space "
227 "neighbour cache belong to that foreground, so answering "
228 "would drop the wrong background copies and subtract erf "
229 "corrections for copies that were never dropped -- a wrong "
230 "energy with no symptom. JobTopology declares the whole "
231 "foreground with RegisterForeground before any region is "
232 "evaluated; if this fires, that declaration is missing or "
234 throw std::runtime_error(message.str());
240 std::vector<std::pair<Index, Eigen::Vector3d>> source =
242 if (source.empty()) {
244 source.push_back({seg.getId(), Centroid(seg)});
250 for (
const auto& entry : source) {
251 const Index seg_id = entry.first;
253 throw std::runtime_error(
254 "EwaldRegion: the foreground contains segment " +
255 std::to_string(seg_id) +
256 ", which is absent from the periodic background. The foreground "
257 "must be carved out of the same system the background was "
284 const Eigen::Vector3d bg_centroid = Centroid(bg_copy);
285 const Eigen::Vector3d delta = entry.second - bg_centroid;
286 const Eigen::Vector3d fractional =
params_.box.inverse() * delta;
287 const Eigen::Vector3d image =
288 params_.box * fractional.array().round().matrix();
289 const Eigen::Vector3d residual = delta - image;
295 constexpr double kResidualWarn = 5.0;
296 if (residual.norm() > kResidualWarn) {
298 <<
TimeStamp() <<
" WARNING: foreground segment " << seg_id
299 <<
" sits " << residual.norm()
300 <<
" bohr from the nearest periodic image of its background "
301 "copy. That is too far to be geometry relaxation, so the "
302 "copy this code is about to suppress may not be the one the "
303 "foreground actually displaces."
310 real_sum_ = std::make_unique<EwaldRealSpaceSum>(
314 recip_sum_ = std::make_unique<EwaldReciprocalSpaceSum>(
316 shape_ = std::make_unique<EwaldShapeCorrection>(
params_.box.determinant(),
323 const std::vector<Eigen::Vector3d>& points)
const {
325 NotInitialized(
"PotentialAt");
328 throw std::runtime_error(
329 "EwaldRegion::PotentialAt: no foreground has been declared. "
330 "ApplyFieldTo can fall back on the segments it is handed, but a "
331 "list of points carries no segment identity, so the copies to "
332 "suppress cannot be inferred. JobTopology declares the foreground "
333 "with RegisterForeground before any region is evaluated.");
345 Eigen::VectorXd phi =
real_sum_->PotentialAtMany(probe_segment_id, points,
354 const std::vector<const PolarSite*> no_exclusions;
355#pragma omp parallel for schedule(static)
356 for (
Index p = 0; p < n_points; ++p) {
357 PolarSite probe(0,
"H", points[std::size_t(p)]);
362 const std::vector<std::pair<const PolarSite*, Eigen::Vector3d>> one{
363 {&probe, points[std::size_t(p)]}};
365 double extra =
shape_->CalcStaticEnergyBetween(one, no_exclusions,
367 shape_->CalcInducedSourceEnergyBetween(
376 const Eigen::Vector3d shift = copy.second - Centroid(bg);
379 source, probe, shift);
380 extra -=
interactor_->CalcErfInducedSourceEnergy(source, probe, shift);
390 NotInitialized(
"ApplyFieldTo");
402 std::vector<PolarSite*> targets;
403 std::vector<Index> target_segment_ids;
406 targets.push_back(&site);
407 target_segment_ids.push_back(seg.getId());
429 std::vector<Eigen::Vector3d> v_before;
430 v_before.reserve(targets.size());
431 for (
const PolarSite* target : targets) {
432 v_before.push_back(target->V());
436 const Index n_targets =
Index(targets.size());
437#pragma omp parallel for schedule(dynamic, 16)
438 for (
Index i = 0; i < n_targets; ++i) {
440 *targets[std::size_t(i)],
455#pragma omp parallel for schedule(dynamic, 16) reduction(+ : e_real)
456 for (
Index i = 0; i < n_targets; ++i) {
457 e_real +=
real_sum_->CalcStaticEnergyAt(*targets[std::size_t(i)],
460 double energy = e_real;
472 double e_real_pu = 0.0;
473#pragma omp parallel for schedule(dynamic, 16) reduction(+ : e_real_pu)
474 for (
Index i = 0; i < n_targets; ++i) {
475 e_real_pu +=
real_sum_->CalcInducedSourceEnergyAt(
485 PolarSite probe(-1,
"X", Eigen::Vector3d::Zero());
487 const Eigen::Vector3d shape_field = probe.
V();
489 target->V() += shape_field;
497#pragma omp parallel for schedule(dynamic, 16)
498 for (
Index i = 0; i < n_targets; ++i) {
499 PolarSite& target = *targets[std::size_t(i)];
503 const Eigen::Vector3d shift = copy.second - Centroid(bg);
506 source, target, shift);
538 double e_recip = 0.0;
539 double e_shape = 0.0;
542 std::vector<std::pair<const PolarSite*, Eigen::Vector3d>> fg_sites;
543 fg_sites.reserve(targets.size());
544 for (std::size_t n = 0; n < targets.size(); ++n) {
545 fg_sites.push_back({targets[n], targets[n]->getPos()});
547 const std::vector<const PolarSite*> no_exclusions;
549 e_recip =
recip_sum_->CalcStaticEnergyBetween(fg_sites, no_exclusions,
551 e_shape =
shape_->CalcStaticEnergyBetween(fg_sites, no_exclusions,
559 const Eigen::Vector3d shift = copy.second - Centroid(bg_copy);
560 for (
const PolarSite& source : bg_copy) {
561 for (
const auto& entry : fg_sites) {
563 source, *entry.first, shift);
567 energy += e_recip + e_shape - e_erf;
572 double e_recip_pu =
recip_sum_->CalcInducedSourceEnergyBetween(
574 double e_shape_pu =
shape_->CalcInducedSourceEnergyBetween(
576 double e_erf_pu = 0.0;
580 const Eigen::Vector3d shift = copy.second - Centroid(bg_copy);
581 for (
const PolarSite& source : bg_copy) {
582 for (
const auto& entry : fg_sites) {
583 e_erf_pu +=
interactor_->CalcErfInducedSourceEnergy(
584 source, *entry.first, shift);
588 energy += e_recip_pu + e_shape_pu - e_erf_pu;
602 <<
" Ewald energy [eV], permanent x permanent: real = " << e_real * h2ev
603 <<
" recip = " << e_recip * h2ev <<
" shape = " << e_shape * h2ev
604 <<
" erf = " << e_erf * h2ev << std::flush;
607 <<
" Ewald energy [eV], fg permanent x bg induced: real = "
608 << e_real_pu * h2ev <<
" recip = " << e_recip_pu * h2ev
609 <<
" shape = " << e_shape_pu * h2ev <<
" erf = " << e_erf_pu * h2ev
612 <<
TimeStamp() <<
" Ewald energy [eV], total = " << energy * h2ev
616 <<
" 1/bohr (" <<
params_.alpha * 18.8972612 <<
" 1/nm)"
617 <<
", k_max = " <<
params_.k_max <<
" 1/bohr ("
618 <<
params_.k_max * 18.8972612 <<
" 1/nm)"
619 <<
", r_min = " <<
params_.r_min
620 <<
" bohr, V = " <<
params_.box.determinant()
621 <<
" bohr^3, thole_a = " <<
params_.thole_a << std::flush;
624 <<
" segments, " << targets.size() <<
" sites, carved from "
625 <<
registry_.AllIds().size() <<
" registered" << std::flush;
633 const Index expected =
637 <<
" of " << expected
638 << ((stats.
foreground == expected) ?
" (ok)" :
" <-- MISMATCH")
644 for (std::size_t i = 0; i < targets.size(); ++i) {
645 const Eigen::Vector3d ewald_contribution = targets[i]->V() - v_before[i];
646 targets[i]->V() = v_before[i] - ewald_contribution;
653 <<
" NOTE: the background's own internal energy is deliberately "
654 "absent -- it is a constant that cancels in any charge-state "
655 "difference. Everything else is here; see ApplyFieldTo's "
656 "declaration for the full accounting."