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.
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.
Read geometries from file
Geometries can be imported using the load_geometry
function.
- For 3D geometries, we support the binary (
.stl
) format. - For 2D geometries, the recommended format is DXF (
.dxf
), with optional support for a simple ASCII (.asc
) format.
ASCII Format (.asc)
An .asc file contains a list of 2D coordinates, space-delimited, one point per line, where the first column are the x values and the second the y values. For example:
# ASCII
0.0 0.0
1.0 0.0
1.0 1.0
0.0 1.0
It is the user’s responsibility to ensure the points are ordered correctly. This format is easy to generate and inspect manually.
DXF Format (.dxf) – recommended
The DXF (Drawing Exchange Format) is a widely-used CAD format for 2D and 3D data. We recommend this format for defining 2D polygonal. Only DXF entities of type LINE
or POLYLINE
are supported. To create DXF files from scratch, we recommend using the open-source tool FreeCAD. For a less technical approach, we recommend using Inkscape to create SVG files and then export them as DXF.
Creating DXF Files with FreeCAD
- Open FreeCAD and create a new document.
- Switch to the Sketcher workbench and create a new sketch.
- Choose the XY-plane orientation and draw your geometry.
- Select the object to be exported.
- Go to "File > Export..." and choose "AutoDesk DXF (*.dxf)" as the format.
- Ensure the following Import-Export options are enabled:
- "Use legacy Python exporter".
- "Project exported objects along current view direction".
Creating DXF Files with Inkscape
SVG vector graphics can also be used as a basis for geometry. Use Inkscape to open or create the SVG. You can simply draw a Bezier curve by pressing "B" on your keyboard. We reommend the mode "Create spiro paths" for a smooth curve. Select the relevant path and export it as DXF:
- Go to "File > Save As...".
- Choose "Desktop Cutting Plotter (AutoCAD DXF R12)(*.dxf)" format.
TrixiParticles.load_geometry
— Methodload_geometry(filename; element_type=Float64)
Load file and return corresponding type for ComplexShape
. Supported file formats are .stl
, .asc
and dxf
. For comprehensive information about the supported file formats, refer to the documentation at Read geometries from file.
Arguments
filename
: Name of the file to be loaded.
Keywords
element_type
: Element type (default isFloat64
)
Particle Packing
To obtain a body-fitted and isotropic particle distribution, an initial configuration (see Sampling of Geometries) is first generated. This configuration is then packed using a ParticlePackingSystem
. The preprocessing pipeline consists of the following steps:
- Load geometry: Fig. 1,
load_geometry
. - Compute the signed distance field (SDF): Fig. 2,
SignedDistanceField
. - Generate boundary particles: Fig. 3,
sample_boundary
. - Initial sampling of the interior particles with inside-outside segmentation: Fig. 4,
ComplexShape
. - Pack the initial configuration of interior and boundary particles (Fig. 5): Fig. 6,
ParticlePackingSystem
.
The input data can either be a 3D triangulated surface mesh represented in STL format or a 2D polygonal traversal of the geometry (see load_geometry
). The second step involves generating the SDF (see SignedDistanceField
), which is necessary for the final packing step as it requires a surface detection. The SDF is illustrated in Fig. 2, where the distances to the surface of the geometry are visualized as a color map. As shown, the SDF is computed only within a narrow band around the geometry’s surface, enabling a face-based neighborhood search (NHS) to be used exclusively during this step. In the third step, the initial configuration of the boundary particles is generated (orange particles in Fig. 3). Boundary particles are created by copying the positions of SDF points located outside the geometry but within a predefined boundary thickness (see sample_boundary
). In the fourth step, the initial configuration of the interior particles (green particles in Fig. 4) is generated using the hierarchical winding number approach (see Hierarchical Winding). After steps 1 through 4, the initial configuration of both interior and boundary particles is obtained, as illustrated in Fig. 5. The interface of the geometry surface is not well resolved with the initial particle configuration. Thus, in the final step, a packing algorithm by Zhu et al. [3] is applied utilizing the SDF to simultaneously optimize the positions of both interior and boundary particles, yielding an isotropic distribution while accurately preserving the geometry surface, as illustrated in Fig. 6.
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::Union{SignedDistanceField, Nothing},
smoothing_kernel=SchoenbergQuinticSplineKernel{ndims(shape)}(),
smoothing_length=shape.particle_spacing,
smoothing_length_interpolation=smoothing_length,
is_boundary=false, boundary_compress_factor=1,
neighborhood_search=GridNeighborhoodSearch{ndims(shape)}(),
background_pressure, tlsph=false, fixed_system=false)
System to generate body-fitted particles for complex shapes. For more information on the methods, see particle packing.
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
, setis_boundary=false
. Otherwise (shape
is the sampled boundary), setis_boundary=true
. The thickness of the boundary is specified by creatingsigned_distance_field
with: -use_for_boundary_packing=true
-max_signed_distance=boundary_thickness
SeeSignedDistanceField
.fixed_system
: When set totrue
, the system remains static, meaning particles will not move and theInitialCondition
will stay unchanged. This is useful when the system is packed together with another (non-fixed)ParticlePackingSystem
. In this case, noSignedDistanceField
is required for both the fixed and non-fixed system (usenothing
as signed distance field).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. Setsigned_distance_field=nothing
when packing with a fixed system (seefixed_system
description above).smoothing_kernel
: Smoothing kernel to be used for this system. See Smoothing Kernels.smoothing_length
: Smoothing length to be used for the gradient estimation. See Smoothing Kernels.smoothing_length_interpolation
: Smoothing length to be used for interpolating theSignedDistanceField
information. The default issmoothing_length_interpolation = smoothing_length
.boundary_compress_factor
: Factor to compress the boundary particles by reducing the boundary thickness by a factor ofboundary_compress_factor
. The default value is1
, which means no compression. Compression can be useful for highly convex geometries, where the boundary volume increases significantly while the mass of the boundary particles remains constant. Recommended values are0.8
or0.9
.