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.