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})$:

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))
triangle

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.

continuous closing

(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.

reconstructed surface

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.

discrete geometry

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.
incorrect evaluation

(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.

correct evaluation resizing

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.
correct evaluation intersecting

The evaluation then looks as follows.

correct evaluation intersecting 2
TrixiParticles.WindingNumberHormannType
WindingNumberHormann()

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.

Experimental Implementation

This is an experimental feature and may change in any future releases.

source
TrixiParticles.WindingNumberJacobsonType
WindingNumberJacobson(; 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 by load_geometry and is only required when using hierarchical_winding=true.
  • hierarchical_winding: If set to true, 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 of 0.4 will give a better inside-outside segmentation.
Experimental Implementation

This is an experimental feature and may change in any future releases.

source

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.

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_geometryMethod
load_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 is Float64)
source

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:

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.

geometry
(1) Geometry representation
sdf
(2) Signed distances to the surface
boundary
(3) Boundary particles
interior
(4) Interior particles
initial_config
(5) Initial configuration
packed_config
(6) Packed configuration
TrixiParticles.SignedDistanceFieldType
SignedDistanceField(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 by load_geometry.
  • particle_spacing: Spacing between the particles.

Keywords

  • max_signed_distance: Maximum signed distance to be stored. That is, only particles with a distance of abs(max_signed_distance) to the surface of the shape will be sampled.
  • points: Points on which the signed distance is computed. When set to nothing (default), the bounding box of the shape will be sampled with a uniform grid of points.
  • use_for_boundary_packing: Set to true if [SignedDistanceField] is used to pack a boundary ParticlePackingSystem. Use the default of false when packing without a boundary.
source
TrixiParticles.ParticlePackingSystemType
ParticlePackingSystem(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

Keywords

  • background_pressure: Constant background pressure to physically pack the particles. A large background_pressure can cause high accelerations which requires a properly adjusted time step.
  • tlsph: With the TotalLagrangianSPHSystem, particles need to be placed on the boundary of the shape and not half a particle spacing away, as for fluids. When tlsph=true, particles will be placed on the boundary of the shape.
  • is_boundary: When shape is inside the geometry that was used to create signed_distance_field, set is_boundary=false. Otherwise (shape is the sampled boundary), set is_boundary=true. The thickness of the boundary is specified by creating signed_distance_field with: - use_for_boundary_packing=true - max_signed_distance=boundary_thickness See SignedDistanceField.
  • fixed_system: When set to true, the system remains static, meaning particles will not move and the InitialCondition will stay unchanged. This is useful when the system is packed together with another (non-fixed) ParticlePackingSystem. In this case, no SignedDistanceField is required for both the fixed and non-fixed system (use nothing 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. Set signed_distance_field=nothing when packing with a fixed system (see fixed_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 the SignedDistanceField information. The default is smoothing_length_interpolation = smoothing_length.
  • boundary_compress_factor: Factor to compress the boundary particles by reducing the boundary thickness by a factor of boundary_compress_factor. The default value is 1, 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 are 0.8 or 0.9.
source