Source code for chemistrykit.crystal.systems.madelung

r"""The Madelung constant of the NaCl (rock-salt) structure, from a genuinely converging lattice sum.

The Madelung constant `M` of an ionic crystal is defined so that the
electrostatic lattice energy per ion pair is

.. math::

    U = -\frac{M z_+ z_- e^2}{4\pi\varepsilon_0 r_0}

with :math:`r_0` the nearest-neighbor (cation-anion) distance and
:math:`z_\pm` the (unsigned) ionic charge numbers (West, *Solid State
Chemistry and its Applications*, 2nd ed. (2014), Ch. 1.4; this is the `M`
consumed by :class:`chemistrykit.crystal.systems.lattice_energy.BornLandeLatticeEnergy`).
For NaCl, `M` is defined by the lattice sum

.. math::

    M = -\sum_{j\neq0}(-1)^{i+j+k}\,\frac{r_0}{r_j}

which, remarkably, can be evaluated over a *simple cubic* lattice of
alternating unit charges at spacing :math:`r_0`: NaCl's two
interpenetrating FCC sublattices (Na+ at the FCC points, Cl- at the FCC
points offset by half the conventional cell edge) combine into a single
simple cubic lattice of spacing :math:`a/2` (`a` the conventional cubic
cell edge) with ion charge alternating by the parity of :math:`i+j+k`,
exactly the object :func:`chemistrykit.crystal.utils.lattice_sums.evjen_lattice_sum_cubic_alternating`
sums (Ashcroft & Mermin, *Solid State Physics*, 1976, problem 20.1).

**Why this needs Evjen's method, not a naive truncated sum.** The series
above is only *conditionally* convergent -- it has no well-defined value
independent of summation order, so truncating the sum to a growing cube
or sphere of lattice points does not settle down to the right answer as
the cutoff grows; it *oscillates* (each newly-added shell is not
itself charge-neutral). Demonstrating this explicitly:

>>> import numpy as np
>>> def naive_sum(n):
...     total = 0.0
...     for i in range(-n, n + 1):
...         for j in range(-n, n + 1):
...             for k in range(-n, n + 1):
...                 if i == 0 and j == 0 and k == 0:
...                     continue
...                 total += (-1) ** (i + j + k) / np.sqrt(i * i + j * j + k * k)
...     return -total
>>> naive_values = [naive_sum(n) for n in range(6, 11)]
>>> bool(np.std(naive_values) > 0.05)  # still swinging wildly at n=6..10
True
>>> evjen_values = [madelung_constant_nacl(n) for n in range(6, 11)]
>>> bool(np.std(evjen_values) < 1e-4)  # Evjen's method has already settled down
True

whereas :func:`madelung_constant_nacl` (Evjen-weighted) converges quickly
and matches the literature value :math:`M_{NaCl}=1.747565` (Kittel,
*Introduction to Solid State Physics*, 8th ed., Table 1, Ch. 3) to 5
significant figures well before `n_shells=20`.
"""

from __future__ import annotations

from chemistrykit.crystal.utils.lattice_sums import evjen_lattice_sum_cubic_alternating

__all__ = ["MADELUNG_CONSTANT_NACL_LITERATURE", "madelung_constant_nacl"]

#: float: The accepted literature value of the NaCl Madelung constant
#: (Kittel, *Introduction to Solid State Physics*, 8th ed., Table 1, Ch.
#: 3), given here purely as a citation/regression-test reference -- see
#: the module docstring for the independent numerical derivation.
MADELUNG_CONSTANT_NACL_LITERATURE = 1.747565


[docs] def madelung_constant_nacl(n_shells: int = 12) -> float: r"""Compute the NaCl Madelung constant via Evjen's converging lattice summation. Parameters ---------- n_shells : int, default 12 Half-width (in simple-cubic lattice units) of the summation region passed to :func:`chemistrykit.crystal.utils.lattice_sums.evjen_lattice_sum_cubic_alternating`. The default already agrees with the literature value (:data:`MADELUNG_CONSTANT_NACL_LITERATURE`) to 5 significant figures. Returns ------- float Examples -------- >>> round(madelung_constant_nacl(), 4) 1.7476 >>> round(madelung_constant_nacl(20), 6) 1.747565 """ return -evjen_lattice_sum_cubic_alternating(n_shells)