Tag: Island Scan Strategy

Custom Island Scan Strategies for L-PBF/SLM using PySLM

The fact that most island scan strategies employed in SLM are nearly always square raised the question whether we could do more. I recently came across this ability to define ‘hexagon’ island regions advertised in the 2020 release of Autodesk Netfabb. Unfortunately this is a commercial tool and not always available. The practical reasons for implementing a hexagon island scanning strategy are largely unclear, but this prompted to create an example to illustrate how one would create custom island regions using PySLM. This in future could open some interesting ideas of tuning the scan strategy spatially across a layer.

Structural materials in cells - OpenLearn - Open University - T356_3
Honeycombs or heaxgonal lattices observed in nature are a popular structure used in composites engineering. Could the same be applied in Additive Manufacturing?

The user needs to customise the behaviour they desire by deriving subclasses from:

These classes serve the purpose for defining a ‘regular’ tessellated sub-region containing hatches. Regular regions that share the same shape characteristics for using the infill optimises the overall clipping performance outlined in the previous post.

PySLM: Checkerboard Island Scan Strategy Implementation used for L-PBF (Selective Laser Melting)
Illustration of Checkerboard Island Scan Strategy Implementation

Theoretically, we could build 2D unstructured cells e.g. Voronoi patterns, however, internally hatches for each region will require individual clipping and penalised with a significant performance hit during the hatching process.

Voronoi Diagram --
Example of a Voronoi diagram: regions are dibi based on the boundaries between.

The Island subclass region is the most important part to re-define the behavior. If we want to change the island regions to become regular tessellated polygons, the localBoundary method should be re-defined. In this example, it will generate a hexagon region, but the implementation below should be generic to cover other N-gon primitives:

   def localBoundary(self) -> np.ndarray:
    # Redefine the local boundary to be the hexagon shape

    if HexIsland._boundary is None:
        # Simple approach is to use a radius to define the overall island size
        #radius = np.sqrt(2*(self._islandWidth*0.5 + self._islandOverlap)**2)

        numPoints = 6

        radius = self._islandWidth / np.cos(np.pi/numPoints)  / 2 + self._islandOverlap

        print('island', radius, self._islandWidth)

        # Generate polygon island
        coords = np.zeros((numPoints+1, 2))

        for i in np.arange(0,numPoints):
            # Subtracting -0.5 orientates the polygon along its face
            angle = (i-0.5)/numPoints*2*np.pi
            coords[i] = [np.cos(angle), np.sin(angle)]

        # Close the polygon
        coords[-1] = coords[0]

        # Scale the polygon
        coords *= radius

        # Assign to the static class attribute
        HexIsland._boundary = coords

    return HexIsland._boundary

The polygon shape is defined by numPoints, so this can be changed to another polygon if desired. The polygon boundary is defined using a radius for the island region and from this a regular polygon is constructed on the outside. The polygon points are rotated by adjusting the start angle so there is a vertical edge on the RHS.

PySLM SLM Additive Manufacturing Scan Stragies: Hexagonal Island Tessellation
The Polygon is constructed around the island size (radius) and is orientated with the RHS edge vertically

This is generated once as a static class attribute, stored in _boundary to remove the overhead when generating the boundary.

The next step is to generate the internal hatch, which in this occasion needs to be clipped with the local boundary. First, the hatch vectors are generated covering the exterior region using the same radius as the polygon. This ensures that for any rotation transformation of the hatch vectors within the island are fully covered. This is relatively familiar to other code which generates these.

def generateInternalHatch(self, isOdd = True) -> np.ndarray:
    """
    Generates a set of hatches orthogonal to the island's coordinate system :math:`(x\\prime, y\\prime)`.

    :param isOdd: The chosen orientation of the hatching
    :return: (nx3) Set of sorted hatch coordinates
    """

    numPoints = 6

    radius = self._islandWidth / np.cos(np.pi / numPoints) / 2 + self._islandOverlap

    startX = -radius
    startY = -radius

    endX = radius
    endY = radius

    # Generate the basic hatch lines to fill the island region
    x = np.tile(np.arange(startX, endX, self._hatchDistance).reshape(-1, 1), 2).flatten()
    y = np.array([startY, endY])
    y = np.resize(y, x.shape)

    z = np.arange(0, y.shape[0] / 2, 0.5).astype(np.int64)

    coords =  np.hstack([x.reshape(-1, 1),
                            y.reshape(-1, 1),
                            z.reshape(-1,1)])

    # Toggle the hatch angle
    theta_h = np.deg2rad(90.0) if isOdd else np.deg2rad(0.0)

    # Create the 2D rotation matrix with an additional row, column to preserve the hatch order
    c, s = np.cos(theta_h), np.sin(theta_h)
    R = np.array([(c, -s, 0),
                  (s, c, 0),
                  (0, 0, 1.0)])

    # Apply the rotation matrix and translate to bounding box centre
    coords = np.matmul(R, coords.T).T

The next stage is to clip the hatch vectors with the local boundary. This is achieved using the static class method hatching.BaseHatcher.clipLines. The clipped hatches need to be sorted using the ‘z’ index or 2nd column of the clippedLines.

# Clip the hatch fill to the boundary
boundary = [[self.localBoundary()]]
clippedLines = np.array(hatching.BaseHatcher.clipLines(boundary, coords))

# Sort the hatches
clippedLines = clippedLines[:, :, :3]
id = np.argsort(clippedLines[:, 0, 2])
clippedLines = clippedLines[id, :, :]

# Convert to a flat 2D array of hatches and resort the indices
coordsUp = clippedLines.reshape(-1,3)
coordsUp[:,2] = np.arange(0, coordsUp.shape[0] / 2, 0.5).astype(np.int64)
return coordsUp

After sorting, the ‘z’ indexes need to the be condensed or flattened by re-building the ‘z’ index into sequential order. This is done to ensure when the hatches for islands are merged, we simply increment the index of the island using the length of the hatch array rather than performing np.max each time. This is later seen in the method hatching.IslandHatcher.hatch

# Generate the hatches for all the islands
idx = 0
for island in sortedIslands:

    # Generate the hatches for each island subregion
    coords = island.hatch()

    # Note for sorting later the order of the hatch vector is updated based on the sortedIsland
    coords[:, 2] += idx
    ...
    
    ...
    # 
    idx += coords.shape[0] / 2

clippedCoords = np.vstack(clippedCoords)
unclippedCoords = np.vstack(unclippedCoords).reshape(-1,2,3)

HexIslandHatcher

The final stage, is to re-implement hatching.IslandHatcher as a subclass. In this class, at a minimum, the generateIsland method needs to be redefined to correctly positioned the islands so that they tessellate correctly.

def generateIslands(self, paths, hatchAngle: float = 90.0):
    """
    Generate a series of tessellating Hex Islands to fill the region. For now this requires re-implementing because
    the boundaries of the island may be different shapes and require a specific placement in order to correctly
    tessellate within a region.
    """

    # Hatch angle
    theta_h = np.radians(hatchAngle)  # 'rad'

    # Get the bounding box of the boundary
    bbox = self.boundaryBoundingBox(paths)

    print('bounding box bbox', bbox)
    # Expand the bounding box
    bboxCentre = np.mean(bbox.reshape(2, 2), axis=0)

    # Calculates the diagonal length for which is the longest
    diagonal = bbox[2:] - bboxCentre
    bboxRadius = np.sqrt(diagonal.dot(diagonal))

    # Number of sides of the polygon island
    numPoints = 6

    # Construct a square which wraps the radius
    numIslandsX = int(2 * bboxRadius / self._islandWidth) + 1
    numIslandsY = int(2 * bboxRadius / ((self._islandWidth + self._islandOverlap) * np.sin(2*np.pi/numPoints)) )+ 1

The key difference here is defining the number of islands in the y-direction to account for the tessellation of the polygons. This is a simple geometry problem. The y-offset for the islands is simply the vertical component of the 2 x island radius at the angular increment to form the polygon.

Example of tesselation of hexagon islands

The HexIsland are generated with the offsets and appended to the list. These are then treat internally by the parent class IslandHatcher.

...

...

for i in np.arange(0, numIslandsX):
    for j in np.arange(0, numIslandsY):

        # gGenerate the island position
        startX = -bboxRadius + i * self._islandWidth + np.mod(j, 2) * self._islandWidth / 2
        startY = -bboxRadius + j * (self._islandWidth) * np.sin(2*np.pi/numPoints)

        pos = np.array([(startX, startY)])

        # Apply the rotation matrix and translate to bounding box centre
        pos = np.matmul(R, pos.T)
        pos = pos.T + bboxCentre

        # Generate a HexIsland and append to the island
        island = HexIsland(origin=pos, orientation=theta_h,
                            islandWidth=self._islandWidth, islandOverlap=self._islandOverlap,
                            hatchDistance=self._hatchDistance)

        island.posId = (i, j)
        island.id = id
        islands.append(island)

        id += 1

return islands

The island tessellation generated is shown below, with the an offset between islands applied by modifying the radius.

PySLM - Additive Manufacturing Library for Selective Laser Melting. The figure shows the generation of hexagonal hatch island regions.
Hexagon Island Boundaries generated across the entire region. The boundaries of the layer are shown, which are used for the intersection test.

The fully clipped scan strategy is shown below with the scanning ordered in the Y-direction.

PySLM - Additive Manufacturing Library for Selective Laser Melting. Figure shows the fully clipped hexagon islands in a custom island scan strategy
Hexagonal Island Scan Strategy: Consists of 5 mm Island (radius) with an offset at the boundaries of 0.1 mm.

Conclusions

This post illustrates how one can effectively decompose a layer region into a series of repeatable ‘island’ units which can be processed in an efficient manner, by only clipping hatches at boundary regions. This potentially has the ability to define spatially aware island regions; for example this could be redefining island sizes or parameters towards the boundary of a part. It could be used to alter the scan strategies within the region too, with the effect of changing the thermal behavior.

The full excerpt of the example can be found on github at examples/example_custom_island_hatcher.py.

Improving Performance of Island Checkerboard Scan Strategy Hatching in PySLM

The hatching performance of PySLM using ClipperLib via PyClipper is reasonably good considering the age of the library using the Vatti polygon clipping algorithm. Without attempting to optimise the underlying library and clipping algorithm for most scenarios, the hatch clipping process should be sufficient for most use case. Future investigation will explore alternative clipping algorithms to further improve the performance of this intensive computational process

For the unfamiliar with the basic hatching process of a single layer, the laser or electron beam (a 1D single point source) must scan across an aerial (2D) region. This is done by creating a series of lines/vectors which infill or raster across the surface.

The most basic form of hatch infill for bulk regions is an alternating, meander, or in some locales referred to a serpentine scan strategy. This tends to be undesirable in SLM due to the creation of localised heat build-up [1] resulting in porosity, poor surface finish [2], residual stress and resultant distortion and anisotropy due to preferential grain growth [3]. Stripe or Island scan strategies are employed in attempt to mitigate these by limiting the length of scan vectors used across a region [4][5][6]. Within the layer hatch vectors for each island are oriented orthogonal to each other and the scan vector length can be precisely controlled in order to reduce the magnitude of residual stresses generated [7].

However, when the user desires a stripe or an island scan strategy, the number of clipping operations for the individual hatch vectors increases drastically. The increase in number of clipping operations increases due to division of the area into fixed size regions corresponding to the desired scan vector length (typically 5 mm)]:

  • Standard Meander Scan Strategy: n_{clip} \propto \frac{A}{hatchDistance(h_d)}
  • Stripe Scan Strategy: n_{clip} \propto \frac{A}{StripeWidth}
  • Island Scan Strategy: n_{clip} \propto \frac{A}{IslandWidth^2}

As can be observed, the performance of hatching with an island scan strategy degrades rapidly when using the island scan due to reciprocal square. As a result, using a naive approach, hatching a very large planar region using an island scan strategy could quickly result in 100,000+ clipping operations for a single layer for a large flat. In addition, this is irrespective of the sparsity of the layer geometry. The way the hatch filling approach works in PySLM, the maximum extent of a contour/polygon region is found. A circle is projected based on this maximum extent, and an outer bounding box is covered. This is explained in a previous post.

The scan vectors are tiled across the region. The reason behind this is to guarantee complete coverage irrespective of the chosen hatch angle, \theta_h, across the layer and largely simplifies the computation. The issue is that many regions will be outside the boundary of the part. Sparse regions both void and solid will not require additional clipping.

The Proposed Technique:

In summary, the proposed technique takes advantage that each island is regular, and therefore each island can be used to discretise the region. This can be used to perform intersection tests for region that may be clipped, whilst recycling existing hatch vectors for those within the interior boundary.

Given that use an island scan strategy provides essentially structured grid, this can be easily transformed into a a method for selecting regions. Using the shapely library, each island boundary consisting of 4 edges can be quickly tested to check if it overlaps internally with the solid part and also intersected with the boundary. This is an efficient operation to perform, despite shapely (libGEOS) being not as efficient as PyClipper.

from shapely.geometry.polygon import LinearRing, Polygon

intersectIslands = []
overlapIslands = []

intersectIslandsSet = set()
overlapIslandsSet= set()

for i in range(len(islands)):
    
    island = islands[i]
    s = Polygon(LinearRing(island[:-1]))

    if poly.overlaps(s):
        overlapIslandsSet.add(i) # id
        overlapIslands.append(island)

    if poly.intersects(s):
        intersectIslandsSet.add(i)  # id
        intersectIslands.append(island)


# Perform difference between the python sets
unTouchedIslandSet = intersectIslandsSet-overlapIslandsSet
unTouchedIslands = [islands[i] for i in unTouchedIslandSet]

This library is used because the user may re-test the same polygon consecutively, unlike re-building the polygon state in ClipperLib. Ultimately, this presents three unique cases:

  1. Non-Intersecting (shapely.polygon.intersects(island) == False) – The Island resides outside of the boundary and is discarded,
  2. Intersecting (shapely.polygon.intersects(island) == True) – The Island is in an internal region, but may be also clipped by the boundary,
  3. Clipped (shapely.polygon.intersects(island) == True) – The island intersects with the boundary and requires clipping.

PySLM - Clipping of island regions when generating Island Scan Strategies for Selective Laser Melting
The result is shown here for a simple 200 mm square filled with 5 mm islands:

Taking the difference between cases 2) and 3), the islands with hatch scan vectors can be generated without requiring unnecessary clipping of the interior scan vectors. As a result this significantly reduces the computational effort required.

Although extreme, the previous example generated a total number of 2209 5 mm islands to cover the entire region. The breakdown of the island intersections are:

  1. Non-intersecting islands: 1591 (72%),
  2. Non-clipped islands: 419 (19%),
  3. Clipped islands: 199 (9%).

With respect to solid regions, the number of clipped islands account for 32% of the total area. The overall result is shown below. The total area of the hatch region that was hatched is 1.97 \times 10^3 \ mm^2, which is equivalent to a square length of 445 mm, significantly larger than what is capable on most commercial SLM systems. Using an island size of 5 mm with an 80 μm hatch spacing, the approximate hatching time is 6.5 s on a modest laptop system. For this example, 780 000 hatch vectors were generated.

PySLM - A close-up view showing the clipped scan vectors using the more efficient island scan strategy.
A close up view showing the 5mm Island Hatching with 0.8 mm Hatch Distance. Blue Lines show the overall path traversed by the laser beam. The total time taken for hatching was approximately 8 seconds.

The order of hatching scanned is shown by the blue lines, which trace the midpoints of the vectors. Hatches inside the island are scanned sequentially. The order of scanning in this case is chosen to go vertically upwards and then horizontally across using the in-built Python 3 sorting function with a lambda expression Remarkably, all performed using one line:

sortedIslands = sorted(islands, key=lambda island: (island.posId[0], island.posId[1]) )

A future post will elaborate further methods for sorting hatch vectors and island groups.

Comparison to Original Implementation:

The following is a non-scientific benchmark performed to illustrate the performance profile of the proposed method in PySLM.

Island Size [mm]Original Method Time [s]Proposed Method Time [s]
34665.3
52586.5
101217.9
20758.23
Approximate benchmark comparing Island Hatching Techniques in PySLM

It is clearly evident that the proposed method reduces the overall time by 1-2 orders for hatching a region. What is strange is that with the new proposed method, the overall time increases with the island size.

Generally it is expected that the number of clipping operations n_{clip} to be the following:

n_{clip} \propto \frac{Perimiter}{IslandWidth}

Potentially, this allows bespoke complex ‘sub-island’ scan strategies to be employed without a significant additional cost because scan vectors within un-clipped island regions can be very quickly replicated across the layer.

Other Benefits

The other benefits of taking approach is making a more modular object orientated approach for generating island based strategies, which don’t arbitrarily follow regular structured patterns. A future article will illustrate further explain the procedures for generating these.

The example can be seen and run in examples/example_island_hatcher.py in the Github repository.

References

References
1 Parry, L. A., Ashcroft, I. A., & Wildman, R. D. (2019). Geometrical effects on residual stress in selective laser melting. Additive Manufacturing, 25. https://doi.org/10.1016/j.addma.2018.09.026
2 Valente, E. H., Gundlach, C., Christiansen, T. L., & Somers, M. A. J. (2019). Effect of scanning strategy during selective laser melting on surface topography, porosity, and microstructure of additively manufactured Ti-6Al-4V. Applied Sciences (Switzerland), 9(24). https://doi.org/10.3390/app9245554
3, 4 Zhang, W., Tong, M., & Harrison, N. M. (2020). Scanning strategies effect on temperature, residual stress and deformation by multi-laser beam powder bed fusion manufacturing. Additive Manufacturing, 36(June), 101507. https://doi.org/10.1016/j.addma.2020.101507
5 Ali, H., Ghadbeigi, H., & Mumtaz, K. (2018). Effect of scanning strategies on residual stress and mechanical properties of Selective Laser Melted Ti6Al4V. Materials Science and Engineering A, 712(October 2017), 175–187. https://doi.org/10.1016/j.msea.2017.11.103
6 Robinson, J., Ashton, I., Fox, P., Jones, E., & Sutcliffe, C. (2018). Determination of the effect of scan strategy on residual stress in laser powder bed fusion additive manufacturing. Additive Manufacturing, 23(February), 13–24. https://doi.org/10.1016/j.addma.2018.07.001
7 Mercelis, P., & Kruth, J.-P. (2006). Residual stresses in selective laser sintering and selective laser melting. Rapid Prototyping Journal, 12(5), 254–265. https://doi.org/10.1108/13552540610707013