Quickly Switch between Blocks

Quickly Switch between Blocks

This guide explains how multiple building blocks, serving the same purpose can be used within one Agent thus allowing to experiment and compare two approaches. We will explain the procedures along the example of the Interaction concept.

Frequently switching between building blocks is often necessary when evaluating which model fits well for the system at hand. cellular_raza makes extensive use of compile-time generics for which it is important to know what types we are dealing with. This means that we are left with two fundamental differing options:

  1. Preserve performance but restrict ourselves to a predefined set of blocks
  2. Allow maximum Flexibility at the expense of computational overhead

Select Set of Blocks

Identical Signatures

For this example, we assume that all types for position, velocity and force are identical. The type for exchanging interaction information may however be distinct. This means that we can implement the Interaction trait by using 2 generic parameters <T, I> and implementing Interaction<T, T, T, I>.

cellular_raza-examples/homepage-training/src/building_blocks/switching.rs
1
2
3
4
5
6
7
8
use cellular_raza::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Deserialize, Serialize)]
pub enum RadiusBasedInteraction {
    Morse(MorsePotential),
    Mie(MiePotential),
}
cellular_raza-examples/homepage-training/src/building_blocks/switching.rs
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
impl<T, I> Interaction<T, T, T, I> for RadiusBasedInteraction
where
    MorsePotential: Interaction<T, T, T, I>,
    MiePotential: Interaction<T, T, T, I>,
{
    fn calculate_force_between(
        &self,
        own_pos: &T,
        own_vel: &T,
        ext_pos: &T,
        ext_vel: &T,
        inf: &I,
    ) -> Result<(T, T), CalcError> {
        use RadiusBasedInteraction::*;
        match self {
            Morse(pot) => pot.calculate_force_between(
                own_pos, own_vel, ext_pos, ext_vel, inf,
            ),
            Mie(pot) => pot.calculate_force_between(
                own_pos, own_vel, ext_pos, ext_vel, inf,
            ),
        }
    }

    fn get_interaction_information(&self) -> I {
        use RadiusBasedInteraction::*;
        match self {
            Morse(pot) => {
                <MorsePotential as Interaction<T, T, T, I>>::
                    get_interaction_information(&pot)
            }
            Mie(pot) => {
                <MiePotential as Interaction<T, T, T, I>>::
                    get_interaction_information(&pot)
            }
        }
    }
}
⚠️
This implementation assumes that the transferred information can be interpreted vice-versa.

We assume that that the interaction information behind the generic parameter I has the same type and interpretation for both interaction variants. For example, the MiePotential and the MorsePotential both exchange the radius variable and are thus compatible with each other. If one of those types would exchange the diameter instead of the radius, the implementation would still work but the interpretation of the exchanged information would not be identical and thus wrong results would be calculated.

Differing Information

In this section we will deal with two interaction types that have different information types I1, I2.

cellular_raza-examples/homepage-training/src/building_blocks/switching.rs
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#[derive(Clone, Deserialize, Serialize)]
pub enum SphericalInteraction {
    Morse(MorsePotential),
    BLJ(BoundLennardJones),
}

#[derive(Clone)]
pub enum IInf<I1, I2> {
    Morse(I1),
    BLJ(I2),
}

impl<T, I1, I2> Interaction<T, T, T, IInf<I1, I2>> for SphericalInteraction
where
    MorsePotential: Interaction<T, T, T, I1>,
    BoundLennardJones: Interaction<T, T, T, I2>,
{
    fn calculate_force_between(
        &self,
        own_pos: &T,
        own_vel: &T,
        ext_pos: &T,
        ext_vel: &T,
        ext_info: &IInf<I1, I2>,
    ) -> Result<(T, T), CalcError> {
        use SphericalInteraction::*;
        match (self, ext_info) {
            (Morse(pot), IInf::Morse(inf)) => pot.calculate_force_between(
                own_pos, own_vel, ext_pos, ext_vel, inf,
            ),
            (BLJ(pot), IInf::BLJ(inf)) => pot.calculate_force_between(
                own_pos, own_vel, ext_pos, ext_vel, inf,
            ),
            _ => Err(CalcError(format!(
                "interaction potential and obtained\
                information did not match"
            ))),
        }
    }

    fn get_interaction_information(&self) -> IInf<I1, I2> {
        use SphericalInteraction::*;
        match self {
            Morse(pot) => IInf::Morse(pot.get_interaction_information()),
            // In this case, the BLJ potential returns ().
            // Thus this is equivalent to
            // BLJ(_) => (),
            BLJ(pot) => IInf::BLJ(pot.get_interaction_information()),
        }
    }
}
⚠️

We assume here that we pick one of the two variants in the beginning and thus initialize all agents with this variant. In the case where multiple variants can be chosen simultaneously, we would have to specify the two conditions

Sketched Implementation
fn calculate_force_between(
    ...,
) -> Result<(T, T), CalcError> {
    use SphericalInteraction::*;
    match (self, ext_info) {
        // See as above
        ... ,
        // Also implement functionality here
        (BLJ(pot), IInf::Morse(inf)) => ...,
        (Morse(pot), IInf::BLJ(inf)) => ...,
    }
}

Differing Position, Velocity, Force

We assume that the position, velocity and force types are distinct but shared between the different interaction types.

cellular_raza-examples/homepage-training/src/building_blocks/switching.rs
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#[allow(unused)]
#[derive(Clone)]
enum SphericalInteraction2 {
    Morse(MorsePotential),
    Mie(MiePotential),
    BLJ(BoundLennardJones),
}

enum IInf2<I1, I2, I3> {
    Morse(I1),
    Mie(I2),
    BLJ(I3),
}

impl<Pos, Vel, For, I1, I2, I3> Interaction<Pos, Vel, For, IInf2<I1, I2, I3>>
    for SphericalInteraction2
where
    MorsePotential: Interaction<Pos, Vel, For, I1>,
    MiePotential: Interaction<Pos, Vel, For, I2>,
    BoundLennardJones: Interaction<Pos, Vel, For, I3>,
{
    fn get_interaction_information(&self) -> IInf2<I1, I2, I3> {
        match self {
            SphericalInteraction2::Morse(pot) => {
                IInf2::Morse(pot.get_interaction_information())
            }
            SphericalInteraction2::Mie(pot) => {
                IInf2::Mie(pot.get_interaction_information())
            }
            SphericalInteraction2::BLJ(pot) => {
                IInf2::BLJ(pot.get_interaction_information())
            }
        }
    }

    fn calculate_force_between(
        &self,
        own_pos: &Pos,
        own_vel: &Vel,
        ext_pos: &Pos,
        ext_vel: &Vel,
        ext_info: &IInf2<I1, I2, I3>,
    ) -> Result<(For, For), CalcError> {
        match (self, ext_info) {
            (SphericalInteraction2::Morse(pot), IInf2::Morse(ext_info)) => pot
                .calculate_force_between(
                    own_pos, own_vel, ext_pos, ext_vel, ext_info,
                ),
            (SphericalInteraction2::Mie(pot), IInf2::Mie(ext_info)) => pot
                .calculate_force_between(
                    own_pos, own_vel, ext_pos, ext_vel, ext_info,
                ),
            (SphericalInteraction2::BLJ(pot), IInf2::BLJ(ext_info)) => pot
                .calculate_force_between(
                    own_pos, own_vel, ext_pos, ext_vel, ext_info,
                ),
            _ => Err(CalcError(format!(
                "interaction type and information type are not matching"
            ))),
        }
    }
}

In the case where each interaction potential has its own position, velocity or force type, we need to also construct enums for position enum PPos<P1, P2, ..> {..} just as we did for the interaction information IInf<I1, I2, ..> {..}. Additionally, we will now have to also implement the Xapy trait for these types in order to be able to numerically use them. We do not in general recommend this approach and would advise to consider to try in using distinct information types instead.

⚠️
The same warning about mixing variants as in the previous section applies here.