Skip to main content

coulomb/pairwise/schemes/
plain.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::{pairwise::ShortRangeFunction, DebyeLength};
19
20#[cfg(feature = "serde")]
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22
23/// Scheme for vanilla Coulomb interactions, $S(q)=1$.
24///
25/// See _Premier mémoire sur l’électricité et le magnétisme_ by Charles-Augustin de Coulomb,
26/// <https://doi.org/msxd>.
27#[derive(Clone, Debug, PartialEq)]
28#[cfg_attr(
29    feature = "serde",
30    derive(Serialize, Deserialize),
31    serde(deny_unknown_fields)
32)]
33pub struct Plain {
34    /// Cut-off distance
35    cutoff: f64,
36    /// Optional inverse Debye length
37    #[cfg_attr(
38        feature = "serde",
39        serde(
40            rename = "debye",
41            alias = "debye_length",
42            alias = "debyelength",
43            serialize_with = "serialize_reciprocal",
44            deserialize_with = "deserialize_reciprocal",
45            default
46        )
47    )]
48    kappa: Option<f64>,
49}
50
51/// Convert from kappa to debye when serializing.
52#[cfg(feature = "serde")]
53fn serialize_reciprocal<S>(x: &Option<f64>, s: S) -> Result<S::Ok, S::Error>
54where
55    S: Serializer,
56{
57    match x.as_ref() {
58        Some(&value) => s.serialize_some(&f64::recip(value)),
59        None => s.serialize_none(),
60    }
61}
62
63/// Convert from debye to kappa when serializing.
64#[cfg(feature = "serde")]
65fn deserialize_reciprocal<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
66where
67    D: Deserializer<'de>,
68{
69    Ok(Option::deserialize(deserializer)?.map(f64::recip))
70}
71
72impl Default for Plain {
73    /// The default is infinite cutoff radius and no screening
74    fn default() -> Self {
75        Self {
76            cutoff: f64::INFINITY,
77            kappa: None,
78        }
79    }
80}
81
82impl core::fmt::Display for Plain {
83    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84        write!(f, "Plain Coulomb: 𝑟✂ = {:.1}", self.cutoff)?;
85        if let Some(debye_length) = self.kappa.map(f64::recip) {
86            write!(
87                f,
88                ", λᴰ = {:.1}, λᴰ/𝑟✂ = {:.1e}",
89                debye_length,
90                debye_length / self.cutoff
91            )?;
92        }
93        write!(f, " <{}>", Self::url())?;
94        Ok(())
95    }
96}
97
98impl Plain {
99    /// Create a plain Coulomb potential without a cutoff distance.
100    pub fn without_cutoff() -> Self {
101        Self::new(f64::INFINITY, None)
102    }
103    /// Create a new plain Coulomb scheme with a given cutoff and optional Debye length.
104    pub fn new(cutoff: f64, debye_length: Option<f64>) -> Self {
105        Self {
106            cutoff,
107            kappa: debye_length.map(f64::recip),
108        }
109    }
110    /// Create a new plain Coulomb scheme with a given cutoff and no salt screening.
111    pub fn new_without_salt(cutoff: f64) -> Self {
112        Self::new(cutoff, None)
113    }
114
115    /// Create from medium. The Debye length is calculated from the medium's properties in units
116    /// of angstrom. `cutoff` should be provided in angstrom.
117    pub fn from_medium(cutoff: f64, medium: &crate::Medium) -> Self {
118        Self::new(cutoff, medium.debye_length())
119    }
120}
121
122impl crate::Cutoff for Plain {
123    #[inline]
124    fn cutoff(&self) -> f64 {
125        self.cutoff
126    }
127}
128
129impl DebyeLength for Plain {
130    #[inline]
131    fn kappa(&self) -> Option<f64> {
132        self.kappa
133    }
134    fn set_debye_length(&mut self, debye_length: Option<f64>) -> crate::Result<()> {
135        self.kappa = debye_length.map(f64::recip);
136        Ok(())
137    }
138}
139
140impl ShortRangeFunction for Plain {
141    fn url() -> &'static str {
142        "https://doi.org/msxd"
143    }
144
145    #[inline]
146    fn short_range_f0(&self, _q: f64) -> f64 {
147        1.0
148    }
149    #[inline]
150    fn short_range_f1(&self, _q: f64) -> f64 {
151        0.0
152    }
153    #[inline]
154    fn short_range_f2(&self, _q: f64) -> f64 {
155        0.0
156    }
157    #[inline]
158    fn short_range_f3(&self, _q: f64) -> f64 {
159        0.0
160    }
161}
162
163#[test]
164fn test_coulomb() {
165    use super::test_utils::{assert_vec3_eq, assert_vec_x_equals_norm, assert_vec_zero};
166    use crate::{NalgebraMatrix3 as Matrix3, NalgebraVector3 as Vector3};
167    use approx::assert_relative_eq;
168
169    use crate::pairwise::{MultipoleEnergy, MultipoleField, MultipoleForce, MultipolePotential};
170    let cutoff: f64 = 29.0;
171    let z1 = 2.0;
172    let z2 = 3.0;
173    let mu1 = Vector3::new(19.0, 7.0, 11.0);
174    let mu2 = Vector3::new(13.0, 17.0, 5.0);
175    let quad1 = Matrix3::new(3.0, 7.0, 8.0, 5.0, 9.0, 6.0, 2.0, 1.0, 4.0);
176    let _quad2 = Matrix3::zeros();
177    let r = Vector3::new(23.0, 0.0, 0.0);
178    let rq = Vector3::new(
179        5.75 * (6.0f64).sqrt(),
180        5.75 * (2.0f64).sqrt(),
181        11.5 * (2.0f64).sqrt(),
182    );
183    let rh = Vector3::new(1.0, 0.0, 0.0);
184
185    let pot = Plain::new(cutoff, None);
186    let eps = 1e-9;
187
188    assert_eq!(
189        pot.to_string(),
190        "Plain Coulomb: 𝑟✂ = 29.0 <https://doi.org/msxd>"
191    );
192
193    // Test short-ranged function
194    assert_eq!(pot.short_range_f0(0.5), 1.0);
195    assert_eq!(pot.short_range_f1(0.5), 0.0);
196    assert_eq!(pot.short_range_f2(0.5), 0.0);
197    assert_eq!(pot.short_range_f3(0.5), 0.0);
198
199    // Test potentials
200    assert_eq!(pot.ion_potential(z1, cutoff + 1.0), 0.0);
201    assert_relative_eq!(
202        pot.ion_potential(z1, r.norm()),
203        0.08695652173913043,
204        epsilon = eps
205    );
206    assert_relative_eq!(
207        pot.dipole_potential(mu1, (cutoff + 1.0) * rh),
208        0.0,
209        epsilon = eps
210    );
211    assert_relative_eq!(
212        pot.dipole_potential(mu1, r),
213        0.035916824196597356,
214        epsilon = eps
215    );
216    assert_relative_eq!(
217        pot.quadrupole_potential(quad1, rq),
218        0.00093632817,
219        epsilon = eps
220    );
221
222    // Test fields
223    assert_vec_zero!(pot.ion_field(z1, (cutoff + 1.0) * rh), eps);
224    assert_vec_x_equals_norm!(pot.ion_field(z1, r), 0.003780718336, eps);
225    assert_vec_zero!(pot.dipole_field(mu1, (cutoff + 1.0) * rh), eps);
226    assert_vec3_eq!(
227        pot.dipole_field(mu1, r),
228        [0.003123202104, -0.0005753267034, -0.0009040848196],
229        eps
230    );
231    assert_vec3_eq!(
232        pot.quadrupole_field(quad1, r),
233        [-0.00003752130674, -0.00006432224013, -0.00005360186677],
234        eps
235    );
236
237    // Test energies
238    assert_relative_eq!(pot.ion_ion_energy(z1, z2, cutoff + 1.0), 0.0, epsilon = eps);
239    assert_relative_eq!(
240        pot.ion_ion_energy(z1, z2, r.norm()),
241        z1 * z2 / r.norm(),
242        epsilon = eps
243    );
244    assert_relative_eq!(
245        pot.ion_dipole_energy(z1, mu2, (cutoff + 1.0) * rh),
246        -0.0,
247        epsilon = eps
248    );
249    assert_relative_eq!(
250        pot.ion_dipole_energy(z1, mu2, r),
251        -0.04914933837,
252        epsilon = eps
253    );
254    assert_relative_eq!(
255        pot.dipole_dipole_energy(mu1, mu2, (cutoff + 1.0) * rh),
256        -0.0,
257        epsilon = eps
258    );
259    assert_relative_eq!(
260        pot.dipole_dipole_energy(mu1, mu2, r),
261        -0.02630064930,
262        epsilon = eps
263    );
264    assert_relative_eq!(
265        pot.ion_quadrupole_energy(z2, quad1, rq),
266        0.002808984511,
267        epsilon = eps
268    );
269
270    // Test forces
271    assert_vec_zero!(pot.ion_ion_force(z1, z2, (cutoff + 1.0) * rh), eps);
272    assert_vec_x_equals_norm!(pot.ion_ion_force(z1, z2, r), 0.01134215501, eps);
273    assert_vec_zero!(pot.ion_dipole_force(z2, mu1, (cutoff + 1.0) * rh), eps);
274    assert_vec3_eq!(
275        pot.ion_dipole_force(z2, mu1, r),
276        [0.009369606312, -0.001725980110, -0.002712254459],
277        eps
278    );
279    assert_vec_zero!(pot.dipole_dipole_force(mu1, mu2, (cutoff + 1.0) * rh), eps);
280    assert_vec3_eq!(
281        pot.dipole_dipole_force(mu1, mu2, r),
282        [0.003430519474, -0.004438234569, -0.002551448858],
283        eps
284    );
285
286    // Now test with a non-zero kappa
287    let pot = Plain::new(cutoff, Some(23.0));
288
289    assert_eq!(
290        pot.to_string(),
291        "Plain Coulomb: 𝑟✂ = 29.0, λᴰ = 23.0, λᴰ/𝑟✂ = 7.9e-1 <https://doi.org/msxd>"
292    );
293
294    assert_relative_eq!(pot.ion_potential(z1, cutoff + 1.0), 0.0, epsilon = eps);
295    assert_relative_eq!(
296        pot.ion_potential(z1, r.norm()),
297        0.03198951663,
298        epsilon = eps
299    );
300    assert_relative_eq!(
301        pot.dipole_potential(mu1, (cutoff + 1.0) * rh),
302        0.0,
303        epsilon = eps
304    );
305    assert_relative_eq!(pot.dipole_potential(mu1, r), 0.02642612243, epsilon = eps);
306
307    // Test fields
308    assert_vec_zero!(pot.ion_field(z1, (cutoff + 1.0) * rh), eps);
309    assert_vec_x_equals_norm!(pot.ion_field(z1, r), 0.002781697098, eps);
310    assert_vec_zero!(pot.dipole_field(mu1, (cutoff + 1.0) * rh), eps);
311
312    let field_scalar = pot.ion_field_scalar(z1, r.norm());
313    let field: Vector3 = pot.ion_field(z1, r).into();
314    assert_relative_eq!(field_scalar, field.norm(), epsilon = eps);
315
316    assert_vec3_eq!(
317        pot.dipole_field(mu1, r),
318        [0.002872404612, -0.0004233017324, -0.0006651884364],
319        eps
320    );
321
322    // Test ion-induced dipole energy
323    let pot = Plain::new(cutoff, None);
324    let alpha = 50.0;
325    let charge = 1.0;
326    let r_vec = Vector3::new(5.0, 0.0, 0.0);
327    let energy = pot.ion_induced_dipole_energy(charge, alpha, r_vec);
328    assert_relative_eq!(energy, -0.04, epsilon = eps);
329
330    // Manually calculate the energy
331    let r4 = r_vec.norm_squared().powi(2);
332    assert_relative_eq!(energy, -0.5 * charge * charge * alpha / r4, epsilon = eps);
333
334    // with non-zero kappa
335    let pot = Plain::new(cutoff, Some(10.0));
336    let energy = pot.ion_induced_dipole_energy(charge, alpha, r_vec);
337    assert_relative_eq!(energy, -0.0331091497054298, epsilon = eps);
338}
339
340#[cfg(feature = "uom")]
341#[test]
342fn test_plain_si() {
343    use crate::{
344        pairwise::{MultipoleEnergySI, MultipoleFieldSI, MultipolePotentialSI},
345        units::*,
346    };
347    use approx::assert_relative_eq;
348    let eps = 1e-9; // Set epsilon for approximate equality
349    let pot = Plain::without_cutoff();
350    let z1 = ElectricCharge::new::<elementary_charge>(2.0);
351    let z2 = ElectricCharge::new::<elementary_charge>(3.0);
352    let r = Length::new::<nanometer>(2.3);
353    let energy = pot.ion_ion_energy(z1, z2, r);
354    assert_relative_eq!(
355        energy.get::<kilojoule_per_mole>(),
356        362.4403242896922,
357        epsilon = eps
358    );
359    let potential = pot.ion_potential(z1, r);
360    assert_relative_eq!(potential.get::<volt>(), 1.2521430850804929, epsilon = eps);
361
362    let field = pot.ion_field(z1, r);
363    assert_relative_eq!(
364        field.get::<volt_per_micrometer>(),
365        544.4100369915187,
366        epsilon = eps
367    );
368}