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 // }