Skip to main content

coulomb/pairwise/schemes/
poisson.rs

1// Copyright 2023 Björn Stenqvist and Mikael Lund
2//
3// Converted to Rust with modification from the C++ library "CoulombGalore":
4// https://zenodo.org/doi/10.5281/zenodo.3522058
5//
6// Licensed under the Apache license, version 2.0 (the "license");
7// you may not use this file except in compliance with the license.
8// You may obtain a copy of the license at
9//
10//     http://www.apache.org/licenses/license-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the license is distributed on an "as is" basis,
14// without warranties or conditions of any kind, either express or implied.
15// See the license for the specific language governing permissions and
16// limitations under the license.
17
18use crate::debye_length::DebyeLength;
19use crate::pairwise::{SelfEnergyPrefactors, ShortRangeFunction};
20use num_integer::binomial;
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Deserializer, Serialize};
23
24/// # Scheme for the Poisson short-range function
25///
26/// This is a general scheme for the short-ranged part of the electrostatic interaction
27/// which can be used to arbitrarily cancel derivatives at the origin and at the cut-off.
28/// From the abstract of <https://doi.org/c5fr>:
29///
30/// _"Electrostatic pair-potentials within molecular simulations are often based on empirical data,
31/// cancellation of derivatives or moments, or statistical distributions of image-particles.
32/// In this work we start with the fundamental Poisson equation and show that no truncated Coulomb
33/// pair-potential, unsurprisingly, can solve the Poisson equation. For any such pair-potential
34/// the Poisson equation gives two incompatible constraints, yet we find a single unique expression
35/// which, pending two physically connected smoothness parameters, can obey either one of these.
36/// This expression has a general form which covers several recently published pair-potentials.
37/// For sufficiently large degree of smoothness we find that the solution implies a Gaussian
38/// distribution of the charge, a feature which is frequently assumed in pair-potential theory.
39/// We end up by recommending a single pair-potential based both on theoretical arguments and
40/// empirical evaluations of non-thermal lattice- and thermal water-systems.
41/// The same derivations have also been made for the screened Poisson equation,
42/// i.e. for Yukawa potentials, with a similar solution."_
43///
44/// The general short-range function is:
45/// $$
46/// S(q) = (1 - q)^{D + 1} \sum_{c = 0}^{C - 1} \frac{C - c}{C} \binom{D - 1 + c}{c} q^c
47/// $$
48///
49/// where $C$ is the number of cancelled derivatives at origin -2 (starting from the second derivative),
50/// and $D$ is the number of cancelled derivatives at the cut-off (starting from the zeroth derivative).
51///
52/// For infinite Debye-length, $\kappa=0$, the [`Poisson`] scheme captures several
53/// other truncation schemes by setting $C$ and $D$ according to this table:
54///
55/// | Type          | $C$ | $D$ | Reference / Comment
56/// |---------------|-----|-----|---------------------
57/// | `plain`       | 1   | -1  | Scheme for a vanilla coulomb interaction using the Poisson framework. Same as `Coulomb`.
58/// | `wolf`        | 1   | 0   | Scheme for [Undamped Wolf](https://doi.org/10.1063/1.478738)
59/// | `fennell`     | 1   | 1   | Scheme for [Levitt/undamped Fennell](https://doi.org/10/fp959p). See also doi:10/bqgmv2.
60/// | `kale`        | 1   | 2   | Scheme for [Kale](https://doi.org/10/csh8bg)
61/// | `mccann`      | 1   | 3   | Scheme for [McCann](https://doi.org/10.1021/ct300961)
62/// | `fukuda`      | 2   | 1   | Scheme for [Undamped Fukuda](https://doi.org/10.1063/1.3582791)
63/// | `markland`    | 2   | 2   | Scheme for [Markland](https://doi.org/10.1016/j.cplett.2008.09.019)
64/// | `stenqvist`   | 3   | 3   | Scheme for [Stenqvist](https://doi.org/10/c5fr)
65/// | `fanourgakis` | 4   | 3   | Scheme for [Fanourgakis](https://doi.org/10.1063/1.3216520),
66///
67/// Helper struct to store salt screening parameters
68#[derive(Debug, Clone, PartialEq)]
69#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
70struct Screening {
71    /// Inverse Debye screening length
72    #[cfg_attr(feature = "serde", serde(alias = "κ"))]
73    pub kappa: f64,
74    /// Reduced kappa = cutoff * kappa
75    pub reduced_kappa: f64,
76    pub reduced_kappa_squared: f64,
77    pub yukawa_denom: f64,
78}
79
80impl Screening {
81    fn new(debye_length: f64, cutoff: f64) -> Self {
82        let reduced_kappa = cutoff / debye_length;
83        Screening {
84            kappa: 1.0 / debye_length,
85            reduced_kappa,
86            reduced_kappa_squared: reduced_kappa.powi(2),
87            yukawa_denom: 1.0 / (1.0 - (2.0 * reduced_kappa).exp()),
88        }
89    }
90}
91
92#[derive(Debug, Clone, PartialEq)]
93#[cfg_attr(feature = "serde", derive(Serialize))]
94/// Generalized Poisson scheme for short-range electrostatic interactions.
95pub struct Poisson<const C: i32, const D: i32> {
96    /// Cutoff radius
97    cutoff: f64,
98    /// Debye length
99    #[cfg_attr(feature = "serde", serde(alias = "debyelength"))]
100    debye_length: Option<f64>,
101    /// Currently not in use
102    #[cfg_attr(feature = "serde", serde(skip))]
103    _has_dipolar_selfenergy: bool,
104    #[cfg_attr(feature = "serde", serde(skip))]
105    binom_cdc: f64,
106    #[cfg_attr(feature = "serde", serde(skip))]
107    screening: Option<Screening>,
108}
109
110#[cfg(feature = "serde")]
111impl<'de, const C: i32, const D: i32> Deserialize<'de> for Poisson<C, D> {
112    fn deserialize<DES>(deserializer: DES) -> Result<Self, DES::Error>
113    where
114        DES: Deserializer<'de>,
115    {
116        #[derive(Deserialize)]
117        #[serde(deny_unknown_fields)]
118        struct PoissonData {
119            cutoff: f64,
120            debye_length: Option<f64>,
121        }
122
123        let PoissonData {
124            cutoff,
125            debye_length,
126        } = PoissonData::deserialize(deserializer)?;
127        Ok(Poisson::new(cutoff, debye_length))
128    }
129}
130
131/// Scheme for a vanilla coulomb interaction using the Poisson framework. Same as `Coulomb`.
132pub type _Plain = Poisson<1, -1>;
133
134/// Energy and force shifted Yukawa potential [Levitt/undamped Fennell](https://doi.org/10/fp959p).
135///
136/// See also doi:10/bqgmv2.
137pub type Yukawa = Poisson<1, 1>;
138
139/// Scheme for [Undamped Wolf](https://doi.org/10.1063/1.478738)
140pub type UndampedWolf = Poisson<1, 0>;
141
142/// Scheme for [Kale](https://doi.org/10/csh8bg)
143pub type Kale = Poisson<1, 2>;
144
145/// Scheme for [McCann](https://doi.org/10.1021/ct300961)
146pub type McCann = Poisson<1, 3>;
147
148/// Scheme for [Undamped Fukuda](https://doi.org/10.1063/1.3582791)
149pub type UndampedFukuda = Poisson<2, 1>;
150
151/// Scheme for [Markland](https://doi.org/10.1016/j.cplett.2008.09.019)
152pub type Markland = Poisson<2, 2>;
153
154/// Scheme for [Stenqvist](https://doi.org/10/c5fr)
155pub type Stenqvist = Poisson<3, 3>;
156
157/// Scheme for [Fanourgakis](https://doi.org/10.1063/1.3216520)
158pub type Fanourgakis = Poisson<4, 3>;
159
160impl<const C: i32, const D: i32> Poisson<C, D> {
161    /// Create a new Poisson scheme with a given cutoff and optional Debye length.
162    pub fn new(cutoff: f64, debye_length: Option<f64>) -> Self {
163        if C < 1 {
164            panic!("`C` must be larger than zero");
165        }
166        if D < -1 && D != -C {
167            panic!("If `D` is less than negative one, then it has to equal negative `C`");
168        }
169        if D == 0 && C != 1 {
170            panic!("If `D` is zero, then `C` has to equal one ");
171        }
172
173        let _has_dipolar_selfenergy = C >= 2;
174
175        let screening = debye_length.map(|d| Screening::new(d, cutoff));
176
177        let binom_cdc = if screening.is_some() || D != -C {
178            f64::from(binomial(C + D, C) * D)
179        } else {
180            0.0
181        };
182
183        Self {
184            cutoff,
185            debye_length,
186            _has_dipolar_selfenergy,
187            binom_cdc,
188            screening,
189        }
190    }
191}
192
193impl<const C: i32, const D: i32> crate::Cutoff for Poisson<C, D> {
194    fn cutoff(&self) -> f64 {
195        self.cutoff
196    }
197}
198
199impl<const C: i32, const D: i32> crate::DebyeLength for Poisson<C, D> {
200    #[inline]
201    fn kappa(&self) -> Option<f64> {
202        self.screening.as_ref().map(|s| s.kappa)
203    }
204    fn set_debye_length(&mut self, debye_length: Option<f64>) -> crate::Result<()> {
205        self.screening = debye_length.map(|d| Screening::new(d, self.cutoff));
206        Ok(())
207    }
208}
209
210impl<const C: i32, const D: i32> ShortRangeFunction for Poisson<C, D> {
211    fn url() -> &'static str {
212        match (C, D) {
213            (1, -1) => "https://doi.org/msxd",             // plain
214            (1, 0) => "https://doi.org/10.1063/1.478738",  // wolf
215            (1, 1) => "https://doi.org/10/fp959p",         // fennell
216            (1, 2) => "https://doi.org/10/csh8bg",         // kale
217            (1, 3) => "https://doi.org/10.1021/ct300961",  // mccann
218            (2, 1) => "https://doi.org/10.1063/1.3582791", // fukuda
219            (2, 2) => "https://doi.org/dbpbts",            // markland
220            (3, 3) => "https://doi.org/10/c5fr",           // stenqvist
221            (4, 3) => "https://doi.org/10.1063/1.3216520", // fanourgakis
222            _ => "https://doi.org/c5fr",                   // generic poisson
223        }
224    }
225
226    fn short_range_f0(&self, q: f64) -> f64 {
227        // checks on C and D are evaluated at compile time
228        if D == -C {
229            return 1.0;
230        }
231        let qp: f64 = self.screening.as_ref().map_or(q, |s| {
232            (1.0 - (2.0 * s.reduced_kappa * q).exp()) * s.yukawa_denom
233        });
234
235        if D == 0 && C == 1 {
236            return 1.0 - qp;
237        }
238
239        // todo: could binomial coeffs be evaluated at compile time? E.g. with recursion.
240        let sum: f64 = (0..C)
241            .map(|c| (binomial(D - 1 + c, c) * (C - c)) as f64 / f64::from(C) * qp.powi(c))
242            .sum();
243        (1.0 - qp).powi(D + 1) * sum
244    }
245    fn short_range_f1(&self, q: f64) -> f64 {
246        if D == -C {
247            return 0.0;
248        }
249        if D == 0 && C == 1 {
250            return 0.0;
251        }
252        let (qp, dqpdq) = if let Some(s) = &self.screening {
253            let exp2kq = (2.0 * s.reduced_kappa * q).exp();
254            let qp = (1.0 - exp2kq) * s.yukawa_denom;
255            let dqpdq = -2.0 * s.reduced_kappa * exp2kq * s.yukawa_denom;
256            (qp, dqpdq)
257        } else {
258            (q, 1.0)
259        };
260        let mut sum1 = 1.0;
261        let mut sum2 = 0.0;
262
263        for c in 1..C {
264            let factor = (binomial(D - 1 + c, c) * (C - c)) as f64 / C as f64;
265            sum1 += factor * qp.powi(c);
266            sum2 += factor * c as f64 * qp.powi(c - 1);
267        }
268        let dsdqp = -f64::from(D + 1) * (1.0 - qp).powi(D) * sum1 + (1.0 - qp).powi(D + 1) * sum2;
269        dsdqp * dqpdq
270    }
271
272    fn short_range_f2(&self, q: f64) -> f64 {
273        if D == -C {
274            return 0.0;
275        }
276        if D == 0 && C == 1 {
277            return 0.0;
278        }
279
280        let (qp, dqpdq, d2qpdq2, dsdqp) = if let Some(s) = &self.screening {
281            let qp = (1.0 - (2.0 * s.reduced_kappa * q).exp()) * s.yukawa_denom;
282            let dqpdq = -2.0 * s.reduced_kappa * (2.0 * s.reduced_kappa * q).exp() * s.yukawa_denom;
283            let d2qpdq2 =
284                -4.0 * s.reduced_kappa_squared * (2.0 * s.reduced_kappa * q).exp() * s.yukawa_denom;
285            let mut tmp1 = 1.0;
286            let mut tmp2 = 0.0;
287            for c in 1..C {
288                let b = binomial(D - 1 + c, c) as f64 * (C - c) as f64;
289                tmp1 += b / C as f64 * qp.powi(c);
290                tmp2 += b * c as f64 / C as f64 * qp.powi(c - 1);
291            }
292            let dsdqp =
293                -f64::from(D + 1) * (1.0 - qp).powi(D) * tmp1 + (1.0 - qp).powi(D + 1) * tmp2;
294            (qp, dqpdq, d2qpdq2, dsdqp)
295        } else {
296            (q, 1.0, 0.0, 0.0)
297        };
298        let d2sdqp2 = self.binom_cdc * (1.0 - qp).powi(D - 1) * qp.powi(C - 1);
299        d2sdqp2 * dqpdq * dqpdq + dsdqp * d2qpdq2
300    }
301
302    fn short_range_f3(&self, q: f64) -> f64 {
303        if D == -C {
304            return 0.0;
305        }
306        if D == 0 && C == 1 {
307            return 0.0;
308        }
309
310        let (qp, dqpdq, d2qpdq2, d3qpdq3, d2sdqp2, dsdqp) = if let Some(s) = &self.screening {
311            let qp = (1.0 - (2.0 * s.reduced_kappa * q).exp()) * s.yukawa_denom;
312            let dqpdq = -2.0 * s.reduced_kappa * (2.0 * s.reduced_kappa * q).exp() * s.yukawa_denom;
313            let d2qpdq2 =
314                -4.0 * s.reduced_kappa_squared * (2.0 * s.reduced_kappa * q).exp() * s.yukawa_denom;
315            let d3qpdq3 = -8.0
316                * s.reduced_kappa_squared
317                * s.reduced_kappa
318                * (2.0 * s.reduced_kappa * q).exp()
319                * s.yukawa_denom;
320            let d2sdqp2 = self.binom_cdc * (1.0 - qp).powi(D - 1) * qp.powi(C - 1);
321            let mut tmp1 = 1.0;
322            let mut tmp2 = 0.0;
323            for c in 1..C {
324                tmp1 += (binomial(D - 1 + c, c) * (C - c)) as f64 / C as f64 * qp.powi(c);
325                tmp2 += (binomial(D - 1 + c, c) * (C - c)) as f64 / C as f64
326                    * c as f64
327                    * qp.powi(c - 1);
328            }
329            let dsdqp =
330                -f64::from(D + 1) * (1.0 - qp).powi(D) * tmp1 + (1.0 - qp).powi(D + 1) * tmp2;
331            (qp, dqpdq, d2qpdq2, d3qpdq3, d2sdqp2, dsdqp)
332        } else {
333            (q, 1.0, 0.0, 0.0, 0.0, 0.0)
334        };
335        let d3sdqp3 = self.binom_cdc
336            * (1.0 - qp).powi(D - 2)
337            * qp.powi(C - 2)
338            * ((2.0 - C as f64 - D as f64) * qp + C as f64 - 1.0);
339        d3sdqp3 * dqpdq * dqpdq * dqpdq + 3.0 * d2sdqp2 * dqpdq * d2qpdq2 + dsdqp * d3qpdq3
340    }
341
342    fn self_energy_prefactors(&self) -> SelfEnergyPrefactors {
343        let mut c1: f64 = -0.5 * (C + D) as f64 / C as f64;
344        if let Some(s) = &self.screening {
345            c1 = c1 * -2.0 * s.reduced_kappa * s.yukawa_denom;
346        }
347        SelfEnergyPrefactors {
348            monopole: Some(c1),
349            dipole: None,
350        }
351    }
352}
353
354impl<const C: i32, const D: i32> core::fmt::Display for Poisson<C, D> {
355    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
356        write!(
357            f,
358            "Poisson: 𝐶 = {}, 𝐷 = {}, 𝑟✂ = {:.1} Å",
359            C, D, self.cutoff
360        )?;
361        if let Some(debye_length) = self.kappa().map(f64::recip) {
362            write!(f, ", λᴰ = {:.1} Å", debye_length)?;
363        }
364        write!(f, " <{}>", Self::url())?;
365        Ok(())
366    }
367}
368
369#[test]
370fn test_poisson() {
371    use super::test_utils::{assert_vec3_eq, assert_vec_x_equals_norm, assert_vec_zero};
372    use crate::{
373        pairwise::{MultipoleEnergy, MultipoleField, MultipoleForce, MultipolePotential},
374        NalgebraMatrix3 as Matrix3, NalgebraVector3 as Vector3,
375    };
376    use approx::assert_relative_eq;
377    let cutoff = 29.0;
378    let pot = Stenqvist::new(cutoff, None);
379    let eps = 1e-9; // Set epsilon for approximate equality
380
381    // Test Stenqvist short-range function
382    assert_relative_eq!(pot.short_range_f0(0.5), 0.15625, epsilon = eps);
383    assert_relative_eq!(pot.short_range_f1(0.5), -1.0, epsilon = eps);
384    assert_relative_eq!(pot.short_range_f2(0.5), 3.75, epsilon = eps);
385    assert_relative_eq!(pot.short_range_f3(0.5), 0.0, epsilon = eps);
386    assert_relative_eq!(pot.short_range_f3(0.6), -5.76, epsilon = eps);
387    assert_relative_eq!(pot.short_range_f0(1.0), 0.0, epsilon = eps);
388    assert_relative_eq!(pot.short_range_f1(1.0), 0.0, epsilon = eps);
389    assert_relative_eq!(pot.short_range_f2(1.0), 0.0, epsilon = eps);
390    assert_relative_eq!(pot.short_range_f3(1.0), 0.0, epsilon = eps);
391    assert_relative_eq!(pot.short_range_f0(0.0), 1.0, epsilon = eps);
392    assert_relative_eq!(pot.short_range_f1(0.0), -2.0, epsilon = eps);
393    assert_relative_eq!(pot.short_range_f2(0.0), 0.0, epsilon = eps);
394    assert_relative_eq!(pot.short_range_f3(0.0), 0.0, epsilon = eps);
395
396    let pot = Stenqvist::new(cutoff, Some(23.0));
397    approx::assert_relative_eq!(
398        pot.self_energy(&[2.0], &[0.0]),
399        -0.03037721287,
400        epsilon = eps
401    );
402
403    assert_eq!(
404        pot.to_string(),
405        "Poisson: 𝐶 = 3, 𝐷 = 3, 𝑟✂ = 29.0 Å, λᴰ = 23.0 Å <https://doi.org/10/c5fr>"
406    );
407
408    // Test Fanougarkis short-range function
409    let pot = Fanourgakis::new(cutoff, None);
410    assert_relative_eq!(pot.short_range_f0(0.5), 0.19921875, epsilon = eps);
411    assert_relative_eq!(pot.short_range_f1(0.5), -1.1484375, epsilon = eps);
412    assert_relative_eq!(pot.short_range_f2(0.5), 3.28125, epsilon = eps);
413    assert_relative_eq!(pot.short_range_f3(0.5), 6.5625, epsilon = eps);
414
415    assert_eq!(
416        pot.to_string(),
417        "Poisson: 𝐶 = 4, 𝐷 = 3, 𝑟✂ = 29.0 Å <https://doi.org/10.1063/1.3216520>"
418    );
419
420    let pot = Poisson::<4, 3>::new(cutoff, None);
421    let z1 = 2.0;
422    let z2 = 3.0;
423    let r = Vector3::new(23.0, 0.0, 0.0); // distance vector
424    let rq = Vector3::new(
425        5.75 * 6.0_f64.sqrt(),
426        5.75 * 2.0_f64.sqrt(),
427        11.5 * 2.0_f64.sqrt(),
428    );
429    let rh = r.normalize();
430    let mu1 = Vector3::new(19.0, 7.0, 11.0);
431    let mu2 = Vector3::new(13.0, 17.0, 5.0);
432    //      [3 7 8]
433    //  Q = [5 9 6]
434    //      [2 1 4]
435    let quad1 = Matrix3::from_row_slice(&[3.0, 7.0, 8.0, 5.0, 9.0, 6.0, 2.0, 1.0, 4.0]);
436    let quad2 = Matrix3::zeros();
437    assert_relative_eq!(quad1.trace(), 16.0, epsilon = eps);
438
439    // Test potentials
440    assert_relative_eq!(pot.ion_potential(z1, cutoff), 0.0, epsilon = eps);
441    assert_relative_eq!(
442        pot.ion_potential(z1, r.norm()),
443        0.0009430652121,
444        epsilon = eps
445    );
446    assert_relative_eq!(
447        pot.dipole_potential(mu1, rh.scale(cutoff)),
448        0.0,
449        epsilon = eps
450    );
451    assert_relative_eq!(pot.dipole_potential(mu1, r), 0.005750206554, epsilon = eps);
452    assert_relative_eq!(
453        pot.quadrupole_potential(quad1, rq),
454        0.000899228165,
455        epsilon = eps
456    );
457    assert_relative_eq!(
458        pot.quadrupole_potential(quad1, rq.scale(cutoff / 23.0)),
459        0.0,
460        epsilon = eps
461    );
462
463    // Test fields with no salt
464    assert_vec_zero!(pot.ion_field(z1, rh.scale(cutoff)), eps);
465    assert_vec_x_equals_norm!(pot.ion_field(z1, r), 0.0006052849004, eps);
466    assert_vec_zero!(pot.dipole_field(mu1, rh.scale(cutoff)), eps);
467
468    let ion_field_scalar = pot.ion_field_scalar(z1, r.norm());
469    let e_ion: Vector3 = pot.ion_field(z1, r).into();
470    assert_relative_eq!(ion_field_scalar, e_ion.norm(), epsilon = eps);
471
472    assert_vec3_eq!(
473        pot.dipole_field(mu1, r),
474        [0.002702513754, -0.00009210857180, -0.0001447420414],
475        eps
476    );
477    assert_vec3_eq!(
478        pot.quadrupole_field(quad1, r),
479        [0.00001919309993, -0.00004053806958, -0.00003378172465],
480        eps
481    );
482
483    // Test energies
484    assert_relative_eq!(pot.ion_ion_energy(z1, z2, cutoff), 0.0, epsilon = eps);
485    assert_relative_eq!(
486        pot.ion_ion_energy(z1, z2, r.norm()),
487        0.002829195636,
488        epsilon = eps
489    );
490    assert_relative_eq!(
491        pot.ion_dipole_energy(z1, mu2, rh.scale(cutoff)),
492        0.0,
493        epsilon = eps
494    );
495    assert_relative_eq!(
496        pot.ion_dipole_energy(z1, mu2, r),
497        -0.007868703705,
498        epsilon = eps
499    );
500    assert_relative_eq!(
501        pot.ion_dipole_energy(z2, mu1, -r),
502        0.01725061966,
503        epsilon = eps
504    );
505    assert_relative_eq!(
506        pot.dipole_dipole_energy(mu1, mu2, rh.scale(cutoff)),
507        0.0,
508        epsilon = eps
509    );
510    assert_relative_eq!(
511        pot.dipole_dipole_energy(mu1, mu2, r),
512        -0.03284312288,
513        epsilon = eps
514    );
515    assert_relative_eq!(
516        pot.ion_quadrupole_energy(z2, quad1, rq.scale(cutoff / 23.0 * 29.0)),
517        0.0,
518        epsilon = eps
519    );
520    assert_relative_eq!(
521        pot.ion_quadrupole_energy(z2, quad1, rq),
522        0.002697684495,
523        epsilon = eps
524    );
525    assert_relative_eq!(
526        pot.ion_quadrupole_energy(z1, quad2, -rq.scale(cutoff / 23.0 * 29.0)),
527        0.0,
528        epsilon = eps
529    );
530    assert_relative_eq!(
531        pot.ion_quadrupole_energy(z1, quad2, -rq),
532        0.0,
533        epsilon = eps
534    );
535
536    // Test forces
537    assert_vec_zero!(
538        pot.ion_ion_force(z1, z2, Vector3::new(cutoff, 0.0, 0.0)),
539        eps
540    );
541    assert_vec_x_equals_norm!(pot.ion_ion_force(z1, z2, r), 0.001815854701, eps);
542    assert_vec_zero!(pot.ion_dipole_force(z2, mu1, rh.scale(cutoff)), eps);
543    assert_vec3_eq!(
544        pot.ion_dipole_force(z2, mu1, r),
545        [0.008107541263, -0.0002763257154, -0.0004342261242],
546        eps
547    );
548    assert_vec3_eq!(
549        pot.ion_dipole_force(z1, mu2, -r),
550        [0.003698176716, -0.0004473844916, -0.0001315836740],
551        eps
552    );
553    assert_vec_zero!(pot.dipole_dipole_force(mu1, mu2, rh.scale(cutoff)), eps);
554    assert_vec3_eq!(
555        pot.dipole_dipole_force(mu1, mu2, r),
556        [0.009216400961, -0.002797126801, -0.001608010094],
557        eps
558    );
559
560    // Test with screening
561    let debye_length = 23.0;
562    let pot = Poisson::<3, 3>::new(cutoff, Some(debye_length));
563
564    // Test short-ranged function with screening
565    assert_relative_eq!(pot.short_range_f0(0.5), 0.5673222086324718, epsilon = eps);
566    assert_relative_eq!(pot.short_range_f1(0.5), -1.4373727619264975, epsilon = eps);
567    assert_relative_eq!(pot.short_range_f2(0.5), -2.552012309527445, epsilon = eps);
568    assert_relative_eq!(pot.short_range_f3(0.5), 4.384434366606605, epsilon = eps);
569
570    // Test potentials with screening
571    assert_relative_eq!(pot.ion_potential(z1, cutoff), 0.0, epsilon = eps);
572    assert_relative_eq!(
573        pot.ion_potential(z1, r.norm()),
574        0.003344219306,
575        epsilon = eps
576    );
577    assert_relative_eq!(
578        pot.dipole_potential(mu1, rh.scale(cutoff)),
579        0.0,
580        epsilon = eps
581    );
582    assert_relative_eq!(pot.dipole_potential(mu1, r), 0.01614089171, epsilon = eps);
583    assert_relative_eq!(
584        pot.quadrupole_potential(quad1, rq),
585        0.0016294707475,
586        epsilon = eps
587    );
588    assert_relative_eq!(
589        pot.quadrupole_potential(quad1, rq.scale(cutoff / 23.0 * 29.0)),
590        0.0,
591        epsilon = eps
592    );
593
594    // Test fields with screening
595    assert_vec_zero!(pot.ion_field(z1, r.scale(cutoff)), eps);
596    assert_vec_x_equals_norm!(pot.ion_field(z1, r), 0.001699041230, eps);
597    assert_vec_zero!(pot.dipole_field(mu1, r.scale(cutoff)), eps);
598
599    let ion_field_scalar = pot.ion_field_scalar(z1, r.norm());
600    let ion_field: Vector3 = pot.ion_field(z1, r).into();
601    assert_relative_eq!(ion_field_scalar, ion_field.norm(), epsilon = eps);
602
603    assert_vec3_eq!(
604        pot.dipole_field(mu1, r),
605        [0.004956265485, -0.0002585497523, -0.0004062924688],
606        eps
607    );
608    assert_vec3_eq!(
609        pot.quadrupole_field(quad1, r),
610        [-0.00005233355205, -0.00007768480608, -0.00006473733856],
611        eps
612    );
613
614    // Test energies with screening
615    assert_relative_eq!(pot.ion_ion_energy(z1, z2, cutoff), 0.0, epsilon = eps);
616    assert_relative_eq!(
617        pot.ion_ion_energy(z1, z2, r.norm()),
618        0.01003265793,
619        epsilon = eps
620    );
621    assert_relative_eq!(
622        pot.ion_dipole_energy(z1, mu2, r.scale(cutoff)),
623        0.0,
624        epsilon = eps
625    );
626    assert_relative_eq!(
627        pot.ion_dipole_energy(z1, mu2, r),
628        -0.02208753604,
629        epsilon = eps
630    );
631    assert_relative_eq!(
632        pot.ion_dipole_energy(z2, mu1, -r),
633        0.04842267505,
634        epsilon = eps
635    );
636    assert_relative_eq!(
637        pot.dipole_dipole_energy(mu1, mu2, r.scale(cutoff)),
638        0.0,
639        epsilon = eps
640    );
641    assert_relative_eq!(
642        pot.dipole_dipole_energy(mu1, mu2, r),
643        -0.05800464321,
644        epsilon = eps
645    );
646    assert_relative_eq!(
647        pot.ion_quadrupole_energy(z2, quad1, rq.scale(cutoff / 23.0 * 29.0)),
648        0.0,
649        epsilon = eps
650    );
651    assert_relative_eq!(
652        pot.ion_quadrupole_energy(z2, quad1, rq),
653        0.004888412229,
654        epsilon = eps
655    );
656
657    // Test forces with screening
658    assert_vec_zero!(pot.ion_ion_force(z1, z2, r.scale(cutoff)), eps);
659    assert_vec_x_equals_norm!(pot.ion_ion_force(z1, z2, r), 0.005097123689, eps);
660    assert_vec_zero!(pot.ion_dipole_force(z2, mu1, r.scale(cutoff)), eps);
661    assert_vec3_eq!(
662        pot.ion_dipole_force(z2, mu1, r),
663        [0.01486879646, -0.0007756492577, -0.001218877402],
664        eps
665    );
666    assert_vec3_eq!(
667        pot.ion_dipole_force(z1, mu2, -r),
668        [0.006782258035, -0.001255813082, -0.0003693567885],
669        eps
670    );
671    assert_vec_zero!(pot.dipole_dipole_force(mu1, mu2, r.scale(cutoff)), eps);
672    assert_vec3_eq!(
673        pot.dipole_dipole_force(mu1, mu2, r),
674        [0.002987655323, -0.005360251624, -0.003081497314],
675        eps
676    );
677}