Sampling of Geometries
Generating the initial configuration of a simulation requires filling volumes (3D) or surfaces (2D) of complex geometries with particles. The algorithm to sample a complex geometry should be robust and fast, since for large problems (large numbers of particles) or complex geometries (many geometry faces), generating the initial configuration is not trivial and can be very expensive in terms of computational cost. We therefore use a winding number approach for an inside-outside segmentation of an object. The winding number $w(\mathbf{p})$ is a signed integer-valued function of a point $\mathbf{p}$ and is defined as
\[w(\mathbf{p}) = \frac{1}{2 \pi} \sum^n_{i=1} \Theta_i.\]
Here, $\Theta_i$ is the signed angle between $\mathbf{c}_i - \mathbf{p}$ and $\mathbf{c}_{i+1} - \mathbf{p}$ where $\mathbf{c}_i$ and $\mathbf{c}_{i+1}$ are two consecutive vertices on a curve. In 3D, we refer to the solid angle of an oriented triangle with respect to $\mathbf{p}$.
We provide the following methods to calculate $w(\mathbf{p})$:
- Hormann et al. (2001) evaluate the winding number combined with an even-odd rule, but only for 2D polygons (see WindingNumberHormann).
- Naive winding: Jacobson et al. (2013) generalized the winding number so that the algorithm can be applied for both 2D and 3D geometries (see WindingNumberJacobson).
- Hierarchical winding: Jacobson et al. (2013) also introduced a fast hierarchical evaluation of the winding number. For further information see the description below.
Hierarchical Winding
According to Jacobson et al. (2013) the winding number with respect to a polygon (2D) or triangle mesh (3D) is the sum of the winding numbers with respect to each edge (2D) or face (3D). We can show this with the following example in which we determine the winding number for each edge of a triangle separately and sum them up:
using TrixiParticles
using Plots
triangle = [125.0 375.0 250.0 125.0;
175.0 175.0 350.0 175.0]
# Delete all edges but one
edge1 = deleteat!(TrixiParticles.Polygon(triangle), [2, 3])
edge2 = deleteat!(TrixiParticles.Polygon(triangle), [1, 3])
edge3 = deleteat!(TrixiParticles.Polygon(triangle), [1, 2])
algorithm = WindingNumberJacobson()
grid = hcat(([x, y] for x in 1:500, y in 1:500)...)
_, w1 = algorithm(edge1, grid; store_winding_number=true)
_, w2 = algorithm(edge2, grid; store_winding_number=true)
_, w3 = algorithm(edge3, grid; store_winding_number=true)
w = w1 + w2 + w3
heatmap(1:500, 1:500, reshape(w1, 500, 500)', color=:coolwarm, showaxis=false,
tickfontsize=12, size=(570, 500), margin=6 * Plots.mm)
heatmap(1:500, 1:500, reshape(w2, 500, 500)', color=:coolwarm, showaxis=false,
tickfontsize=12, size=(570, 500), margin=6 * Plots.mm)
heatmap(1:500, 1:500, reshape(w3, 500, 500)', color=:coolwarm, showaxis=false,
tickfontsize=12, size=(570, 500), margin=6 * Plots.mm)
heatmap(1:500, 1:500, reshape(w, 500, 500)', color=:coolwarm, showaxis=false,
tickfontsize=12, size=(570, 500), margin=6 * Plots.mm, clims=(-1, 1))
This summation property has some interesting consequences that we can utilize for an efficient computation of the winding number. Let $\mathcal{S}$ be an open surface and $\bar{\mathcal{S}}$ an arbitrary closing surface, such that
\[\partial \bar{\mathcal{S}} = \partial \mathcal{S}\]
and $\mathcal{B} = \bar{\mathcal{S}} \cup \mathcal{S}$ is some closed oriented surface. For any query point $\mathbf{p}$ outside of $\mathcal{B}$, we know that
\[w_{\mathcal{S}}(\mathbf{p}) + w_{\bar{\mathcal{S}}}(\mathbf{p}) = w_{\mathcal{B}}(\mathbf{p}) = 0.\]
This means
\[w_{\mathcal{S}}(\mathbf{p}) = - w_{\bar{\mathcal{S}}}(\mathbf{p}),\]
regardless of how $\bar{\mathcal{S}}$ is constructed (as long as $\mathbf{p}$ is outside of $\mathcal{B}$).
We can use this property in the discrete case to efficiently compute the winding number of a query point by partitioning the polygon or mesh in a "small" part (as in consisting of a small number of edges/faces) and a "large" part. For the small part we just compute the winding number, and for the large part we construct a small closing and compute its winding number. The partitioning is based on a hierarchical construction of bounding boxes.
Bounding volume hierarchy
To efficiently find a "small part" and a "large part" as mentioned above, we construct a hierarchy of bounding boxes by starting with the whole domain and recursively splitting it in two equally sized boxes. The resulting hierarchy is a binary tree.
The algorithm by Jacobsen et al. (Algorithm 2, p. 5) traverses this binary tree recursively until we find the leaf in which the query point is located. The recursion stops with the following criteria:
- if the bounding box $T$ is a leaf then $T.\mathcal{S} = \mathcal{S} \cap T$, the part of $\mathcal{S}$ that lies inside $T$, is the "small part" mentioned above, so evaluate the winding number naively as $w(\mathbf{p}, T.\mathcal{S})$.
- else if $\mathbf{p}$ is outside $T$ then $T.\mathcal{S}$ is the "large part", so evaluate the winding number naively as $-w(\mathbf{p}, T.\bar{\mathcal{S}})$, where $T.\bar{\mathcal{S}}$ is the closing surface of $T.\mathcal{S}$.
Continuous example
Now consider the following continuous (not discretized to a polygon) 2D example. We compute the winding number of the point $\mathbf{p}$ with respect to $\mathcal{S}$ using the depicted hierarchy of bounding boxes.
(1):
- Recurse left: $w_{\text{left}} = \text{\texttt{hierarchical\_winding}} (\mathbf{p}, T.\text{left})$
- Recurse right: $w_{\text{right}} = \text{\texttt{hierarchical\_winding}} (\mathbf{p},T.\text{right})$
(2):
- Query point $\mathbf{p}$ is outside bounding box $T$, so don't recurse deeper.
- Compute $w_{\mathcal{S}}(\mathbf{p}) = - w_{\bar{\mathcal{S}}}(\mathbf{p})$ with the closure $T.\bar{\mathcal{S}}$, which is generally much smaller (fewer edges in the discrete version) than $T.\mathcal{S}$:
\[w_{\text{left}} = -\text{\texttt{naive\_winding}} (\mathbf{p}, T.\bar{\mathcal{S}})\]
(3):
- Bounding box $T$ is a leaf. Use open surface $T.\mathcal{S}$:
\[w_{\text{right}} = \text{\texttt{naive\_winding}} (\mathbf{p}, T.\mathcal{S})\]
The reconstructed surface will then look as in the following image.
We finally sum up the winding numbers
\[w = w_{\text{left}} + w_{\text{right} } = -w_{T_{\text{left}}.\bar{\mathcal{S}}} + w_{T_{\text{right}}.\mathcal{S}}\]
Discrete example
We will now go through the discrete version of the example above.
To construct the hierarchy for the discrete piecewise-linear example in (1), we have to do the following.
(2): Each edge is distributed to the child whose box contains the edge's barycenter (red dots in (2)). Splitting stops when the number of a box's edges slips below a threshold (usually $\approx 100$ faces in 3D, here: 6 edges).
(3): For the closure, Jacobson et al. (2013) define exterior vertices (exterior edges in 3D) as boundary vertices of such a segmentation (red dots in (3)). To find them, we traverse around each edge (face in 3D) in order, and increment or decrement for each vertex (edge) a specific counter.
v1 = edge_vertices_ids[edge][1]
v2 = edge_vertices_ids[edge][2]
vertex_count[v1] += 1
vertex_count[v2] -= 1
In 2D, a vertex is declared as exterior if vertex_count(vertex) != 0
, so there is not the same amount of edges in this box going into versus out of the vertex. To construct the closing surface, the exterior vertices are then connected to one arbitrary exterior vertex using appropriately oriented line segments:
edge = vertex_count[v] > 0 ? (closing_vertex, v) : (v, closing_vertex)
The resulting closed surface $T.S \cup T.\bar{S}$ then has the same number of edges going into and out of each vertex.
Incorrect evaluation
If we follow the algorithm, we know that recursion stops if
- the bounding box $T$ is a leaf or
- the query point $\mathbf{p}$ is outside the box.
(1): The query point $\mathbf{p}$ is outside the box, so we calculate the winding number with the (red) closure of the box.
(2): The query point $\mathbf{p}$ is inside the box, so we use the (blue) edges distributed to the box.
(3): In this case, it leads to an incorrect evaluation of the winding number. The query point is clearly inside the box, but not inside the reconstructed surface. This is because the property $w_{\mathcal{S}}(\mathbf{p}) = - w_{\bar{\mathcal{S}}}(\mathbf{p})$ only holds when $\mathbf{p}$ is outside of $\mathcal{B}$, which is not the case here.
Correct evaluation
Jacobson et al. (2013) don't mention this problem or provide a solution to it. We contacted the authors and found that they know about this problem and solve it by resizing the bounding box to fully include the closing surface of the neighboring box, since it doesn't matter if the boxes overlap.
To avoid resizing, we take a different approach and calculate the closure of the bounding box differently:
- Exclude intersecting edges in the calculation of the exterior vertices.
- This way, all exterior vertices are inside the bounding box, and so will be the closing surface.
- The intersecting edges are later added with flipped orientation, so that the closing is actually a closing of the exterior plus intersecting edges.
The evaluation then looks as follows.
TrixiParticles.WindingNumberHormann
— TypeWindingNumberHormann()
Algorithm for inside-outside segmentation of a complex geometry proposed by Hormann (2001). It is only supported for 2D geometries. WindingNumberHormann
might handle edge cases a bit better, since the winding number is an integer value.
This is an experimental feature and may change in any future releases.
TrixiParticles.WindingNumberJacobson
— TypeWindingNumberJacobson(; geometry=nothing, winding_number_factor=sqrt(eps()),
hierarchical_winding=false)
Algorithm for inside-outside segmentation of a complex geometry proposed by [2].
Keywords
geometry
: Complex geometry returned byload_geometry
and is only required when usinghierarchical_winding=true
.hierarchical_winding
: If set totrue
, an optimized hierarchical approach will be used, which gives a significant speedup. For further information see Hierarchical Winding.winding_number_factor
: For leaky geometries, a factor of0.4
will give a better inside-outside segmentation.
This is an experimental feature and may change in any future releases.
TrixiParticles.load_geometry
— Methodload_geometry(filename; element_type=Float64)
Load file and return corresponding type for ComplexShape
. Supported file formats are .stl
and .asc
.
Arguments
filename
: Name of the file to be loaded.
Keywords
element_type
: Element type (default isFloat64
)
Particle Packing
TODO
TrixiParticles.SignedDistanceField
— TypeSignedDistanceField(geometry, particle_spacing;
points=nothing,
max_signed_distance=4 * particle_spacing,
use_for_boundary_packing=false)
Generate particles along a surface of a complex geometry storing the signed distances and normals to this surface.
Arguments
geometry
: Geometry returned byload_geometry
.particle_spacing
: Spacing between the particles.
Keywords
max_signed_distance
: Maximum signed distance to be stored. That is, only particles with a distance ofabs(max_signed_distance)
to the surface of the shape will be sampled.points
: Points on which the signed distance is computed. When set tonothing
(default), the bounding box of the shape will be sampled with a uniform grid of points.use_for_boundary_packing
: Set totrue
if [SignedDistanceField
] is used to pack a boundaryParticlePackingSystem
. Use the default offalse
when packing without a boundary.
TrixiParticles.ParticlePackingSystem
— TypeParticlePackingSystem(shape::InitialCondition;
signed_distance_field::SignedDistanceField,
smoothing_kernel=SchoenbergCubicSplineKernel{ndims(shape)}(),
smoothing_length=1.2 * shape.particle_spacing,
is_boundary=false, boundary_compress_factor=1.0,
neighborhood_search=GridNeighborhoodSearch{ndims(shape)}(),
background_pressure, tlsph=true)
System to generate body-fitted particles for complex shapes. For more information on the methods, see description below.
Arguments
shape
:InitialCondition
to be packed.
Keywords
background_pressure
: Constant background pressure to physically pack the particles. A largebackground_pressure
can cause high accelerations which requires a properly adjusted time step.tlsph
: With theTotalLagrangianSPHSystem
, particles need to be placed on the boundary of the shape and not half a particle spacing away, as for fluids. Whentlsph=true
, particles will be placed on the boundary of the shape.is_boundary
: Whenshape
is inside the geometry that was used to createsigned_distance_field, set
isboundary=false. Otherwise (
shapeis the sampled boundary), set
isboundary=true. The thickness of the boundary is specified by creating
signeddistancefieldwith: -
useforboundarypacking=true-
maxsigneddistance=boundarythicknessSee [
SignedDistanceField`](@ref).signed_distance_field
: To constrain particles onto the surface, the information about the signed distance from a particle to a face is required. The precalculated signed distances will be interpolated to each particle during the packing procedure.smoothing_kernel
: Smoothing kernel to be used for this system. See Smoothing Kernels.smoothing_length
: Smoothing length to be used for this system. See Smoothing Kernels.