Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
d3bj.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * D3BJDispersionCalculator — Grimme DFT-D3 dispersion energy, Becke-Johnson
5 * damping (two-body term only; three-body Axilrod-Teller-Muto is out of
6 * scope, matching this feature's own matched-inputs QE protocol which pins
7 * dftd3_threebody=.false.).
8 *
9 * S. Grimme, J. Antony, S. Ehrlich, H. Krieg, J. Chem. Phys. 132, 154104 (2010)
10 * S. Grimme, S. Ehrlich, L. Goerigk, J. Comput. Chem. 32, 1456 (2011) (BJ damping)
11 *
12 * Algorithm (energy only):
13 * 1. Coordination number CN_i = sum over periodic images of a smooth
14 * bond-counting function of r_ij vs. the covalent-radius sum.
15 * 2. C6_ij from a Gaussian-weighted average over the reference (CN, CN, C6)
16 * grid for that element pair — NOT a lookup at fixed CN, per Grimme
17 * eq. 8. C8_ij = 3 * C6_ij * r2r4_i * r2r4_j.
18 * 3. E_disp = -(s6 * E6 + s8 * E8), with
19 * E6 = sum_{pairs, images} C6_ij / (r_ij^6 + R0_ij^6)
20 * E8 = sum_{pairs, images} C8_ij / (r_ij^8 + R0_ij^8)
21 * and BJ damping radius R0_ij = a1*sqrt(3*r2r4_i*r2r4_j) + a2.
22 *
23 * Functional-specific (a1, a2, s8) parameters below are PBE's, matching the
24 * only functional Q3 currently ships; s6 = 1 always for BJ damping.
25 *
26 * Only elements in d3bj_reference_data.hpp are supported; species outside
27 * that set fail loudly rather than silently contributing zero dispersion.
28 */
29
30#include <cmath>
31#include <limits>
32#include <stdexcept>
33#include <string>
34#include <vector>
35
40
41namespace q3 {
42
44 public:
45 explicit D3BJDispersionCalculator(const Crystal& crystal_structure)
46 : crystal_structure_(crystal_structure) {}
47
48 [[nodiscard]] std::string name() const override {
49 return "d3bj";
50 }
51
52 /// Total D3(BJ) dispersion energy correction, in Rydberg.
53 [[nodiscard]] Real compute_energy() const override {
54 int number_of_atoms = crystal_structure_.number_of_atoms();
55 std::vector<int> atomic_numbers(number_of_atoms);
56 std::vector<Vector3D> cartesian_positions_bohr(number_of_atoms);
57 Matrix3x3 lattice_vectors_bohr = crystal_structure_.lattice_vectors_bohr();
58 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
59 atomic_numbers[atom_index] = crystal_structure_.atomic_number(atom_index);
60 if (dispersion::rcov_by_atomic_number(atomic_numbers[atom_index]) == 0.0) {
61 throw std::runtime_error("D3(BJ) dispersion: no reference data for element '" +
62 crystal_structure_.element(atom_index) +
63 "'. Supported elements: C, Si, Cr, Fe, Ni, Cu.");
64 }
65 /**
66 * convert_from_crystal_to_cartesian() returns positions in units
67 * of alat, not Bohr -- project through the Bohr-scaled lattice
68 * matrix directly instead.
69 */
70 const Vector3D& fractional = crystal_structure_.fractional_position(atom_index);
71 cartesian_positions_bohr[atom_index] = fractional.x * lattice_vectors_bohr[0] +
72 fractional.y * lattice_vectors_bohr[1] +
73 fractional.z * lattice_vectors_bohr[2];
74 }
75
76 std::vector<Real> coordination_numbers = compute_coordination_numbers(
77 atomic_numbers, cartesian_positions_bohr, lattice_vectors_bohr);
78 std::vector<Real> r2r4(number_of_atoms);
79 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
80 r2r4[atom_index] = dispersion::r2r4_by_atomic_number(atomic_numbers[atom_index]);
81 }
82
83 /**
84 * Periodic-image shifts covering the van der Waals cutoff sphere
85 * (QE default: cutoff_squared = 9000 Bohr^2), precomputed once for
86 * the whole pair sum -- see enumerate_image_shifts().
87 */
88 constexpr Real vdw_cutoff_squared_bohr = 9000.0;
89 const std::vector<LatticeImageShift> vdw_image_shifts =
90 enumerate_image_shifts(lattice_vectors_bohr, vdw_cutoff_squared_bohr);
91
92 Real energy_6 = 0.0;
93 Real energy_8 = 0.0;
94 for (int atom_i = 0; atom_i < number_of_atoms; atom_i++) {
95 for (int atom_j = atom_i; atom_j < number_of_atoms; atom_j++) {
96 Real c6 = interpolate_c6(atomic_numbers[atom_i],
97 atomic_numbers[atom_j],
98 coordination_numbers[atom_i],
99 coordination_numbers[atom_j]);
100 Real c8 = 3.0 * c6 * r2r4[atom_i] * r2r4[atom_j];
101 Real damping_radius = becke_johnson_damping_radius(r2r4[atom_i], r2r4[atom_j]);
102 Real damping_radius_6 = ipow(damping_radius, 6);
103 Real damping_radius_8 = ipow(damping_radius, 8);
104
105 Vector3D separation_bohr =
106 cartesian_positions_bohr[atom_i] - cartesian_positions_bohr[atom_j];
107 bool is_self_pair = (atom_i == atom_j);
108 /**
109 * Unordered-pair (i<j) contributions are counted once; the
110 * self-pair (i==i) periodic-image sum double-counts every
111 * image with its mirror translation, hence 0.5.
112 */
113 Real weight = is_self_pair ? 0.5 : 1.0;
114
115 for (const LatticeImageShift& image : vdw_image_shifts) {
116 if (is_self_pair && image.is_origin) {
117 continue;
118 }
119 Vector3D image_separation_bohr = separation_bohr + image.shift_bohr;
120 Real distance_squared_bohr = image_separation_bohr.x * image_separation_bohr.x +
121 image_separation_bohr.y * image_separation_bohr.y +
122 image_separation_bohr.z * image_separation_bohr.z;
123 if (distance_squared_bohr > vdw_cutoff_squared_bohr) {
124 continue;
125 }
126 Real distance_6_bohr = ipow(distance_squared_bohr, 3);
127 Real distance_8_bohr = distance_6_bohr * distance_squared_bohr;
128 energy_6 += weight * c6 / (distance_6_bohr + damping_radius_6);
129 energy_8 += weight * c8 / (distance_8_bohr + damping_radius_8);
130 }
131 }
132 }
133
134 constexpr Real s6 = 1.0; // PBE, BJ damping (always 1 for this damping scheme)
135 constexpr Real s8 = 0.7875; // PBE, BJ damping (Grimme & al. 2011, version "4")
136 constexpr Real hartree_to_rydberg = 2.0;
137 return -(s6 * energy_6 + s8 * energy_8) * hartree_to_rydberg;
138 }
139
140 private:
141 static constexpr Real k1_ = 16.0; // CN counting-function steepness
142 static constexpr Real k3_ = -4.0; // C6 interpolation Gaussian weight
143 static constexpr Real bj_a1_ = 0.4289; // PBE, BJ damping radius scale
144 static constexpr Real bj_a2_ = 4.4407; // PBE, BJ damping radius offset
145
146 [[nodiscard]] static Real ipow(Real base, int exponent) {
147 Real result = 1.0;
148 for (int i = 0; i < exponent; i++) {
149 result *= base;
150 }
151 return result;
152 }
153
154 [[nodiscard]] static Real becke_johnson_damping_radius(Real r2r4_i, Real r2r4_j) {
155 return bj_a1_ * std::sqrt(3.0 * r2r4_i * r2r4_j) + bj_a2_;
156 }
157
158 /**
159 * Real-space translation range (in lattice-vector units) needed to cover
160 * a sphere of the given squared radius, matching QE's SET_CRITERIA: the
161 * number of cells along each reciprocal direction is set by how many
162 * interplanar spacings fit inside the cutoff.
163 */
164 [[nodiscard]] static IntegerVector3D
165 lattice_translation_range(const Matrix3x3& lattice_vectors_bohr, Real cutoff_squared_bohr) {
166 Real cutoff_bohr = std::sqrt(cutoff_squared_bohr);
167 Real cell_volume = std::abs(lattice_vectors_bohr.determinant());
168 IntegerVector3D range;
169 for (int direction = 0; direction < 3; direction++) {
170 const Vector3D& first = lattice_vectors_bohr[(direction + 1) % 3];
171 const Vector3D& second = lattice_vectors_bohr[(direction + 2) % 3];
172 Real interplanar_spacing = cell_volume / first.cross(second).norm();
173 range[direction] = static_cast<int>(cutoff_bohr / interplanar_spacing) + 1;
174 }
175 return range;
176 }
177
178 /**
179 * One precomputed periodic-image shift; is_origin marks the (0,0,0) cell,
180 * which self-pair (i == i) sums must skip.
181 */
182 struct LatticeImageShift {
183 Vector3D shift_bohr;
184 bool is_origin;
185 };
186
187 /**
188 * Every periodic-image shift inside the translation range covering the
189 * given cutoff, precomputed once per lattice sum instead of re-deriving
190 * the shift vector inside triple-nested translation loops at every atom
191 * pair. Enumeration order is exactly the previous x-outer / y-middle /
192 * z-inner loop order, so pair sums iterating this list accumulate in the
193 * identical order and stay bit-identical to the pre-flattened code.
194 */
195 [[nodiscard]] static std::vector<LatticeImageShift>
196 enumerate_image_shifts(const Matrix3x3& lattice_vectors_bohr, Real cutoff_squared_bohr) {
197 IntegerVector3D range =
198 lattice_translation_range(lattice_vectors_bohr, cutoff_squared_bohr);
199 std::vector<LatticeImageShift> image_shifts;
200 image_shifts.reserve(static_cast<std::size_t>(2 * range.x + 1) *
201 static_cast<std::size_t>(2 * range.y + 1) *
202 static_cast<std::size_t>(2 * range.z + 1));
203 for (int translation_x = -range.x; translation_x <= range.x; translation_x++) {
204 for (int translation_y = -range.y; translation_y <= range.y; translation_y++) {
205 for (int translation_z = -range.z; translation_z <= range.z; translation_z++) {
206 image_shifts.push_back(
207 {translation_x * lattice_vectors_bohr[0] +
208 translation_y * lattice_vectors_bohr[1] +
209 translation_z * lattice_vectors_bohr[2],
210 translation_x == 0 && translation_y == 0 && translation_z == 0});
211 }
212 }
213 }
214 return image_shifts;
215 }
216
217 [[nodiscard]] std::vector<Real>
218 compute_coordination_numbers(const std::vector<int>& atomic_numbers,
219 const std::vector<Vector3D>& cartesian_positions_bohr,
220 const Matrix3x3& lattice_vectors_bohr) const {
221 constexpr Real cn_cutoff_squared_bohr = 1600.0; // QE default: cutoff_cn = sqrt(1600) Bohr
222 const std::vector<LatticeImageShift> cn_image_shifts =
223 enumerate_image_shifts(lattice_vectors_bohr, cn_cutoff_squared_bohr);
224 int number_of_atoms = static_cast<int>(atomic_numbers.size());
225 std::vector<Real> covalent_radii(number_of_atoms);
226 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
227 covalent_radii[atom_index] =
228 dispersion::rcov_by_atomic_number(atomic_numbers[atom_index]);
229 }
230 std::vector<Real> coordination_numbers(number_of_atoms, 0.0);
231
232 for (int atom_i = 0; atom_i < number_of_atoms; atom_i++) {
233 for (int atom_j = 0; atom_j < number_of_atoms; atom_j++) {
234 Real covalent_radius_sum = covalent_radii[atom_i] + covalent_radii[atom_j];
235 Vector3D separation_bohr =
236 cartesian_positions_bohr[atom_j] - cartesian_positions_bohr[atom_i];
237 for (const LatticeImageShift& image : cn_image_shifts) {
238 if (atom_i == atom_j && image.is_origin) {
239 continue;
240 }
241 Vector3D image_separation_bohr = separation_bohr + image.shift_bohr;
242 Real distance_squared_bohr = image_separation_bohr.x * image_separation_bohr.x +
243 image_separation_bohr.y * image_separation_bohr.y +
244 image_separation_bohr.z * image_separation_bohr.z;
245 if (distance_squared_bohr > cn_cutoff_squared_bohr) {
246 continue;
247 }
248 Real distance_bohr = std::sqrt(distance_squared_bohr);
249 Real ratio = covalent_radius_sum / distance_bohr;
250 coordination_numbers[atom_i] += 1.0 / (1.0 + std::exp(-k1_ * (ratio - 1.0)));
251 }
252 }
253 }
254 return coordination_numbers;
255 }
256
257 /**
258 * Value and coordination-number partial derivatives of the
259 * Gaussian-weighted C6 average (Grimme 2010, eq. 8), computed in a
260 * single pass over the reference table for this element pair so that
261 * `interpolate_c6()` and the gradient path can never disagree on the
262 * value itself.
263 */
264 struct C6InterpolationResult {
265 Real value = 0.0;
266 Real derivative_with_respect_to_coordination_number_i = 0.0;
267 Real derivative_with_respect_to_coordination_number_j = 0.0;
268 };
269
270 [[nodiscard]] static C6InterpolationResult
271 interpolate_c6_and_derivatives(int atomic_number_i,
272 int atomic_number_j,
273 Real coordination_number_i,
274 Real coordination_number_j) {
275 Real weighted_sum = 0.0; // sum_ref w_ref * C6_ref
276 Real weight_sum = 0.0; // sum_ref w_ref
277 Real weighted_offset_i = 0.0; // sum_ref w_ref * (CN_i - CNref_i)
278 Real weighted_offset_j = 0.0; // sum_ref w_ref * (CN_j - CNref_j)
279 Real weighted_c6_offset_i = 0.0; // sum_ref w_ref * C6_ref * (CN_i - CNref_i)
280 Real weighted_c6_offset_j = 0.0; // sum_ref w_ref * C6_ref * (CN_j - CNref_j)
281 /**
282 * Fallback for when the actual coordination number falls far outside
283 * the fitted reference grid (common for close-packed bulk metals,
284 * whose CN often exceeds every tabulated reference point): matches
285 * QE's getc6() c6mem/r_save behavior of using the single nearest
286 * reference point's C6 rather than an interpolated (numerically
287 * underflowing) average.
288 */
289 Real nearest_c6 = 0.0;
290 Real nearest_distance_squared = std::numeric_limits<Real>::max();
291 bool found_reference_point = false;
292 for (const auto& point : dispersion::c6_reference_table()) {
293 /**
294 * The table is pre-symmetrized (both (Z_i,Z_j) and (Z_j,Z_i)
295 * orderings are present as separate entries), so a direct match
296 * is sufficient -- no separate swapped-order check needed.
297 */
298 if (point.atomic_number_i != atomic_number_i ||
299 point.atomic_number_j != atomic_number_j) {
300 continue;
301 }
302 found_reference_point = true;
303 Real offset_i = coordination_number_i - point.coordination_number_i;
304 Real offset_j = coordination_number_j - point.coordination_number_j;
305 Real cn_distance_squared = offset_i * offset_i + offset_j * offset_j;
306 if (cn_distance_squared < nearest_distance_squared) {
307 nearest_distance_squared = cn_distance_squared;
308 nearest_c6 = point.c6;
309 }
310 Real weight = std::exp(k3_ * cn_distance_squared);
311 weighted_sum += point.c6 * weight;
312 weight_sum += weight;
313 weighted_offset_i += weight * offset_i;
314 weighted_offset_j += weight * offset_j;
315 weighted_c6_offset_i += weight * point.c6 * offset_i;
316 weighted_c6_offset_j += weight * point.c6 * offset_j;
317 }
318 if (!found_reference_point) {
319 throw std::runtime_error("D3(BJ) dispersion: no C6 reference data for element pair (" +
320 std::to_string(atomic_number_i) + ", " +
321 std::to_string(atomic_number_j) + ")");
322 }
323 /**
324 * Underflow guard, mirroring dftd3's reference implementation: when all
325 * Gaussian CN weights underflow, fall back to the nearest reference point.
326 * In that regime C6 is locally constant (deep in the Gaussian tail), so
327 * its coordination-number derivative is consistently taken to be zero.
328 */
329 constexpr Real coordination_weight_underflow_floor = 1e-99;
330 if (weight_sum <= coordination_weight_underflow_floor) {
331 return {nearest_c6, 0.0, 0.0};
332 }
333 Real value = weighted_sum / weight_sum;
334 Real derivative_i =
335 2.0 * k3_ / weight_sum * (weighted_c6_offset_i - value * weighted_offset_i);
336 Real derivative_j =
337 2.0 * k3_ / weight_sum * (weighted_c6_offset_j - value * weighted_offset_j);
338 return {value, derivative_i, derivative_j};
339 }
340
341 /**
342 * Gaussian-weighted average of C6 over all (CN_ref_i, CN_ref_j) grid
343 * points for this element pair (Grimme 2010, eq. 8) — not a lookup at
344 * the nearest reference point.
345 */
346 [[nodiscard]] static Real interpolate_c6(int atomic_number_i,
347 int atomic_number_j,
348 Real coordination_number_i,
349 Real coordination_number_j) {
350 return interpolate_c6_and_derivatives(
351 atomic_number_i, atomic_number_j, coordination_number_i, coordination_number_j)
352 .value;
353 }
354
355 public:
356 /**
357 * Analytical gradients of the D3(BJ) dispersion energy (forces and
358 * stress; units and sign conventions documented on DispersionGradients).
359 *
360 * Two coupled mechanisms contribute, mirroring compute_energy() exactly:
361 * 1. Direct: r_ij appears explicitly in 1/(r^6+R0^6) and 1/(r^8+R0^8).
362 * 2. Indirect: C6_ij depends on CN_i and CN_j (interpolate_c6), and
363 * each coordination number is itself a smooth sum over ALL of that
364 * atom's neighbors -- so a displaced neighbor changes C6_ij even
365 * for pairs it is not directly part of. Handled via a standard
366 * two-pass algorithm: pass 1 walks the same (C6, C8) pair loop as
367 * compute_energy() and accumulates dE/dCN_i per atom; pass 2 walks
368 * the same neighbor loop as compute_coordination_numbers() and
369 * applies the chain rule through dCN_i/dR.
370 */
371 [[nodiscard]] DispersionGradients compute_gradients() const override {
372 int number_of_atoms = crystal_structure_.number_of_atoms();
373 std::vector<int> atomic_numbers(number_of_atoms);
374 std::vector<Vector3D> cartesian_positions_bohr(number_of_atoms);
375 Matrix3x3 lattice_vectors_bohr = crystal_structure_.lattice_vectors_bohr();
376 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
377 atomic_numbers[atom_index] = crystal_structure_.atomic_number(atom_index);
378 if (dispersion::rcov_by_atomic_number(atomic_numbers[atom_index]) == 0.0) {
379 throw std::runtime_error("D3(BJ) dispersion: no reference data for element '" +
380 crystal_structure_.element(atom_index) +
381 "'. Supported elements: C, Si, Cr, Fe, Ni, Cu.");
382 }
383 const Vector3D& fractional = crystal_structure_.fractional_position(atom_index);
384 cartesian_positions_bohr[atom_index] = fractional.x * lattice_vectors_bohr[0] +
385 fractional.y * lattice_vectors_bohr[1] +
386 fractional.z * lattice_vectors_bohr[2];
387 }
388
389 std::vector<Real> coordination_numbers = compute_coordination_numbers(
390 atomic_numbers, cartesian_positions_bohr, lattice_vectors_bohr);
391 std::vector<Real> r2r4(number_of_atoms);
392 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
393 r2r4[atom_index] = dispersion::r2r4_by_atomic_number(atomic_numbers[atom_index]);
394 }
395
396 constexpr Real vdw_cutoff_squared_bohr = 9000.0;
397 const std::vector<LatticeImageShift> vdw_image_shifts =
398 enumerate_image_shifts(lattice_vectors_bohr, vdw_cutoff_squared_bohr);
399
400 constexpr Real s6 = 1.0;
401 constexpr Real s8 = 0.7875;
402 constexpr Real hartree_to_rydberg = 2.0;
403
404 std::vector<Vector3D> forces(number_of_atoms, {0.0, 0.0, 0.0});
405 Real stress_raw[3][3] = {};
406 /**
407 * g_i = dE/dCN_i, accumulated across every (C6, C8) pair atom i takes
408 * part in (either role, including its own self-pair).
409 */
410 std::vector<Real> coordination_number_energy_derivative(number_of_atoms, 0.0);
411
412 // PASS 1: direct pairwise force + stress, plus accumulation of g_i.
413 for (int atom_i = 0; atom_i < number_of_atoms; atom_i++) {
414 for (int atom_j = atom_i; atom_j < number_of_atoms; atom_j++) {
415 C6InterpolationResult c6_result =
416 interpolate_c6_and_derivatives(atomic_numbers[atom_i],
417 atomic_numbers[atom_j],
418 coordination_numbers[atom_i],
419 coordination_numbers[atom_j]);
420 Real c6 = c6_result.value;
421 Real c8 = 3.0 * c6 * r2r4[atom_i] * r2r4[atom_j];
422 Real r2r4_product_times_three = 3.0 * r2r4[atom_i] * r2r4[atom_j];
423 Real damping_radius = becke_johnson_damping_radius(r2r4[atom_i], r2r4[atom_j]);
424 Real damping_radius_6 = ipow(damping_radius, 6);
425 Real damping_radius_8 = ipow(damping_radius, 8);
426
427 Vector3D separation_bohr =
428 cartesian_positions_bohr[atom_i] - cartesian_positions_bohr[atom_j];
429 bool is_self_pair = (atom_i == atom_j);
430 Real weight = is_self_pair ? 0.5 : 1.0;
431
432 // dE_pair/dCN_i and dE_pair/dCN_j, summed over this pair's images.
433 Real pair_derivative_with_respect_to_cn_i = 0.0;
434 Real pair_derivative_with_respect_to_cn_j = 0.0;
435
436 for (const LatticeImageShift& image : vdw_image_shifts) {
437 if (is_self_pair && image.is_origin) {
438 continue;
439 }
440 Vector3D image_separation_bohr = separation_bohr + image.shift_bohr;
441 Real distance_squared_bohr = image_separation_bohr.x * image_separation_bohr.x +
442 image_separation_bohr.y * image_separation_bohr.y +
443 image_separation_bohr.z * image_separation_bohr.z;
444 if (distance_squared_bohr > vdw_cutoff_squared_bohr) {
445 continue;
446 }
447 Real distance_6_bohr = ipow(distance_squared_bohr, 3);
448 Real distance_8_bohr = distance_6_bohr * distance_squared_bohr;
449 Real denominator_6 = distance_6_bohr + damping_radius_6;
450 Real denominator_8 = distance_8_bohr + damping_radius_8;
451
452 pair_derivative_with_respect_to_cn_i +=
453 weight * (s6 * c6_result.derivative_with_respect_to_coordination_number_i /
454 denominator_6 +
455 s8 * r2r4_product_times_three *
456 c6_result.derivative_with_respect_to_coordination_number_i /
457 denominator_8);
458 pair_derivative_with_respect_to_cn_j +=
459 weight * (s6 * c6_result.derivative_with_respect_to_coordination_number_j /
460 denominator_6 +
461 s8 * r2r4_product_times_three *
462 c6_result.derivative_with_respect_to_coordination_number_j /
463 denominator_8);
464
465 /**
466 * Direct term: d(1/(r^6+R0^6))/dR and d(1/(r^8+R0^8))/dR,
467 * expressed sqrt-free via r^2 (matching compute_energy()).
468 */
469 Real distance_squared_power_2 = distance_squared_bohr * distance_squared_bohr;
470 Real direct_coefficient =
471 hartree_to_rydberg * weight *
472 (6.0 * s6 * c6 * distance_squared_power_2 /
473 (denominator_6 * denominator_6) +
474 8.0 * s8 * c8 * distance_6_bohr / (denominator_8 * denominator_8));
475
476 /**
477 * Force: F_i = -dE/dR_i. Self-pair separations are pure
478 * lattice vectors (position-independent), so the two
479 * increments below cancel automatically for atom_i==atom_j
480 * without any special-casing -- they still contribute to
481 * stress below, since the cell shape does move them.
482 */
483 Vector3D pair_force_on_i = (-direct_coefficient) * image_separation_bohr;
484 forces[atom_i] += pair_force_on_i;
485 forces[atom_j] += (-pair_force_on_i);
486
487 for (int direction_a = 0; direction_a < 3; direction_a++) {
488 for (int direction_b = 0; direction_b < 3; direction_b++) {
489 stress_raw[direction_a][direction_b] +=
490 direct_coefficient * image_separation_bohr[direction_a] *
491 image_separation_bohr[direction_b];
492 }
493 }
494 }
495 coordination_number_energy_derivative[atom_i] +=
496 -hartree_to_rydberg * pair_derivative_with_respect_to_cn_i;
497 coordination_number_energy_derivative[atom_j] +=
498 -hartree_to_rydberg * pair_derivative_with_respect_to_cn_j;
499 }
500 }
501
502 /**
503 * PASS 2: chain rule through dCN_i/dR, walking the identical neighbor
504 * loop as compute_coordination_numbers().
505 */
506 constexpr Real cn_cutoff_squared_bohr = 1600.0;
507 const std::vector<LatticeImageShift> cn_image_shifts =
508 enumerate_image_shifts(lattice_vectors_bohr, cn_cutoff_squared_bohr);
509 std::vector<Real> covalent_radii(number_of_atoms);
510 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
511 covalent_radii[atom_index] =
512 dispersion::rcov_by_atomic_number(atomic_numbers[atom_index]);
513 }
514 for (int atom_i = 0; atom_i < number_of_atoms; atom_i++) {
515 Real energy_derivative_i = coordination_number_energy_derivative[atom_i];
516 for (int atom_j = 0; atom_j < number_of_atoms; atom_j++) {
517 Real covalent_radius_sum = covalent_radii[atom_i] + covalent_radii[atom_j];
518 Vector3D separation_bohr =
519 cartesian_positions_bohr[atom_j] - cartesian_positions_bohr[atom_i];
520 for (const LatticeImageShift& image : cn_image_shifts) {
521 if (atom_i == atom_j && image.is_origin) {
522 continue;
523 }
524 Vector3D image_separation_bohr = separation_bohr + image.shift_bohr;
525 Real distance_squared_bohr = image_separation_bohr.x * image_separation_bohr.x +
526 image_separation_bohr.y * image_separation_bohr.y +
527 image_separation_bohr.z * image_separation_bohr.z;
528 if (distance_squared_bohr > cn_cutoff_squared_bohr) {
529 continue;
530 }
531 Real distance_bohr = std::sqrt(distance_squared_bohr);
532 Real ratio = covalent_radius_sum / distance_bohr;
533 Real logistic_argument = -k1_ * (ratio - 1.0);
534 Real logistic_value = 1.0 / (1.0 + std::exp(logistic_argument));
535 /**
536 * d(logistic)/dr = -k1*(ratio/r)*L*(1-L); CN decreases as
537 * atoms move apart, so this is negative.
538 */
539 Real counting_function_derivative =
540 -k1_ * (ratio / distance_bohr) * logistic_value * (1.0 - logistic_value);
541
542 Real coefficient =
543 energy_derivative_i * counting_function_derivative / distance_bohr;
544 Vector3D gradient_contribution = coefficient * image_separation_bohr;
545 forces[atom_i] += gradient_contribution;
546 forces[atom_j] += (-gradient_contribution);
547
548 for (int direction_a = 0; direction_a < 3; direction_a++) {
549 for (int direction_b = 0; direction_b < 3; direction_b++) {
550 stress_raw[direction_a][direction_b] +=
551 coefficient * image_separation_bohr[direction_a] *
552 image_separation_bohr[direction_b];
553 }
554 }
555 }
556 }
557 }
558
559 Real cell_volume_bohr_cubed = crystal_structure_.cell_volume();
560 Matrix3x3 stress;
561 for (int direction_a = 0; direction_a < 3; direction_a++) {
562 for (int direction_b = 0; direction_b < 3; direction_b++) {
563 stress[direction_a][direction_b] =
564 -stress_raw[direction_a][direction_b] / cell_volume_bohr_cubed;
565 }
566 }
567
568 return {forces, stress};
569 }
570
571 private:
572 const Crystal& crystal_structure_;
573};
574
575} // namespace q3
Real cell_volume() const
Definition crystal.hpp:193
Vector3D fractional_position(int atom_index) const
Definition crystal.hpp:269
const std::string & element(int atom_index) const
Definition crystal.hpp:346
int atomic_number(int atom_index) const
Definition crystal.hpp:338
Matrix3x3 lattice_vectors_bohr() const
Definition crystal.hpp:324
int number_of_atoms() const
Definition crystal.hpp:209
D3BJDispersionCalculator(const Crystal &crystal_structure)
Definition d3bj.hpp:45
Real compute_energy() const override
Total D3(BJ) dispersion energy correction, in Rydberg.
Definition d3bj.hpp:53
DispersionGradients compute_gradients() const override
Definition d3bj.hpp:371
std::string name() const override
Definition d3bj.hpp:48
Real r2r4_by_atomic_number(int atomic_number)
sqrt(0.5 * <r^4>/<r^2> * sqrt(Z)) atomic values, indexed by atomic number (1-based).
Real rcov_by_atomic_number(int atomic_number)
D3 covalent radii (Pyykko-Atsumi, metals scaled -10%), indexed by atomic number.
const std::array< C6ReferencePoint, number_of_c6_reference_points > & c6_reference_table()
double Real
3×3 matrix stored row-major: row i = mat[i]