Skip to main content

moldyn_core/forces/
newton.rs

1//! This module contains the [NewtonForce] struct, which implements the
2//! [Force] trait according to Newton's law of universal gravitation.
3
4use crate::{Particle, forces::Force};
5
6/// A struct representing a Newton (or Coloumb-like) force, which implements
7/// the [Force] trait.
8pub struct NewtonForce {
9    factor: f64,
10}
11
12#[cfg(test)]
13impl NewtonForce {
14    /// The constructor of [NewtonForce]. The parameters are set to make
15    /// tests forward-compatible (not relying on the [Default] implementation).
16    fn with_gravity_factor(factor: f64) -> Self {
17        Self { factor }
18    }
19}
20
21impl Force for NewtonForce {
22    fn system_name(&self) -> &str {
23        "newton"
24    }
25
26    /// Calculates the potential energy between two particles according to Newton's
27    /// law of universal gravitation.
28    /// 
29    /// ```text
30    /// potential = -G * M / r
31    /// potential = -M / r          (assuming G = 1)
32    /// ```
33    fn potential(&self, particle: &Particle, other: &Particle) -> f64 {
34        let distance = Particle::distance(particle, other);
35
36        if distance == 0.0 {
37            0.0
38        } else {
39            let mul_mass = Particle::mass_product(particle, other);
40            -self.factor * mul_mass / distance
41        }
42
43        // TODO consider using the following code instead of above (after fixing paraview bugs)
44
45        // let distance = Particle::distance(particle, other);
46
47        // // match distance {
48        // //     0.0 => 0.0,
49        // //     _ => {
50        // //         let mul_mass = Particle::mass_product(particle, other);
51        // //         -self.factor * mul_mass / distance
52        // //     }
53        // // }
54
55        // if distance == 0.0 {
56        //     0.0
57        // } else {
58        //     let mul_mass = Particle::mass_product(particle, other);
59        //     -self.factor * mul_mass / distance
60        // }
61    }
62}
63
64impl Default for NewtonForce {
65    /// The default instance of [NewtonForce]. The parameters are set
66    /// to the following.
67    ///
68    /// | Parameter | Value |
69    /// | --- | --- |
70    /// | `factor` | `1.0` |
71    fn default() -> Self {
72        Self { factor: 1.0 }
73    }
74}
75
76#[cfg(test)]
77mod test {
78    use crate::{Force, NewtonForce, Particle, Vec3};
79
80    /// This test validates that newton is attractive. ( ͡° ͜ʖ ͡°)
81    #[test]
82    fn newton_is_attractive() {
83        let p1 = Particle::default().with_mass(1.0);
84        let p2 = Particle::at(1.0, 0.0, 0.0).with_mass(1.0);
85
86        let force_on_p1 = NewtonForce::default().force(&p1, &p2);
87        let force_on_p2 = NewtonForce::default().force(&p2, &p1);
88
89        assert!(force_on_p1.x > 0.0, "force on p1 should be attractive");
90        assert!(force_on_p2.x < 0.0, "force on p2 should be attractive");
91    }
92
93    /// This test validates that newton's third law holds: actio = reactio.
94    #[test]
95    fn newtons_third_law() {
96        let p1 = Particle::default().with_mass(1.0);
97        let p2 = Particle::at(1.0, 0.0, 0.0).with_mass(1.0);
98
99        let force_on_p1 = NewtonForce::default().force(&p1, &p2);
100        let force_on_p2 = NewtonForce::default().force(&p2, &p1);
101
102        assert_eq!(force_on_p1, -force_on_p2, "newton's third law should hold");
103    }
104
105    /// This test validates that forces occur on a single line (dimensional
106    /// correctness)
107    #[test]
108    fn may_the_force_be_one_dimensional() {
109        let p1 = Particle::default().with_mass(1.0);
110        let p2 = Particle::at(1.0, 0.0, 0.0).with_mass(1.0);
111
112        let force = NewtonForce::default().force(&p1, &p2);
113
114        assert_eq!(force.y, 0.0, "force on p1 should be zero along y-axis only");
115        assert_eq!(force.z, 0.0, "force on p1 should be zero along z-axis only");
116    }
117
118    /// This test validates the correctness of the potential energy calculation.
119    /// 
120    /// In this test, the distance is strictly `1.0` and the masses are `1.0`.
121    /// Therefore, the potential energy should be `-1.0`.
122    /// 
123    /// # Equation
124    /// 
125    /// ```text
126    /// U = -G *         M / r
127    ///   = -1 * 1.0 * 1.0 / 1.0
128    ///   = -1.0
129    /// ```
130    #[test]
131    fn potential_energy_validation1() {
132        let p1pos = Vec3::new(0.0, 0.0, 0.0);
133        let p2pos = Vec3::new(1.0, 0.0, 0.0);
134
135        let p1m = 1.0;
136        let p2m = 1.0;
137
138        let newton = NewtonForce::with_gravity_factor(1.0);
139
140        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
141        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
142
143        let potential = newton.potential(&p1, &p2);
144
145        assert_eq!(potential, -1.0, "potential energy should be -1.0");
146    }
147
148    /// This test validates the correctness of the potential energy calculation.
149    /// 
150    /// In this test, the distance is strictly `1.0` and the masses are respectively
151    /// `1.0` and `4.0`. Since the product of masses is in the numerator, the potential
152    /// energy should scale by the factor `4`, yielding the potential energy `-4.0`.
153    /// 
154    /// # Equation
155    /// 
156    /// ```text
157    /// U = -G *         M / r
158    ///   = -1 * 1.0 * 4.0 / 1.0
159    ///   = -4.0
160    /// ```
161    #[test]
162    fn potential_energy_validation2() {
163        let p1pos = Vec3::new(0.0, 0.0, 0.0);
164        let p2pos = Vec3::new(1.0, 0.0, 0.0);
165
166        let p1m = 1.0;
167        let p2m = 4.0;
168
169        let newton = NewtonForce::with_gravity_factor(1.0);
170
171        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
172        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
173
174        let potential = newton.potential(&p1, &p2);
175
176        assert_eq!(potential, -4.0, "potential energy should be -4.0");
177    }
178
179    /// This test validates the correctness of the potential energy calculation.
180    /// 
181    /// In this test, the distance is strictly `1.0` and the masses are `4.0`.
182    /// Since the product of masses is in the numerator, the potential energy
183    /// should scale by the factor squared `16`, yielding the potential energy
184    /// `-16.0`.
185    /// 
186    /// # Equation
187    /// 
188    /// ```text
189    /// U = -G *         M / r
190    ///   = -1 * 4.0 * 4.0 / 1.0
191    ///   = -16.0
192    /// ```
193    #[test]
194    fn potential_energy_validation3() {
195        let p1pos = Vec3::new(0.0, 0.0, 0.0);
196        let p2pos = Vec3::new(1.0, 0.0, 0.0);
197
198        let p1m = 4.0;
199        let p2m = 4.0;
200
201        let newton = NewtonForce::with_gravity_factor(1.0);
202
203        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
204        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
205
206        let potential = newton.potential(&p1, &p2);
207
208        assert_eq!(potential, -16.0, "potential energy should be -16.0");
209    }
210
211    /// This test validates the correctness of the potential energy calculation. This
212    /// test depends on [potential_energy_validation2] and [potential_energy_validation3],
213    /// which validate the masses being in the numerator.
214    /// 
215    /// In this test, the distance is strictly `2.0` and the masses are `1.0`.
216    /// Since the masses do not affect the equation as covered by other tests, the
217    /// potential energy should scale by in the inverse factor, yielding `-0.5`.
218    /// 
219    /// # Equation
220    /// 
221    /// ```text
222    /// U = -G *         M / r
223    ///   = -1 * 1.0 * 1.0 / 2.0
224    ///   = -0.5
225    /// ```
226    /// 
227    /// This test validates the correctness of the potential energy calculation.
228    /// In this test, the distance is strictly 2.0 and the masses are strictly 1.0.
229    /// Since the tests [potential_energy_validation2] and [potential_energy_validation3]
230    /// validate the masses being in the numerator, we expect the numerator to be -1.0.
231    /// 
232    /// Since the distance is in the denominator, we expect the potential energy to be -0.5.
233    #[test]
234    fn potential_energy_validation4() {
235        let p1pos = Vec3::new(0.0, 0.0, 0.0);
236        let p2pos = Vec3::new(2.0, 0.0, 0.0);
237
238        let p1m = 1.0;
239        let p2m = 1.0;
240
241        let newton = NewtonForce::with_gravity_factor(1.0);
242
243        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
244        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
245
246        let potential = newton.potential(&p1, &p2);
247
248        assert_eq!(potential, -0.5, "potential energy should be -0.5");
249    }
250
251    /// This test validates the correctness of the potential energy calculation
252    /// in a more complex scenario.
253    /// 
254    /// # Equation
255    /// 
256    /// ```text
257    /// U = -G *         M / r
258    ///   = -1 * 5.0 * 2.0 / 10.0
259    ///   = -1.0
260    /// ```
261    #[test]
262    fn potential_energy_validation_complex() {
263        let p1pos = Vec3::new(0.0, 0.0, 0.0);
264        let p2pos = Vec3::new(10.0, 0.0, 0.0);
265
266        let p1m = 5.0;
267        let p2m = 2.0;
268
269        let newton = NewtonForce::with_gravity_factor(1.0);
270
271        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
272        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
273
274        let potential = newton.potential(&p1, &p2);
275
276        assert_eq!(potential, -1.0, "potential energy should be -1.0");
277    }
278
279    /// This test validates the correctness of the force calculation. The
280    /// formula is taken from the article [Wikipedia - Newton's law of universal gravitation](https://en.wikipedia.org/wiki/Newton%27s_law_of_universal_gravitation)
281    /// 
282    /// In this test, the distance and the masses are strictly `1.0`.
283    /// 
284    /// # Equation
285    /// 
286    /// ```text
287    /// U = -G *         M / r
288    ///   = -1 * 1.0 * 1.0 / 1.0
289    ///   = -1.0
290    /// 
291    /// F =  -U / r
292    ///   = 1.0 / 1.0
293    /// ```
294    #[test]
295    fn force_validation1() {
296        let p1pos = Vec3::new(0.0, 0.0, 0.0);
297        let p2pos = Vec3::new(1.0, 0.0, 0.0);
298
299        let p1m = 1.0;
300        let p2m = 1.0;
301
302        let newton = NewtonForce::with_gravity_factor(1.0);
303
304        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
305        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
306
307        let force = newton.force(&p1, &p2);
308
309        assert_eq!(force.length(), 1.0, "force length should be 1.0");
310    }
311
312    /// This test validates the correctness of the force calculation.
313    /// 
314    /// In this test, the distance is strictly `1.0` and one of the bodies has
315    /// the mass `4.0`.
316    /// 
317    /// # Equation
318    /// 
319    /// ```text
320    /// U = -G *         M / r
321    ///   = -1 * 1.0 * 4.0 / 1.0
322    ///   = -4.0
323    /// 
324    /// F =  -U / r
325    ///   = 4.0 / 1.0
326    /// ```
327    #[test]
328    fn force_validation2() {
329        let p1pos = Vec3::new(0.0, 0.0, 0.0);
330        let p2pos = Vec3::new(1.0, 0.0, 0.0);
331
332        let p1m = 1.0;
333        let p2m = 4.0;
334
335        let newton = NewtonForce::with_gravity_factor(1.0);
336
337        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
338        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
339
340        let force = newton.force(&p1, &p2);
341
342        assert_eq!(force.length(), 4.0, "force should be 4.0");
343    }
344
345    /// This test validates the correctness of the force calculation.
346    /// 
347    /// # Equation
348    /// 
349    /// ```text
350    /// U = -G *         M / r
351    ///   = -1 * 4.0 * 4.0 / 1.0
352    ///   = -16.0
353    /// 
354    /// F =   -U / r
355    ///   = 16.0 / 1.0
356    /// ```
357    #[test]
358    fn force_validation3() {
359        let p1pos = Vec3::new(0.0, 0.0, 0.0);
360        let p2pos = Vec3::new(1.0, 0.0, 0.0);
361
362        let p1m = 4.0;
363        let p2m = 4.0;
364
365        let newton = NewtonForce::with_gravity_factor(1.0);
366
367        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
368        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
369
370        let force = newton.force(&p1, &p2);
371
372        assert_eq!(force.length(), 16.0, "force should be 16.0");
373    }
374
375    /// This test validates the correctness of the force calculation.
376    /// 
377    /// # Equation
378    /// 
379    /// ```text
380    /// U = -G *         M / r
381    ///   = -1 * 1.0 * 1.0 / 2.0
382    ///   = -0.5
383    /// 
384    /// F =   -U / r
385    ///   =  0.5 / 2.0
386    ///   =  0.25
387    /// ```
388    #[test]
389    fn force_validation4() {
390        let p1pos = Vec3::new(0.0, 0.0, 0.0);
391        let p2pos = Vec3::new(2.0, 0.0, 0.0);
392
393        let p1m = 1.0;
394        let p2m = 1.0;
395
396        let newton = NewtonForce::with_gravity_factor(1.0);
397
398        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
399        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
400
401        let force = newton.force(&p1, &p2);
402
403        assert_ne!(force.length(), 0.5, "newton force should not be 0.5. force is missing division of potential (correct) by distance (missing)");
404        assert_eq!(force.length(), 0.25, "force should be 0.25");
405    }
406
407    /// This test validates the correctness of the force calculation.
408    /// 
409    /// # Equation
410    /// 
411    /// ```text
412    /// U = -G *         M / r
413    ///   = -1 * 1.0 * 2.0 / 2.0
414    ///   = -1.0
415    /// 
416    /// F =   -U / r
417    ///   = 1.0 / 2.0
418    ///   = 0.5
419    /// ```
420    #[test]
421    fn force_validation5() {
422        let p1pos = Vec3::new(0.0, 0.0, 0.0);
423        let p2pos = Vec3::new(2.0, 0.0, 0.0);
424
425        let p1m = 1.0;
426        let p2m = 2.0;
427
428        let newton = NewtonForce::with_gravity_factor(1.0);
429
430        let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
431        let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
432
433        let force = newton.force(&p1, &p2);
434
435        assert_ne!(force.length(), 1.0, "newton force should not be 1.0. force is missing division of potential (correct) by distance (missing)");
436        assert_eq!(force.length(), 0.5, "force should be 0.5");
437    }
438
439}
440
441
442    // /// This test validates the correctness of the potential energy calculation
443    // /// in a more complex scenario.
444    // /// 
445    // /// # Equation
446    // /// 
447    // /// ```text
448    // /// U = -G *         M / r
449    // ///   = -1 * 5.0 * 2.0 / 10.0
450    // ///   = -1.0
451    // /// ```
452    // #[test]
453    // fn potential_energy_validation_complex() {
454    //     let p1pos = Vec3::new(0.0, 0.0, 0.0);
455    //     let p2pos = Vec3::new(10.0, 0.0, 0.0);
456
457    //     let p1m = 5.0;
458    //     let p2m = 2.0;
459
460    //     let newton = NewtonForce::with_gravity_factor(1.0);
461
462    //     let p1 = Particle::from_data(p1pos, Vec3::zero(), p1m);
463    //     let p2 = Particle::from_data(p2pos, Vec3::zero(), p2m);
464
465    //     let potential = newton.potential(&p1, &p2);
466
467    //     assert_eq!(potential, -1.0, "potential energy should be -1.0");
468    // }