cellular_raza_building_blocks/cell_building_blocks/
mechanics.rs

1use cellular_raza_concepts::{CalcError, Mechanics, RngError};
2
3use itertools::Itertools;
4use nalgebra::{SMatrix, SVector};
5
6use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "pyo3")]
9use pyo3::prelude::*;
10
11#[cfg(feature = "approx")]
12use approx::RelativeEq;
13
14macro_rules! implement_newton_damped_mechanics(
15    (
16        $struct_name:ident,
17        $d:literal
18    ) => {
19        implement_newton_damped_mechanics!($struct_name, $d, f64);
20    };
21    (
22        $struct_name:ident,
23        $d:literal,
24        $float_type:ty
25    ) => {
26        /// Newtonian dynamics governed by mass and damping.
27        ///
28        /// # Parameters
29        /// | Symbol | Parameter | Description |
30        /// | --- | --- | --- |
31        /// | $\vec{x}$ | `pos` | Position of the particle. |
32        /// | $\dot{\vec{x}}$ | `vel` | Velocity of the particle. |
33        /// | $\lambda$ | `damping` | Damping constant |
34        /// | $m$ | `mass` | Mass of the particle. |
35        ///
36        /// # Equations
37        /// The equation of motion is given by
38        /// \\begin{equation}
39        ///     m \ddot{\vec{x}} = \vec{F} - \lambda \dot{\vec{x}}
40        /// \\end{equation}
41        /// where $\vec{F}$ is the force as calculated by the
42        /// [Interaction](cellular_raza_concepts::Interaction) trait.
43        ///
44        /// # Comments
45        /// If the cell is growing, we need to increase the mass $m$.
46        /// By interacting with the outside world, we can adapt $\lambda$ to external values
47        /// although this is rarely desirable.
48        /// Both operations need to be implemented by other concepts such as
49        /// [Cycle](cellular_raza_concepts::Cycle).
50        #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
51        #[cfg_attr(feature = "pyo3", pyclass)]
52        #[cfg_attr(feature = "approx", derive(RelativeEq))]
53        #[cfg_attr(feature = "approx", approx(epsilon_type = $float_type))]
54        pub struct $struct_name {
55            /// Current position $\vec{x}$ given by a vector of dimension `D`.
56            #[cfg_attr(feature = "approx", approx(into_iter))]
57            pub pos: SVector<$float_type, $d>,
58            /// Current velocity $\dot{\vec{x}}$ given by a vector of dimension `D`.
59            #[cfg_attr(feature = "approx", approx(into_iter))]
60            pub vel: SVector<$float_type, $d>,
61            /// Damping constant $\lambda$.
62            pub damping_constant: $float_type,
63            /// Mass $m$ of the object.
64            pub mass: $float_type,
65        }
66
67        impl $struct_name {
68            #[doc = "Create a new "]
69            #[doc = stringify!($struct_name)]
70            /// from position, velocity, damping constant and mass
71            pub fn new(
72                pos: [$float_type; $d],
73                vel: [$float_type; $d],
74                damping_constant: $float_type,
75                mass: $float_type,
76            ) -> Self {
77                Self {
78                    pos: pos.into(),
79                    vel: vel.into(),
80                    damping_constant,
81                    mass,
82                }
83            }
84
85        }
86
87        #[cfg(feature = "pyo3")]
88        #[pymethods]
89        #[cfg_attr(docsrs, doc(cfg(feature = "pyo3")))]
90        impl $struct_name {
91            #[new]
92            fn _new(
93                pos: [$float_type; $d],
94                vel: [$float_type; $d],
95                damping_constant: $float_type,
96                mass: $float_type,
97            ) -> Self {
98                Self::new(pos, vel, damping_constant, mass)
99            }
100
101
102            /// [pyo3] getter for `pos`
103            #[getter]
104            pub fn get_pos(&self) -> [$float_type; $d] {
105                self.pos.into()
106            }
107
108            /// [pyo3] getter for `vel`
109            #[getter]
110            pub fn get_vel(&self) -> [$float_type; $d] {
111                self.vel.into()
112            }
113
114            /// [pyo3] getter for `damping_constant`
115            #[getter]
116            pub fn get_damping_constant(&self) -> $float_type {
117                self.damping_constant
118            }
119
120            /// [pyo3] getter for `mass`
121            #[getter]
122            pub fn get_mass(&self) -> $float_type {
123                self.mass
124            }
125
126            /// [pyo3] setter for `pos`
127            #[setter]
128            pub fn set_pos(&mut self, pos: [$float_type; $d]) {
129                self.pos = pos.into();
130            }
131
132            /// [pyo3] setter for `vel`
133            #[setter]
134            pub fn set_vel(&mut self, vel: [$float_type; $d]) {
135                self.vel = vel.into();
136            }
137
138            /// [pyo3] setter for `damping_constant`
139            #[setter]
140            pub fn set_damping_constant(&mut self, damping_constant: $float_type) {
141                self.damping_constant = damping_constant;
142            }
143
144            /// [pyo3] setter for `mass`
145            #[setter]
146            pub fn set_mass(&mut self, mass: $float_type) {
147                self.mass = mass;
148            }
149        }
150
151        impl Mechanics<
152            SVector<$float_type, $d>,
153            SVector<$float_type, $d>,
154            SVector<$float_type, $d>,
155            $float_type
156        > for $struct_name
157        {
158            fn get_random_contribution(
159                &self,
160                _: &mut rand_chacha::ChaCha8Rng,
161                _dt: $float_type,
162            ) -> Result<(SVector<$float_type, $d>, SVector<$float_type, $d>), RngError> {
163                Ok((num::Zero::zero(), num::Zero::zero()))
164            }
165
166            fn calculate_increment(
167                &self,
168                force: SVector<$float_type, $d>,
169            ) -> Result<(SVector<$float_type, $d>, SVector<$float_type, $d>), CalcError> {
170                let dx = self.vel;
171                let dv = force / self.mass - self.damping_constant * self.vel;
172                Ok((dx, dv))
173            }
174        }
175
176        impl cellular_raza_concepts::Position<SVector<$float_type, $d>> for $struct_name {
177            fn pos(&self) -> SVector<$float_type, $d> {
178                self.pos
179            }
180
181            fn set_pos(&mut self, pos: &SVector<$float_type, $d>) {
182                self.pos = *pos;
183            }
184        }
185
186        impl cellular_raza_concepts::Velocity<SVector<$float_type, $d>> for $struct_name {
187            fn velocity(&self) -> SVector<$float_type, $d> {
188                self.vel
189            }
190
191            fn set_velocity(&mut self, velocity: &SVector<$float_type, $d>) {
192                self.vel = *velocity;
193            }
194        }
195    }
196);
197
198implement_newton_damped_mechanics!(NewtonDamped1D, 1);
199implement_newton_damped_mechanics!(NewtonDamped2D, 2);
200implement_newton_damped_mechanics!(NewtonDamped3D, 3);
201
202implement_newton_damped_mechanics!(NewtonDamped1DF32, 1, f32);
203implement_newton_damped_mechanics!(NewtonDamped2DF32, 2, f32);
204implement_newton_damped_mechanics!(NewtonDamped3DF32, 3, f32);
205
206/// Generate a vector corresponding to a wiener process.
207///
208/// This function calculates a statically sized random vector with dimension `D`.
209/// It uses a [rand_distr::StandardNormal] distribution and divides the result by `dt` such that
210/// the correct incremental wiener process is obtained.
211pub fn wiener_process<F, const D: usize>(
212    rng: &mut rand_chacha::ChaCha8Rng,
213    dt: F,
214) -> Result<SVector<F, D>, RngError>
215where
216    F: core::ops::DivAssign + nalgebra::Scalar + num::Float,
217    rand_distr::StandardNormal: rand_distr::Distribution<F>,
218{
219    let std_dev = dt.sqrt();
220    let distr = match rand_distr::Normal::new(F::zero(), std_dev) {
221        Ok(e) => Ok(e),
222        Err(e) => Err(RngError(format!("{e}"))),
223    }?;
224    let random_dir = SVector::<F, D>::from_distribution(&distr, rng);
225    Ok(random_dir / dt)
226}
227
228macro_rules! implement_brownian_mechanics(
229    ($struct_name:ident, $d:literal, $float_type:ty) => {
230        /// Brownian motion of particles
231        ///
232        /// # Parameters & Variables
233        /// | Symbol | Struct Field | Description |
234        /// | --- | --- | --- |
235        /// | $D$ | `diffusion_constant` | Damping constant of each particle. |
236        /// | $k_BT$ | `kb_temperature` | Product of temperature $T$ and Boltzmann constant $k_B$. |
237        /// | | | |
238        /// | $\vec{x}$ | `pos` | Position of the particle. |
239        /// | $R(t)$ | (automatically generated) | Gaussian process |
240        ///
241        /// # Equations
242        /// We integrate the standard brownian motion stochastic differential equation.
243        /// \\begin{equation}
244        ///     \dot{\vec{x}} = -\frac{D}{k_B T}\nabla V(x) + \sqrt{2D}R(t)
245        /// \\end{equation}
246        /// The new random vector is then also sampled by a distribution with greater width.
247        /// If we choose this value larger than one, we can
248        /// resolve smaller timesteps to more accurately solve the equations.
249        #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
250        #[cfg_attr(feature = "pyo3", pyclass)]
251        #[cfg_attr(feature = "approx", derive(RelativeEq))]
252        #[cfg_attr(feature = "approx", approx(epsilon_type = $float_type))]
253        pub struct $struct_name {
254            /// Current position of the particle $\vec{x}$.
255            #[cfg_attr(feature = "approx", approx(into_iter))]
256            pub pos: SVector<$float_type, $d>,
257            /// Diffusion constant $D$.
258            pub diffusion_constant: $float_type,
259            /// The product of temperature and boltzmann constant $k_B T$.
260            pub kb_temperature: $float_type,
261        }
262
263        impl $struct_name {
264            /// Constructs a new
265            #[doc = concat!("[", stringify!($struct_name), "]")]
266            pub fn new(
267                pos: [$float_type; $d],
268                diffusion_constant: $float_type,
269                kb_temperature: $float_type,
270            ) -> Self {
271                Self {
272                    pos: pos.into(),
273                    diffusion_constant,
274                    kb_temperature,
275                }
276            }
277        }
278
279        #[cfg(feature = "pyo3")]
280        #[pymethods]
281        #[cfg_attr(docsrs, doc(cfg(feature = "pyo3")))]
282        impl $struct_name {
283            #[new]
284            fn _new(
285                pos: [$float_type; $d],
286                diffusion_constant: $float_type,
287                kb_temperature: $float_type,
288            ) -> Self {
289                Self::new(pos, diffusion_constant, kb_temperature)
290            }
291
292
293            /// [pyo3] setter for `pos`
294            #[setter]
295            pub fn set_pos(&mut self, pos: [$float_type; $d]) {
296                self.pos = pos.into();
297            }
298
299            /// [pyo3] setter for `diffusion_constant`
300            #[setter]
301            pub fn set_diffusion_constant(&mut self, diffusion_constant: $float_type) {
302                self.diffusion_constant = diffusion_constant;
303            }
304
305            /// [pyo3] setter for `kb_temperature`
306            #[setter]
307            pub fn set_kb_temperature(&mut self, kb_temperature: $float_type) {
308                self.kb_temperature = kb_temperature;
309            }
310
311            /// [pyo3] getter for `pos`
312            #[getter]
313            pub fn get_pos(&self) -> [$float_type; $d] {
314                self.pos.into()
315            }
316
317            /// [pyo3] getter for `diffusion_constant`
318            #[getter]
319            pub fn get_diffusion_constant(&self) -> $float_type {
320                self.diffusion_constant
321            }
322
323            /// [pyo3] getter for `kb_temperature`
324            #[getter]
325            pub fn get_kb_temperature(&self) -> $float_type {
326                self.kb_temperature
327            }
328        }
329
330        impl Mechanics<
331            SVector<$float_type, $d>,
332            SVector<$float_type, $d>,
333            SVector<$float_type, $d>,
334            $float_type
335        > for $struct_name {
336            fn get_random_contribution(
337                &self,
338                rng: &mut rand_chacha::ChaCha8Rng,
339                dt: $float_type,
340            ) -> Result<(SVector<$float_type, $d>, SVector<$float_type, $d>), RngError> {
341                let dpos = (2.0 as $float_type * self.diffusion_constant).sqrt()
342                    * wiener_process(
343                    rng,
344                    dt
345                )?;
346                let dvel = SVector::<$float_type, $d>::zeros();
347                Ok((dpos, dvel))
348            }
349
350            fn calculate_increment(
351                &self,
352                force: SVector<$float_type, $d>,
353            ) -> Result<(SVector<$float_type, $d>, SVector<$float_type, $d>), CalcError> {
354                use num::Zero;
355                let dx = self.diffusion_constant / self.kb_temperature * force;
356                Ok((dx, SVector::<$float_type, $d>::zero()))
357            }
358        }
359
360        impl cellular_raza_concepts::Position<SVector<$float_type, $d>> for $struct_name {
361            fn pos(&self) -> SVector<$float_type, $d> {
362                self.pos
363            }
364
365            fn set_pos(&mut self, pos: &SVector<$float_type, $d>) {
366                self.pos = *pos;
367            }
368
369        }
370
371        impl cellular_raza_concepts::Velocity<SVector<$float_type, $d>> for $struct_name {
372            fn velocity(&self) -> SVector<$float_type, $d> {
373                use num::Zero;
374                SVector::<$float_type, $d>::zero()
375            }
376
377            fn set_velocity(&mut self, _velocity: &SVector<$float_type, $d>) {}
378        }
379    }
380);
381
382implement_brownian_mechanics!(Brownian1D, 1, f64);
383implement_brownian_mechanics!(Brownian2D, 2, f64);
384implement_brownian_mechanics!(Brownian3D, 3, f64);
385implement_brownian_mechanics!(Brownian1DF32, 1, f32);
386implement_brownian_mechanics!(Brownian2DF32, 2, f32);
387implement_brownian_mechanics!(Brownian3DF32, 3, f32);
388
389macro_rules! define_langevin_nd(
390    ($struct_name:ident, $d:literal, $float_type:ident) => {
391        /// Langevin dynamics
392        ///
393        /// # Parameters & Variables
394        /// | Symbol | Struct Field | Description |
395        /// |:---:| --- | --- |
396        /// | $M$ | `mass` | Mass of the particle. |
397        /// | $\gamma$ | `damping` | Damping constant |
398        /// | $k_BT$ | `kb_temperature` | Product of temperature $T$ and Boltzmann constant $k_B$. |
399        /// | | | |
400        /// | $\vec{X}$ | `pos` | Position of the particle. |
401        /// | $\dot{\vec{X}}$ | `vel` | Velocity of the particle. |
402        /// | $R(t)$ | (automatically generated) | Gaussian process |
403        ///
404        /// # Equations
405        ///
406        /// \\begin{equation}
407        ///     M \ddot{\mathbf{X}} = - \mathbf{\nabla} U(\mathbf{X}) - \gamma M\dot{\mathbf{X}} + \sqrt{2 M \gamma k_{\rm B} T}\mathbf{R}(t)
408        /// \\end{equation}
409        #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
410        #[cfg_attr(feature = "pyo3", pyclass)]
411        #[cfg_attr(feature = "approx", derive(RelativeEq))]
412        #[cfg_attr(feature = "approx", approx(epsilon_type = $float_type))]
413        pub struct $struct_name {
414            /// Current position
415            #[cfg_attr(feature = "approx", approx(into_iter))]
416            pub pos: SVector<$float_type, $d>,
417            /// Current velocity
418            #[cfg_attr(feature = "approx", approx(into_iter))]
419            pub vel: SVector<$float_type, $d>,
420            /// Mass of the object
421            pub mass: $float_type,
422            /// Damping constant
423            pub damping: $float_type,
424            /// Product of Boltzmann constant and temperature
425            pub kb_temperature: $float_type,
426        }
427
428        impl Mechanics<
429            SVector<$float_type, $d>,
430            SVector<$float_type, $d>,
431            SVector<$float_type, $d>,
432            $float_type
433        > for $struct_name {
434            fn get_random_contribution(
435                &self,
436                rng: &mut rand_chacha::ChaCha8Rng,
437                dt: $float_type,
438            ) -> Result<(SVector<$float_type, $d>, SVector<$float_type, $d>), RngError> {
439                let dvel = (
440                    2.0 as $float_type
441                    * self.damping
442                    * self.kb_temperature
443                    / self.mass
444                ).sqrt() * wiener_process(
445                    rng,
446                    dt
447                )?;
448                let dpos = SVector::<$float_type, $d>::zeros();
449                Ok((dpos, dvel))
450            }
451
452            fn calculate_increment(
453                &self,
454                force: SVector<$float_type, $d>,
455            ) -> Result<(SVector<$float_type, $d>, SVector<$float_type, $d>), CalcError> {
456                let dx = self.vel;
457                let dv1 =
458                    1.0 as $float_type / self.mass * force;
459                let dv2 =
460                    - self.damping * self.vel;
461                let dv = dv1 + dv2;
462                Ok((dx, dv))
463            }
464        }
465
466        impl cellular_raza_concepts::Position<SVector<$float_type, $d>> for $struct_name {
467            fn pos(&self) -> SVector<$float_type, $d> {
468                self.pos
469            }
470
471            fn set_pos(&mut self, pos: &SVector<$float_type, $d>) {
472                self.pos = *pos;
473            }
474        }
475
476        impl cellular_raza_concepts::Velocity<SVector<$float_type, $d>> for $struct_name {
477            fn velocity(&self) -> SVector<$float_type, $d> {
478                self.vel
479            }
480
481            fn set_velocity(&mut self, velocity: &SVector<$float_type, $d>) {
482                self.vel = *velocity;
483            }
484        }
485
486        #[cfg(feature = "pyo3")]
487        #[pymethods]
488        #[cfg_attr(docsrs, doc(cfg(feature = "pyo3")))]
489        impl $struct_name {
490            /// Creates a new [
491            #[doc = stringify!($struct_name)]
492            /// ] struct from position, velocity, mass, damping,
493            /// kb_temperature and the update interval of the mechanics aspect.
494            #[new]
495            fn _new(
496                pos: [$float_type; $d],
497                vel: [$float_type; $d],
498                mass: $float_type,
499                damping: $float_type,
500                kb_temperature: $float_type,
501            ) -> Self {
502                Self {
503                    pos: pos.into(),
504                    vel: vel.into(),
505                    mass,
506                    damping,
507                    kb_temperature,
508                }
509            }
510
511            #[getter(pos)]
512            /// [pyo3] getter for `position`
513            pub fn get_position(&self) -> [$float_type; $d] {
514                self.pos.into()
515            }
516
517            #[setter(pos)]
518            /// [pyo3] setter for `position`
519            pub fn set_position(&mut self, pos: [$float_type; $d]) {
520                self.pos = pos.into();
521            }
522
523            #[getter(damping)]
524            /// [pyo3] getter for `damping`
525            pub fn get_damping(&self) -> $float_type {
526                self.damping
527            }
528
529            #[setter(damping)]
530            /// [pyo3] setter for `damping`
531            pub fn set_damping(&mut self, damping: $float_type) {
532                self.damping = damping;
533            }
534
535            #[getter(mass)]
536            /// [pyo3] getter for `mass`
537            pub fn get_mass(&self) -> $float_type {
538                self.mass
539            }
540
541            #[setter(mass)]
542            /// [pyo3] setter for `mass`
543            pub fn set_mass(&mut self, mass: $float_type) {
544                self.mass = mass;
545            }
546
547            #[getter(kb_temperature)]
548            /// [pyo3] getter for `kb_temperature`
549            pub fn get_kb_temperature(&self) -> $float_type {
550                self.kb_temperature
551            }
552
553            #[setter(kb_temperature)]
554            /// [pyo3] setter for `kb_temperature`
555            pub fn set_kb_temperature(&mut self, kb_temperature: $float_type) {
556                self.kb_temperature = kb_temperature;
557            }
558
559            fn __repr__(&self) -> String {
560                format!("{self:#?}")
561            }
562        }
563    }
564);
565
566define_langevin_nd!(Langevin1D, 1, f64);
567define_langevin_nd!(Langevin2D, 2, f64);
568define_langevin_nd!(Langevin3D, 3, f64);
569define_langevin_nd!(Langevin1DF32, 1, f32);
570define_langevin_nd!(Langevin2DF32, 2, f32);
571define_langevin_nd!(Langevin3DF32, 3, f32);
572
573/// Mechanics model which represents cells as vertices with edges between them.
574///
575/// The vertices are attached to each other with springs and a given length between each
576/// vertex.
577/// Furthermore, we define a central pressure that acts when the total cell area is greater
578/// or smaller than the desired one.
579/// Each vertex is damped individually by the same constant.
580// TODO include more formulas for this model
581#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
582pub struct VertexMechanics2D<const D: usize> {
583    points: nalgebra::SMatrix<f64, D, 2>,
584    velocity: nalgebra::SMatrix<f64, D, 2>,
585    /// Boundary lengths of individual edges
586    pub cell_boundary_lengths: nalgebra::SVector<f64, D>,
587    /// Spring tensions of individual edges
588    pub spring_tensions: nalgebra::SVector<f64, D>,
589    /// Total cell area
590    pub cell_area: f64,
591    /// Central pressure going from middle of the cell outwards
592    pub central_pressure: f64,
593    /// Damping constant
594    pub damping_constant: f64,
595    /// Controls the random motion of the entire cell
596    pub diffusion_constant: f64,
597}
598
599impl<const D: usize> VertexMechanics2D<D> {
600    /// Creates a new vertex model in equilibrium conditions.
601    ///
602    /// The specified parameters are then used to carefully calculate relating properties of the model.
603    /// We outline the formulas used.
604    /// Given the number of vertices \\(N\\) in our model (specified by the const generic argument
605    /// of the [VertexMechanics2D] struct),
606    /// the resulting average angle when all nodes are equally distributed is
607    /// \\[
608    ///     \Delta\varphi = \frac{2\pi}{N}
609    /// \\]
610    /// Given the total area of the cell ([regular polygon](https://en.wikipedia.org/wiki/Regular_polygon)) \\(A\\), we can calculate the distance from the center point to the individual vertices by inverting
611    /// \\[
612    ///     A = r^2 \sin\left(\frac{\pi}{N}\right)\cos\left(\frac{\pi}{N}\right).
613    /// \\]
614    /// This formula can be derived by elementary geometric arguments.
615    /// We can then generate the points \\(\vec{p}\_i\\) which make up the cell-boundary by using
616    /// \\[
617    ///     \vec{p}\_i = \vec{p}\_{mid} + r(\cos(i \Delta\varphi), \sin(i\Delta\varphi))^T.
618    /// \\]
619    /// From these points, their distance is calculated and passed as the individual boundary lengths.
620    /// When randomization is turned on, these points will be slightly randomized in their radius and angle which might lead to non-equilibrium configurations.
621    /// Pressure, damping and spring tensions are not impacted by randomization.
622    pub fn new(
623        middle: SVector<f64, 2>,
624        cell_area: f64,
625        rotation_angle: f64,
626        spring_tensions: f64,
627        central_pressure: f64,
628        damping_constant: f64,
629        diffusion_constant: f64,
630        randomize: Option<(f64, rand_chacha::ChaCha8Rng)>,
631    ) -> Self {
632        use rand::Rng;
633        // Restrict the randomize variable between 0 and 1
634        let r = match randomize {
635            Some((rand, _)) => rand.clamp(0.0, 1.0),
636            _ => 0.0,
637        };
638        let rng = || -> f64 {
639            match randomize {
640                Some((_, mut rng)) => rng.random_range(0.0..1.0),
641                None => 0.0,
642            }
643        };
644        // Randomize the overall rotation angle
645        let rotation_angle = (1.0 - r * rng.clone()()) * rotation_angle;
646        // Calculate the angle fraction used to determine the points of the polygon
647        let angle_fraction = std::f64::consts::PI / D as f64;
648        // Calculate the radius from cell area
649        let radius = (cell_area / D as f64 / angle_fraction.sin() / angle_fraction.cos()).sqrt();
650        // TODO this needs to be calculated again
651        let points = nalgebra::SMatrix::<f64, D, 2>::from_row_iterator((0..D).flat_map(|n| {
652            let angle =
653                rotation_angle + 2.0 * angle_fraction * n as f64 * (1.0 - r * rng.clone()());
654            let radius_modified = radius * (1.0 + 0.5 * r * (1.0 - rng.clone()()));
655            [
656                middle.x + radius_modified * angle.cos(),
657                middle.y + radius_modified * angle.sin(),
658            ]
659            .into_iter()
660        }));
661        // Randomize the boundary lengths
662        let cell_boundary_lengths = SVector::<f64, D>::from_iterator(
663            points
664                .row_iter()
665                .circular_tuple_windows()
666                .map(|(p1, p2)| (p1 - p2).norm()),
667        );
668        VertexMechanics2D {
669            points,
670            velocity: nalgebra::SMatrix::<f64, D, 2>::zeros(),
671            cell_boundary_lengths,
672            spring_tensions: SVector::<f64, D>::from_element(spring_tensions),
673            cell_area,
674            central_pressure,
675            damping_constant,
676            diffusion_constant,
677        }
678    }
679
680    /// Calculates the boundary length of the regular polygon given the total area in equilibrium.
681    ///
682    /// The formula used is
683    /// $$\\begin{align}
684    ///     A &= \frac{L^2}{4n\tan\left(\frac{\pi}{n}\right)}\\\\
685    ///     L &= \sqrt{4An\tan\left(\frac{\pi}{n}\right)}
686    /// \\end{align}$$
687    /// where $A$ is the total area, $n$ is the number of vertices and $L$ is the total boundary
688    /// length.
689    pub fn calculate_boundary_length(cell_area: f64) -> f64 {
690        (4.0 * cell_area * (std::f64::consts::PI / D as f64).tan() * D as f64).sqrt()
691    }
692
693    /// Calculates the cell area of the regular polygon in equilibrium.
694    ///
695    /// The formula used is identical the the one of [Self::calculate_boundary_length].
696    pub fn calculate_cell_area(boundary_length: f64) -> f64 {
697        D as f64 * boundary_length.powf(2.0) / (4.0 * (std::f64::consts::PI / D as f64).tan())
698    }
699
700    /// Calculates the current area of the cell
701    pub fn get_current_cell_area(&self) -> f64 {
702        0.5_f64
703            * self
704                .points
705                .row_iter()
706                .circular_tuple_windows()
707                .map(|(p1, p2)| p1.transpose().perp(&p2.transpose()))
708                .sum::<f64>()
709    }
710
711    /// Calculate the current polygons boundary length
712    pub fn calculate_current_boundary_length(&self) -> f64 {
713        self.points
714            .row_iter()
715            .tuple_windows::<(_, _)>()
716            .map(|(p1, p2)| (p2 - p1).norm())
717            .sum::<f64>()
718    }
719
720    /// Obtain current cell area
721    pub fn get_cell_area(&self) -> f64 {
722        self.cell_area
723    }
724
725    /// Set the current cell area and adjust the length of edges such that the cell is still in
726    /// equilibrium.
727    pub fn set_cell_area_and_boundary_length(&mut self, cell_area: f64) {
728        // Calculate the relative difference to current area
729        match self.cell_area {
730            0.0 => {
731                let new_interaction_parameters = Self::new(
732                    self.points
733                        .row_iter()
734                        .map(|v| v.transpose())
735                        .sum::<nalgebra::Vector2<f64>>(),
736                    cell_area,
737                    0.0,
738                    self.spring_tensions.sum() / self.spring_tensions.len() as f64,
739                    self.central_pressure,
740                    self.damping_constant,
741                    self.diffusion_constant,
742                    None,
743                );
744                *self = new_interaction_parameters;
745            }
746            _ => {
747                let relative_length_difference = (cell_area.abs() / self.cell_area.abs()).sqrt();
748                // Calculate the new length of the cell boundary lengths
749                self.cell_boundary_lengths
750                    .iter_mut()
751                    .for_each(|length| *length *= relative_length_difference);
752                self.cell_area = cell_area;
753            }
754        };
755    }
756
757    /// Change the internal cell area
758    pub fn set_cell_area(&mut self, cell_area: f64) {
759        self.cell_area = cell_area;
760    }
761}
762
763impl VertexMechanics2D<6> {
764    /// Fills the area of a given rectangle with hexagonal cells. Their orientation is such that
765    /// the top border has a flat top.
766    ///
767    /// The produced pattern will like similar to this.
768    /// ```text
769    /// __________________________________
770    /// |   ___       ___          ___   |
771    /// |  /   \     /   \        /   \  |
772    /// | /     \___/     \_ ..._/     \ |
773    /// | \     /   \     /      \     / |
774    /// |  \___/     \___/        \___/  |
775    /// |  /   \     /   \        /   \  |
776    /// | .     .   .     .      .     . |
777    /// ```
778    /// The padding around the generated cells will be determined automatically.
779    pub fn fill_rectangle_flat_top(
780        cell_area: f64,
781        spring_tensions: f64,
782        central_pressure: f64,
783        damping_constant: f64,
784        diffusion_constant: f64,
785        rectangle: [SVector<f64, 2>; 2],
786    ) -> Vec<Self> {
787        // If the supplied area is larger than the total area, return nothing
788        let side_x = rectangle[1].x - rectangle[0].x;
789        let side_y = rectangle[1].y - rectangle[0].y;
790        if cell_area > side_x * side_y {
791            return Vec::new();
792        }
793        let segment_length = Self::calculate_boundary_length(cell_area) / 6.0;
794        let radius_outer = Self::outer_radius_from_cell_area(cell_area);
795        let radius_inner = Self::inner_radius_from_cell_area(cell_area);
796
797        // Check if only one single hexagon fits into the domain in any dimension
798        let n_max_x = (side_x - 2.0 * radius_outer).div_euclid(3.0 / 2.0 * radius_outer) as usize;
799        let n_max_y = side_y.div_euclid(2.0 * radius_inner);
800        let total_width_x = 2.0 * radius_outer + (n_max_x - 1) as f64 * 3.0 / 2.0 * radius_outer;
801        let total_width_y = n_max_y * 2.0 * radius_inner;
802
803        let pad_x = (side_x - total_width_x) / 2.0;
804        let pad_y = (side_y - total_width_y) / 2.0;
805        let padding = nalgebra::RowVector2::from([pad_x, pad_y]);
806
807        let mut generated_models = vec![];
808        for n_x in 0..n_max_x {
809            for n_y in 0..n_max_y as usize - n_x % 2 {
810                let middle = rectangle[0].transpose()
811                    + padding
812                    + nalgebra::RowVector2::from([
813                        (1.0 + 3.0 / 2.0 * n_x as f64) * radius_outer,
814                        (1 + 2 * n_y + n_x % 2) as f64 * radius_inner,
815                    ]);
816                let mut pos = nalgebra::SMatrix::<f64, 6, 2>::zeros();
817                for i in 0..6 {
818                    let phi = 2.0 * std::f64::consts::PI * i as f64 / 6.0;
819                    pos.set_row(
820                        i,
821                        &(middle
822                            + radius_outer * nalgebra::RowVector2::from([phi.cos(), phi.sin()])),
823                    );
824                }
825                generated_models.push(Self {
826                    points: pos,
827                    velocity: SMatrix::zeros(),
828                    cell_boundary_lengths: SVector::from_element(segment_length),
829                    spring_tensions: SVector::from_element(spring_tensions),
830                    cell_area,
831                    central_pressure,
832                    damping_constant,
833                    diffusion_constant,
834                });
835            }
836        }
837        generated_models
838    }
839}
840
841impl VertexMechanics2D<4> {
842    /// Fill a specified rectangle with cells of 4 vertices
843    pub fn fill_rectangle(
844        cell_area: f64,
845        spring_tensions: f64,
846        central_pressure: f64,
847        damping_constant: f64,
848        diffusion_constant: f64,
849        rectangle: [SVector<f64, 2>; 2],
850    ) -> Vec<Self> {
851        let cell_side_length: f64 = cell_area.sqrt();
852        let cell_side_length_padded: f64 = cell_side_length * 1.04;
853
854        let number_of_cells_x: u64 =
855            ((rectangle[1].x - rectangle[0].x) / cell_side_length_padded).floor() as u64;
856        let number_of_cells_y: u64 =
857            ((rectangle[1].y - rectangle[0].y) / cell_side_length_padded).floor() as u64;
858
859        let start_x: f64 = rectangle[0].x
860            + 0.5
861                * ((rectangle[1].x - rectangle[0].x)
862                    - number_of_cells_x as f64 * cell_side_length_padded);
863        let start_y: f64 = rectangle[0].y
864            + 0.5
865                * ((rectangle[1].y - rectangle[0].y)
866                    - number_of_cells_y as f64 * cell_side_length_padded);
867
868        use itertools::iproduct;
869        iproduct!(0..number_of_cells_x, 0..number_of_cells_y)
870            .map(|(i, j)| {
871                let corner = (
872                    start_x + (i as f64) * cell_side_length_padded,
873                    start_y + (j as f64) * cell_side_length_padded,
874                );
875
876                let points = nalgebra::SMatrix::<f64, 4, 2>::from_row_iterator([
877                    corner.0,
878                    corner.1,
879                    corner.0 + cell_side_length,
880                    corner.1,
881                    corner.0 + cell_side_length,
882                    corner.1 + cell_side_length,
883                    corner.0,
884                    corner.1 + cell_side_length,
885                ]);
886                let cell_boundary_lengths = nalgebra::SVector::<f64, 4>::from_iterator(
887                    (0..2 * 4).map(|_| cell_side_length),
888                );
889
890                VertexMechanics2D {
891                    points,
892                    velocity: nalgebra::SMatrix::<f64, 4, 2>::zeros(),
893                    cell_boundary_lengths,
894                    spring_tensions: nalgebra::SVector::<f64, 4>::from_element(spring_tensions),
895                    cell_area,
896                    central_pressure,
897                    damping_constant,
898                    diffusion_constant,
899                }
900            })
901            .collect::<Vec<_>>()
902    }
903}
904
905impl<const D: usize> VertexMechanics2D<D> {
906    /// Calculates the outer circle radius of the Regular Polygon given its area.
907    pub fn outer_radius_from_cell_area(cell_area: f64) -> f64 {
908        // let segment_length = Self::calculate_boundary_length(cell_area) / D as f64;
909        // segment_length / (std::f64::consts::PI / D as f64).tan() / 2.0
910        let boundary_length = Self::calculate_boundary_length(cell_area);
911        Self::outer_radius_from_boundary_length(boundary_length)
912    }
913
914    /// Calculates the outer circle radius of the Regular Polygon given its boundary length.
915    pub fn outer_radius_from_boundary_length(boundary_length: f64) -> f64 {
916        let segment_length = boundary_length / D as f64;
917        segment_length / (std::f64::consts::PI / D as f64).sin() / 2.0
918    }
919
920    /// Calculates the inner circle radius of the Regular Polygon given its area.
921    pub fn inner_radius_from_cell_area(cell_area: f64) -> f64 {
922        let boundary_length = Self::calculate_boundary_length(cell_area);
923        Self::inner_radius_from_boundary_length(boundary_length)
924    }
925
926    /// Calculates the inner circle radius of the Regular Polygon given its boundary length.
927    pub fn inner_radius_from_boundary_length(boundary_length: f64) -> f64 {
928        let segment_length = boundary_length / D as f64;
929        segment_length / (std::f64::consts::PI / D as f64).tan() / 2.0
930    }
931}
932
933impl<const D: usize>
934    Mechanics<
935        nalgebra::SMatrix<f64, D, 2>,
936        nalgebra::SMatrix<f64, D, 2>,
937        nalgebra::SMatrix<f64, D, 2>,
938    > for VertexMechanics2D<D>
939{
940    fn calculate_increment(
941        &self,
942        force: nalgebra::SMatrix<f64, D, 2>,
943    ) -> Result<(nalgebra::SMatrix<f64, D, 2>, nalgebra::SMatrix<f64, D, 2>), CalcError> {
944        // Calculate the total internal force
945        let middle = self.points.row_sum() / self.points.shape().0 as f64;
946        let current_ara: f64 = 0.5_f64
947            * self
948                .points
949                .row_iter()
950                .circular_tuple_windows()
951                .map(|(p1, p2)| p1.transpose().perp(&p2.transpose()))
952                .sum::<f64>();
953
954        let mut internal_force = self.points * 0.0;
955        for (index, (point_1, point_2, point_3)) in self
956            .points
957            .row_iter()
958            .circular_tuple_windows::<(_, _, _)>()
959            .enumerate()
960        {
961            let tension_12 = self.spring_tensions[index];
962            let tension_23 = self.spring_tensions[(index + 1) % self.spring_tensions.len()];
963            let mut force_2 = internal_force.row_mut((index + 1) % self.points.shape().0);
964
965            // Calculate forces arising from springs between points
966            let p_21 = point_2 - point_1;
967            let p_23 = point_2 - point_3;
968            let force1 =
969                p_21.normalize() * (self.cell_boundary_lengths[index] - p_21.norm()) * tension_12;
970            let force2 = p_23.normalize()
971                * (self.cell_boundary_lengths[(index + 1) % self.cell_boundary_lengths.len()]
972                    - p_23.norm())
973                * tension_23;
974
975            // Calculate force arising from internal pressure
976            let middle_to_point_2 = point_2 - middle;
977            let force3 = middle_to_point_2.normalize()
978                * (self.cell_area - current_ara)
979                * self.central_pressure;
980
981            // Combine forces
982            force_2 += force1 + force2 + force3;
983        }
984        let dx = self.velocity;
985        let dv = force + internal_force - self.damping_constant * self.velocity;
986        Ok((dx, dv))
987    }
988
989    fn get_random_contribution(
990        &self,
991        rng: &mut rand_chacha::ChaCha8Rng,
992        dt: f64,
993    ) -> Result<(nalgebra::SMatrix<f64, D, 2>, nalgebra::SMatrix<f64, D, 2>), RngError> {
994        let mut dvel = nalgebra::SMatrix::<f64, D, 2>::zeros();
995        let dpos = nalgebra::SMatrix::<f64, D, 2>::zeros();
996        if dt != 0.0 {
997            let random_vector: SVector<f64, 2> = wiener_process(rng, dt)?;
998            dvel.row_iter_mut().for_each(|mut r| {
999                r *= 0.0;
1000                r += random_vector.transpose();
1001            });
1002            Ok((dpos, self.diffusion_constant * dvel))
1003        } else {
1004            Ok((nalgebra::SMatrix::<f64, D, 2>::zeros(), dvel))
1005        }
1006    }
1007}
1008
1009impl<const D: usize> cellular_raza_concepts::Position<nalgebra::SMatrix<f64, D, 2>>
1010    for VertexMechanics2D<D>
1011{
1012    fn pos(&self) -> nalgebra::SMatrix<f64, D, 2> {
1013        self.points
1014    }
1015
1016    fn set_pos(&mut self, pos: &nalgebra::SMatrix<f64, D, 2>) {
1017        self.points = *pos;
1018    }
1019}
1020
1021impl<const D: usize> cellular_raza_concepts::Velocity<nalgebra::SMatrix<f64, D, 2>>
1022    for VertexMechanics2D<D>
1023{
1024    fn velocity(&self) -> nalgebra::SMatrix<f64, D, 2> {
1025        self.velocity
1026    }
1027
1028    fn set_velocity(&mut self, velocity: &nalgebra::SMatrix<f64, D, 2>) {
1029        self.velocity = *velocity;
1030    }
1031}
1032
1033#[cfg(test)]
1034mod test_vertex_mechanics_6n {
1035    #[test]
1036    fn test_fill_too_small() {
1037        use crate::VertexMechanics2D;
1038        use nalgebra::Vector2;
1039        let models = VertexMechanics2D::<6>::fill_rectangle_flat_top(
1040            200.0,
1041            0.0,
1042            0.0,
1043            0.0,
1044            0.0,
1045            [Vector2::from([1.0, 1.0]), Vector2::from([2.0, 2.0])],
1046        );
1047        assert_eq!(models.len(), 0);
1048    }
1049
1050    #[test]
1051    fn test_fill_multiple() {
1052        use crate::VertexMechanics2D;
1053        use cellular_raza_concepts::Position;
1054        use nalgebra::Vector2;
1055        let models = VertexMechanics2D::<6>::fill_rectangle_flat_top(
1056            36.0,
1057            0.0,
1058            0.0,
1059            0.0,
1060            0.0,
1061            [Vector2::from([0.0; 2]), Vector2::from([100.0; 2])],
1062        );
1063        use itertools::Itertools;
1064        for (m1, m2) in models.into_iter().circular_tuple_windows() {
1065            if m1.pos().row_mean().transpose().x == m2.pos().row_mean().transpose().x {
1066                let max = m1
1067                    .pos()
1068                    .row_iter()
1069                    .map(|row| row.transpose().y)
1070                    .max_by(|x0, x1| x0.partial_cmp(x1).unwrap())
1071                    .unwrap();
1072                let min = m2
1073                    .pos()
1074                    .row_iter()
1075                    .map(|row| row.transpose().y)
1076                    .min_by(|x0, x1| x0.partial_cmp(x1).unwrap())
1077                    .unwrap();
1078                assert!((max - min).abs() < 1e-7);
1079            }
1080        }
1081    }
1082}