Skip to main content

moldyn_core/forces/
mod.rs

1mod custom;
2mod ljp;
3mod newton;
4
5use crate::{Particle, Vec3};
6pub use custom::CustomForce;
7pub use ljp::LennardJonesForce;
8use meval::Expr;
9pub use newton::NewtonForce;
10use serde::de::Error;
11use serde::{Deserialize, Serialize, de::Visitor};
12
13/// The trait for force systems. A force system is a mathematical model which
14/// describes the [potential energy](https://en.wikipedia.org/wiki/Potential_energy)
15/// between two particles.
16///
17/// This trait provides two methods [Force::potential] and [Force::force] for interacting
18/// with the force system. The potential is the scalar value used as the factor to the
19/// normalized force vector.
20///
21/// The method [Force::system_name] is used for serialization of the force system.
22pub trait Force {
23    /// # Returns
24    ///
25    /// Name of the force system, which is used for serialization and
26    /// deserialization. The characters are expected to be in `dash-case`.
27    fn system_name(&self) -> &str;
28
29    /// Calculates the potential energy between two particles.
30    fn potential(&self, particle: &Particle, other: &Particle) -> f64;
31
32    /// Calculates the force between two particles. For directly applying the
33    /// force, see [Force::apply_force].
34    ///
35    /// # Formula
36    ///
37    /// ```text
38    /// F = -U / r    (simplified to scalar, actually a vector)
39    /// ```
40    ///
41    /// The resulting force is a three dimensional vector pointing towards the other
42    /// particle. The magnitude is the fraction of potential energy and distance.
43    ///
44    /// # Returns
45    ///
46    /// The force vector that should be applied to the first particle. According
47    /// to the [Third Law](https://en.wikipedia.org/wiki/Newton%27s_laws_of_motion#Third_law)
48    /// the second particle should receive the negated force.
49    ///
50    /// # Example
51    ///
52    /// ```rust
53    /// use moldyn_core::{Particle, Vec3, LennardJonesForce, Force};
54    ///
55    /// let mut particle1 = Particle::from_data(Vec3::new(0.0, 0.0, 0.0), Vec3::zero(), 1.0);
56    /// let mut particle2 = Particle::from_data(Vec3::new(1.0, 0.0, 0.0), Vec3::zero(), 1.0);
57    ///
58    /// let force = LennardJonesForce::default().force(&particle1, &particle2);
59    ///
60    /// particle1.apply_force(force);
61    /// particle2.apply_force(-force);
62    /// ```
63    fn force(&self, particle: &Particle, other: &Particle) -> Vec3 {
64        let potential = self.potential(particle, other);
65        let diff = Particle::position_difference(other, particle);
66        let distance = diff.length2();
67
68        if distance == 0.0 {
69            Vec3::zero()
70        } else {
71            -diff * (potential / distance)
72        }
73
74        // TODO consider using the following code instead of above (after fixing paraview bugs)
75
76        // // If the direction is not calculatable (`particle.position == other.position`), we can skip the rest of the calculations.
77        // let direction = match Particle::direction(particle, other) {
78        //     Some(dir) => dir,
79        //     None => return Vec3::zero(),
80        // };
81
82        // // The force is the potential multiplied by the normalized direction vector.
83        // direction * self.potential(particle, other)
84    }
85
86    /// Applies the calculated force to a particle pair.
87    ///
88    /// # Example
89    ///
90    /// ```rust
91    /// use moldyn_core::{Particle, Vec3, LennardJonesForce, Force};
92    ///
93    /// let mut particle1 = Particle::from_data(Vec3::new(0.0, 0.0, 0.0), Vec3::zero(), 1.0);
94    /// let mut particle2 = Particle::from_data(Vec3::new(1.0, 0.0, 0.0), Vec3::zero(), 1.0);
95    ///
96    /// let lennard_jones = LennardJonesForce::default();
97    /// let force = lennard_jones.apply_force(&mut particle1, &mut particle2);
98    /// ```
99    fn apply_force(&self, particle: &mut Particle, other: &mut Particle) {
100        let force = self.force(particle, other);
101        particle.apply_force(force);
102        other.apply_force(-force);
103    }
104}
105
106impl<'a> Serialize for dyn Force + 'a {
107    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: serde::Serializer,
110    {
111        serializer.serialize_str(self.system_name())
112    }
113}
114
115struct ForceVisitor;
116
117impl<'de> Visitor<'de> for ForceVisitor {
118    type Value = Box<dyn Force>;
119
120    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
121        formatter.write_str("a force")
122    }
123
124    /// If the force is represented as a string, we can parse it as a known force type,
125    /// assuming default parameters. Strings are case-insensitive.
126    ///
127    /// # Example
128    ///
129    /// ```yaml
130    /// # Particle definition input file example
131    /// name: halleys-comet
132    /// force: gravitational
133    /// ```
134    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
135    where
136        E: serde::de::Error,
137    {
138        match value.to_ascii_lowercase().as_str() {
139            "lennardjones" | "lennard-jones" | "lj" => Ok(Box::new(LennardJonesForce::default())),
140            "newton" | "gravitational" => Ok(Box::new(NewtonForce::default())),
141            _ => Err(E::custom(format!("Unknown force type: {}", value))),
142        }
143    }
144
145    // TODO: implement deserialization with parameters.
146    // idea: force: { type: ..., params... }
147    // idea: force: lennard-jones: { epsilon: ..., sigma: ... }
148    // idea: force: gravity: { factor: ... }
149
150    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
151    where
152        A: serde::de::MapAccess<'de>,
153    {
154        let force_type = map
155            .next_key::<String>()?
156            .ok_or_else(|| A::Error::custom("Expected a force type key"))?;
157
158        match force_type.to_ascii_lowercase().as_str() {
159            "custom-potential" | "potential" => {
160                let params = map.next_value::<Expr>()?;
161                let custom: CustomForce = params.try_into().map_err(|e| {
162                    Error::custom(format!("could not bind custom potential functtuion: {e}"))
163                })?;
164                Ok(Box::new(custom))
165            }
166            _ => Err(A::Error::custom(format!(
167                "Unknown force type: {}",
168                force_type
169            ))),
170        }
171    }
172}
173
174impl<'de> Deserialize<'de> for Box<dyn Force> {
175    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
176    where
177        D: serde::Deserializer<'de>,
178    {
179        deserializer.deserialize_any(ForceVisitor)
180    }
181}
182
183impl Default for Box<dyn Force> {
184    /// The default force calculation system for this project is the lennard
185    /// jones potential. If not specified, the system will be initialized with
186    /// default parameters.
187    fn default() -> Self {
188        Box::new(LennardJonesForce::default())
189    }
190}