This is, essentially, the core module of FINDS. This is where the calculations
that govern the mechanics of the particle simulation occur, and this is written
in accordance with the prior work of Mohamed Niged Mabrouk and Dr. Daniel
Floryan’s past work with the far-field model of swimmers
[3, 4].
The core goal of this module is the computation of the time-derivative of
the system matrix \(\mathbf{X}\). Using this calculation, numerical
integration schemes are implemented under the ‘simulation’ module to allow for
time-stepping.
The following is a simple definition of the far-field model as stated in these
papers alongside descriptions of how this model was implemented in the program
[1].
Each fish-particle consists of two fundamental variables, a center-of-mass
position \(\mathbf{x}_c\) and an orientation unit vector \(\hat{n}\),
and the fish can be represented in full as an array or tuple that stores both
of these variables, \(\mathbf{s} = (\mathbf{x}_c, \hat{n})\), and the
system of swimmers \(\mathbf{X}\) can be defined as a matrix consisting of
each of these fish vectors as rows. Thus, for a system of size \(N\),
the system matrix is \(N \times 6\) or of shape \((N,6)\).
The fundamental approximation of the far-field model is treating fish as
self-propelled dipoles moving in free space, with the head of the fish being
the source and the tail of the fish being the sink. The head and the tail for
each fish are separated \(\ell\) apart, or the length of the fish.
Mathematically, these two ends can differ only up to a sign (so the head and
tail also form positive and negative ends respectively) due to the conservation
of mass of the fluid (the two ends must cancel each other out). Due to this
duality, the sources and sinks can be considered more generally as “features”
of a fish.
We store all of the fish in the system in a matrix \(\mathbf{X}\) where
each fish vector comprises a row, and this is called the system (or state)
matrix.
The core equation governing the motion of bodies within the simulation is the
induced velocity for a particle a displacement \(\mathbf{r}\) away from
a feature (some source or sink of a fish),
where \(\sigma\) is the volumetric flow rate of the source or sink.
Sources produce outward velocities as they repel bodies outward and sinks
produce inward velocities as they attract bodies toward them.
In essence, this equation is used to develop the differential time-derivative
of the full state, as the velocity for a single feature is defined as the
total velocity contribution from all other features in the system, and the
feature velocity and orientation of a fish define the velocity of its
orientation.
This idea leads to the feature velocity equation
(5)¶\[\mathbf{v}_{\alpha,i} = U \hat{n}_i + \frac{\sigma}{4\pi}
\mathbf{h}_{\alpha,i}\]
(see finds.calculations.calculate_feature_velocities())
that computes the velocity of some feature \(\alpha\) (which would be
either the front or back) for a fish. This is comprised of two terms, an
internal contribution to the velocity resulting from the interaction between
this feature and its polar opposite on the same fish (also known as the
self-propelled velocity) defined as
and the external contribution term that encodes the total velocity
contribution from all features (sources and sinks) external to this fish,
where the vectors \(\mathbf{h}\) and \(\mathbf{c}\) are defined to
represent the value of the original term \(\mathbf{r}/r^3\), which I call
the “interaction” between features.
In this context, \(\mathbf{h}_{\alpha,i}\) refers to the total interaction
between feature \(\alpha\) of fish \(i\) with all of the other features
(barring its polar opposite on fish \(i\)) within the greater system
\(\mathbf{X}\),
Furthermore, \(\mathbf{h}_{\alpha,ij}\) refers to the total interaction
between feature \(\alpha\) of fish \(i\) and both features of fish
\(j\) (both the front and the back), and this is always defined as the
individual interaction to the front of fish \(j\) minus the individual
interaction to the back of fish \(j\) due to the aforementioned sign
change between sources and sinks,
(see finds.calculations.calculate_fish_interaction())
Lastly, \(\mathbf{c}_{\alpha \beta}\) is defined as the individual
interaction between feature \(\alpha\) of fish \(i\) and feature
\(\beta\) of fish \(j\) within \(\mathbf{X}\), and this is
defined simply as
(see finds.calculations.calculate_system_derivative())
and equivalently, \(\dot{\mathbf{x}}_c\) is the translational velocity and
\(\frac{d \hat{n}}{dt}\) is the rotational velocity of the fish. The
translational velocity for a fish is just the average of the front and back
velocities,
The Barnes-Hut algorithm simplifies calculating the interaction vectors by
clustering fish that are sufficiently “far away” from a given fish (as
determined by the Barnes-Hut ratio \(\theta\)). A complete, interactive
description of the Barnes-Hut algorithm can be found online
[2].
This begins by constructing an Octree that partitions the three-dimensional
space around the origin. The fish-particles are inserted in list order, and for
each additional point, the Octree expands by further subdividing the three-
dimensional space. Then, once the Octree is fully built, each node of the tree
(essentially representing every possible division of the three-dimensional
space) is “clustered,” meaning that several of the fish-particles are computed
into a fish-particle representing the average of all of them.
For example, if we have fish \(\mathbf{X}_i\) and fish \(\mathbf{X}_j\)
in \(\mathbf{X}\), their clustered form would simply be the average of the
two, \((\mathbf{X}_i + \mathbf{X}_j)/2\). Then, we traverse this tree for
each fish. If the node is a leaf, then we automatically compute the
interaction. For each branch node in the octree, we then compute a size-to-
distance ratio \(\phi\). This is computed as the fraction of the “size” or
the side length of the cube comprising the subdivision over the distance from
the center-of-mass of the subdivision (or the position of the cluster).
For example, if we have a subdivision storing fish 1 through \(k\)
clustered into a cluster-fish with position \(\mathbf{x}_{c}\) with
a side length of size \(l\), then the Barnes-Hut ratio with respect
to a fish at position \(\mathbf{x}_{c0}\) would be
Once we’ve calculated \(\phi\) for the given node, if \(\phi \ge
\theta\), then we continue travering the tree to the children of the node. If
\(\phi < \theta\), then we compute the interaction.
Computes the index of the corresponding octant that a particle located
at position would be found in relative to the center of this
octant.
Parameters:
position (NDArray) – The position of the object we’re comparing against
the center of this octant.
Returns:
The integer index corresponding to the octant the position
belongs in, between 0 to 7.
Return type:
int
The logic behind this is simple, and follows binary. Given the center
position \(\mathbf{r} = \langle r_x, r_y, r_z \rangle\) of this
octant and the comparison point \(\mathbf{p} = \langle p_x, p_y
p_z \rangle\), we loop through each of the dimensions \(i\). For
each dimension, we evaluate whether or not \(p_i > r_i\), and if
so, we set the binary digit at that index to 1. Otherwise, that digit
is set to 0. For brevity, rather than computing directly in binary,
this same effect can be produced by adding \(2^i\) if \(p_i >
r_i\) is true.
Each of the octants map in the following manner:
Octant
Binary
Index
-x,-y,-z
000
0
+x,-y,-z
001
1
-x,+y,-z
010
2
+x,+y,-z
011
3
-x,-y,+z
100
4
+x,-y,+z
101
5
-x,+y,+z
110
6
+x,+y,+z
111
7
For example, given a center point \(\mathbf{r} = \langle 0, 0, 0
\rangle\) and a test point \(\mathbf{p} = \langle 0, -1,
1 \rangle\), the predicate evaluated on each index produces a vector
\(\langle \text{False}, \text{False}, \text{True} \rangle\), which
corresponds to the binary string 100 (as binary is built in reverse)
where 1 is True and 0 is False. From there, the base 10 representation
of 100 is 4, so therefore, \(\mathbf{p}\) would be said to lie in
octant 4, which corresponds to the –+ octant.
Computes the center position of a corresponding child octant relative
to the center of this parent octant.
Parameters:
child_octant_index (int) – The octant index of the child octant.
Returns:
The center position of the child octant.
Return type:
NDArray
First, the offset from the parent center position is calculated as the
side length for this octant divided by four [2]. Then, the child’s
octant index can be reinterpreted as binary and looped through for
each dimension. For each dimension, if the predicate bit corresponding
to that dimension is true [3], then it sets the offset vector for that
dimension to have the positive offset. Otherwise, it sets the offset
vector at that dimension to have the negative offset.
For example, if the side length of this octant is \(\ell\), and
we want to compute the center of an octant with index
Inserts a new fish into the child nodes of this octant.
Parameters:
fish (NDArray) – The new fish to insert.
Given a new fish \(\mathbf{s} = (\mathbf{x}_c, \mathbf{n})\),
we first compute the child octant index \(o\) for the fish based
on its position relative to the center of this octant. If the child
octant has not been initialized yet (i.e., there is no data presently
there), then we initialize a new octree node for that octant with half
the side length of this node and an offset center. Then, we insert the
fish into that octant.
This follows the Barnes-Hut hierarchical tree generation algorithm as
written from Ventimiglia and Wayne [5].
Constructing the Barnes-Hut tree
To construct the Barnes-Hut tree, insert the bodies one after
another. To insert a body b into the tree rooted at node x, use
the following recursive procedure:
If node x does not contain a body, put the new body b here.
If node x is an internal node, update the center-of-mass and
total mass of x. Recursively insert the body b in the appropriate
quadrant.
If node x is an external node, say containing a body named c, then
there are two bodies b and c in the same region. Subdivide the
region further by creating four children. Then, recursively insert
both b and c into the appropriate quadrant(s). Since and c may
still end up in the same quadrant, there may be several
subdivisions during a single insertion. Finally, update the
center-of-mass and total mass of x.
Recursively computes the interaction of the fish at fish_pos
with all of the fish (and clustered fish) within this tree.
Parameters:
fish_pos (NDArray) – The position of the reference fish we’re computing
the interaction with.
fish_front (NDArray) – The position of the front of the reference fish.
fish_back (NDArray) – The position of the back of the reference fish.
minimum_ratio (float) – The Barnes-Hut Ratio \(\theta\), or the
minimum ratio \(s/d < \theta\) of side length \(s\) to
the distance from the reference fish to the center of the octant
\(d\) to compute the interaction at this node rather than
summing the interaction at the child nodes via recursion.
Returns:
A 1-dimensional array of 6 values containing the front and
back interaction for the reference fish.
This function builds an Octree out of a system matrix in three steps.
First, it determines the longest distance along any given axis between
two fish within the system as the maximum between the differences of the
maximums and minimums on each axis.
the minimums for each dimension would be \((1,0,3)\) and the maximums
would be \((11,8,8)\), making the differences \((10,8,5)\) and the
longest distance would be 10.
Then, this value (increased slightly by \(0.1\%\)) is used as the side
length for the root cube of the Octree, and the position of the center is
defined as half of the difference vector (which would be \((5,4,2.5)\)
in the previous example).
Then, each of the fish in the system are inserted sequentially into the
Octree using OctreeNode.insert_data(), and once all of the fish
are inserted into the tree (and all of the nodes are clustered), all of
the feature positions are calculated for the tree with
OctreeNode.calculate_feature_positions().
Computes the individual interaction vector between feature
\(\alpha\) of fish \(i\) and feature \(\beta\) of
fish \(j\), defined as the displacement between their positions
divided by the cube of its norm: