Tag: SLM

Custom Scan Strategies in SLM / L-PBF with PySLM: Sinusoidal Scanning

Building upon the previous post that provided a detailed breakdown for creating custom island scan strategies, this further post documents a method for deploying custom ‘hatch’ infills. This is particularly desirable capability sought by researchers and has been touched upon very little in the current research. The use of unit-cell infills or in particular fractal filling curves such as the Hilbert curve have been sought for better controlling the thermal history and melt pool stability of hatch infills.

This has been previously explored in SLS [1]][2] and in SLM on a previous collaborators at the University of Nottingham investigating Fractal scanning strategy [3][4].

Typically, hatch infills are sequences of linear lines that form the the ‘hatch’ pattern. Practically, these are very efficient mechanism for infilling a 2D area by using 1D line elements when rastering a laser. Clipping of lines within polygons is intuitive. As discussed there are various scan strategies that can be employed to generate variations on this infill – i.e. stripe, checkerboard/island scan strategy and also modifying the order or sorting of the hatch vectors.

Geometrical scan strategies that adapt the infill based on the underlying geometry, i.e. lattices are acknowledges as ways for drastically improving the performance and the quality of these characteristic structures. This would be based on some medial-axis approach. This post will not specifically delve into this, rather, demonstrate an approach for custom infills on bulk regions.

Ultimately, drastically changing the behavior of the underlying hatch infill has not really been explored. This post will demonstrate an example that could be employed and explored as part of future research.

Custom Sinusoidal Approach

Sinusoidal scanning has been employed in welding research [5] and also in direct energy deposition (DED) [6][7][8] in order to improve the stability and quality of the joining or manufacturing process.

The process of generating this particular scan strategy requires some careful thought to improve the efficiency of the generation, especially given the overall increase in number of points require to essentially ‘sample’ across the sin curve.

The implementation requires subclassing the Hatcher class, by re-implementing the BaseHatcher.generateHatching and the BaseHatcher.hatch methods.

Unlike, the normal hatch vectors, the sinusoidal pattern has to be treated as a series of connected line segments, without any jumping. This requires using the ContourGeometry representation to efficiently store the discretised curve. As a result, the Hatcher.hatch method has to be re-implemented to take account of this.

The procedure builds upon previous methods to define customer behavior (see previous post). The first steps are to define a local coordinate system x' and y' for generating the individual sin curve. A sine curve y' = A \sin(k x') is generated to fill the region bounding box accordingly, given a frequency and amplitude parameter along x'.

The number of points used to discretise the sine curve is determined by \delta x. This needs to be chosen to suit the parameters for the periodicity and amplitude of the sine curve. A reasonable compromise is require as this will severely impact both the performance of clipping these curves, but also the overall file size of the build file generated.

dx = self._discretisation # num points per mm
numPoints = 2*bboxRadius * dx

x = np.arange(-bboxRadius, bboxRadius, hatchSpacing, dtype=np.float32).reshape(-1, 1)
hatches = x.copy()

Generate the sinusoidal curve along the local coordinate system x' and y'. These will be later tiled and then
transformed across the entire coordinate space.
xDash = np.linspace(-bboxRadius, bboxRadius, int(numPoints))
yDash = self._amplitude * np.sin(2.0*np.pi * self._frequency * xDash)

We replicate and transform the sine curve along adjacent paths and transform along the y-direction
y = np.tile(yDash, [x.shape[0], 1])
y += x

x = np.tile(xDash, [x.shape[0],1]).flatten()
y = y.ravel()

After generating single sine curve, numpy.tile is used to efficiently replicate the curve to fill the entire bounding box region. Each curve is then translated by an increment defined by x, to represent the effective hatch spacing or hatch distance.

The next important step is to define the sort order for scanning these. This is slightly different, in that the sort order is done per line segment used to discretise the curve. This is subtle, but very important because this ensures that the curves when clipped by the slice boundary are scanned in the same prescribed sequential order.

The increment of 1\times10^5 is used in order to potentially differentiate each curve later, if required.

# Seperate the z-order index per group
inc = np.arange(0, 10000*(xDash.shape[0]), 10000).astype(np.int64).reshape(-1,1)
zInc = np.tile(inc, [1,hatches.shape[0]]).flatten()
z += zInc

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

Following the generation of these sinusoidal curves, a transformation matrix is applied accordingly, before these are clipped in the Hatcher.hatch method.

The next crucial difference, that has been implemented from PySLM version 0.3, is a new clipping method, BaseHatcher.clipContourLines. The following method is different from BaseHatcher.clipLines, in that clips ContourGeometry separately. This is important for keeping the scan vectors separate and in the correct order, which would be otherwise difficult to achieve. The clipped results are implicitly separated into contour geometry groups.

hatches = self.generateHatching(paths, self._hatchDistance, layerHatchAngle)

clippedPaths = self.clipContourLines(paths, hatches)

# Merge the lines together
if len(clippedPaths) > 0:
    for path in clippedPaths:
        clippedLines = np.vstack(path) 
        clippedLines = clippedLines[:,:2]
        contourGeom = ContourGeometry()

        contourGeom.coords = clippedLines.reshape(-1, 2)


The next step is to sort the clipped paths into the right order. This is done by using the 1st value of 3rd index column accordingly sorting using sorted with a lambda function.

Sort the sinusoidal vectors based on the 1st coordinate's sort id (column 3). This only sorts individual paths
rather than the contours internally.            
clippedPaths = sorted(clippedPaths, key=lambda x: x[0][2])

Now, the result of the sinusoidal scan strategy can be visualised below.

Sinusoidal Hatch Scan Strategy for Selective Laser Melting - PySLM
Sinusoidal Hatch Scan Strategy for Selective Laser Melting – PySLM

This approach currently is very intensive to generate during the clipping operation, due to the number of edges along each clipping operation. Using the previous techniques with the island scan strategy in a previous post, could be use to amorotise a lot of the cost of clipping.

Example Script

The script is available on github at examples/example_custom_sinusoidal_scanning.py


1 Yang, J., Bin, H., Zhang, X., & Liu, Z. (2003). Fractal scanning path generation and control system for selective laser sintering (SLS). International Journal of Machine Tools and Manufacture, 43(3), 293–300. https://doi.org/10.1016/S0890-6955(02)00212-2
2 Ma, L., & Bin, H. (2006). Temperature and stress analysis and simulation in fractal scanning-based laser sintering. The International Journal of Advanced Manufacturing Technology, 34(9–10), 898–903. https://doi.org/10.1007/s00170-006-0665-5
3 Catchpole-Smith, S., Aboulkhair, N., Parry, L., Tuck, C., Ashcroft, I. A., & Clare, A. (2017). Fractal scan strategies for selective laser melting of ‘unweldable’ nickel superalloys. Additive Manufacturing, 15, 113–122. https://doi.org/10.1016/j.addma.2017.02.002
4 Sebastian, R., Catchpole-Smith, S., Simonelli, M., Rushworth, A., Chen, H., & Clare, A. (2020). ‘Unit cell’ type scan strategies for powder bed fusion: The Hilbert fractal. Additive Manufacturing, 36(July), 101588. https://doi.org/10.1016/j.addma.2020.101588
5 Tongtong Liu, Zhongyan Mu, Renzhi Hu, Shengyong Pang,
Sinusoidal oscillating laser welding of 7075 aluminum alloy: Hydrodynamics, porosity formation and optimization, International Journal of Heat and Mass Transfer, Volume 140, 2019, Pages 346-358, ISSN 0017-9310, https://doi.org/10.1016/j.ijheatmasstransfer.2019.05.111
6 Cao, Y., Zhu, S., Liang, X., & Wang, W. (2011). Overlapping model of beads and curve fitting of bead section for rapid manufacturing by robotic MAG welding process. Robotics and Computer-Integrated Manufacturing, 27(3), 641–645. https://doi.org/10.1016/j.rcim.2010.11.002
7 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
8 Ding, D., Pan, Z., Cuiuri, D., & Li, H. (2015). A multi-bead overlapping model for robotic wire and arc additive manufacturing (WAAM). Robotics and Computer-Integrated Manufacturing, 31, 101–110. https://doi.org/10.1016/j.rcim.2014.08.008

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

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


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,

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

        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.


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.

Build Time Estimation in L-PBF (SLM) Using PySLM (Part I)

Build-Time = Cost

This quantity is arguably the greatest driver of individual part cost for the majority of Additive Manufacture parts (excluding the additional costs of post-processing). It inherently relates to the proportional utilisation of the AM system that has a fixed capital cost at purchase under an assumed operation time (estimate is around 6-10 years).

Predicting this quickly and effectively for parts built using Powder Bed Fusion processes may initially sound simple, but actually there aren’t many free or opensource tools that provide a utility to predict this. Also the data isn’t not easily obtainable without having some inputs. In the literature, investigations into build-time estimation, embodied energy consumption and the analysis of costs associated with powder-bed for both SLM and EBM have been undertaken [1][2][3][4].

This usually involves submitting your design to an online portal or building up a spreadsheet and calculating some values. A large part of the cost for a part designed for AM is related to its build-time and this as a value can indicate the relative cost of the AM part.

Build-time, as a ‘lump’ measure is quintessentially the most significant factor in determining the ultimate cost of parts manufactured on powder-bed fusion systems. Obviously, this is oblivious to other factors such as post-processing of parts (i.e. heat-treatment, post-machining) surface coatings and post-inspection and part level qualification, usually essentially as part of the entire manufacturing processes for an AM part.

The reference to a ‘lump’ cost value coincides with various parameters inherent to the part that are driven by the decisions of design to meet the functional requirements / performance. The primary factors affecting this:

  • Material alloy
  • Geometrical shape of the part
  • Machine system

These may be further specified as a set of chosen parameters

  • Part Orientation
  • Build Volume Packing (i.e. number of parts within the build)
  • Number of laser beams in the SLM system
  • Recoater time
  • Material Alloy laser [arameters (i.e. effective laser scan speed)
  • Part Volume (V)

From the build-time, the cost estimate solely for building the piece part can be calculated across ‘batches’ or a number of builds, which largely takes into account fixed costs such as capital investment in the machine and those direct costs associated with material inputs, consumables and energy consumption [5].

In this post, additional factors intrinsic to the machine operation, such as build-chamber warm-up and cool-down time, out-gassing time are ignored. Exploring the economics of the process, these should be accounted for because it can in some processes e.g. Selective Laser Sintering (SLS) and High-Speed-Sintering (HSS) of polymers can account for a significant contribution to the actual ‘accumulated‘ build time within the machine.

Calculation of the Build Time in L-PBF

There are many different approaches for calculating the estimate of the build-time depending on the accuracy required.

Build Bulk Volume Method

The build volume method is the most crudest forms for calculating the build time, t_{build}. The method takes the total volume of the part(s) within a build V and divided by machine’s build volume rate \dot{V} – a lumped empirical value corresponding to a specific material deposited or manufactured by an AM system.


This is very approximate, therefore limited, because the prediction ignores build height within the chamber that is a primary contributor to the build time. Also it ignores build volume packaging – the density of numerous parts contained packed inside a chamber, which for each build contributes a fixed cost. However, it is a good measure for accounting the cost of the part based simply on its mass – potentially a useful indicator early during the design conceptualisation phase.

Layer-wise Method

This approach accounts for the actual geometry of the part as part of the estimation. It performs slicing of the part and accounts for the area and boundaries of the part, which may be assigned separate laser scan speeds. This has been implemented as a multi-threaded/process example in order to demonstrate how one can analysis the cost of a part relatively quickly and simply using this as a template.

The entire part is sliced at the constant layer thickness L_t in the function calculateLayer(). In this function, the part is sliced using getVectorSlice(), at the particular z-height and by disabling returnCoordPaths parameter will return a list of Shapely.geometry.Polygon objects.

def calculateLayer(input):
    d = input[0]
    zid= input[1]

    layerThickness = d['layerThickness']
    solidPart = d['part']

    # Slice the boundary
    geomSlice = solidPart.getVectorSlice(zid*layerThickness, returnCoordPaths=False)

The slice represents boundaries across the layer. Each boundary is a Shapely.Polygon, which can be easily queried for its boundary length and area. This is performed later after the python multi-processing map call:

d = Manager().dict()
d['part'] = solidPart
d['layerThickness'] = layerThickness

# Rather than give the z position, we give a z index to calculate the z from.
numLayers = int(solidPart.boundingBox[5] / layerThickness)
z = np.arange(0, numLayers).tolist()

# The layer id and manager shared dict are zipped into a list of tuple pairs
processList = list(zip([d] * len(z), z))

startTime = time.time()

layers = p.map(calculateLayer, processList)
print('multiprocessing time', time.time()-startTime)

polys = []
for layer in layers:
    for poly in layer:

layers = polys

Calculate total layer statistics:
totalHeight = solidPart.boundingBox[5]
totalVolume = solidPart.volume

totalPerimeter = np.sum([layer.length for layer in layers]) * numCountourOffsets
totalArea = np.sum([layer.area for layer in layers])

Once the sum of the total part area and perimeter are calculated the total scan time can be calculated from these. The approximate measure of scan time across the part volume (bulk region) is related by the total scan area accumulated across each layer of the partA, the hatch distance h_d and the laser scan speed v_{bulk}.

t_{hatch} = \frac{A}{L_t v_{bulk}}

Similarly the scan time across the boundary for contour scans (typically scanned at a lower speed is simply the total perimeter length L divided by the contour scan speed v_{contour}

t_{boundary} = \frac{L}{v_{contour}}

Finally, the re-coating time is simply a multiple of the number of layers.

Calculate the time estimates
hatchTimeEstimate = totalArea / hatchDistance / hatchLaserScanSpeed
boundaryTimeEstimate = totalPerimeter / contourLaserScanSpeed
scanTime = hatchTimeEstimate + boundaryTimeEstimate
recoaterTimeEstimate = numLayers * layerRecoatTime

totalTime = hatchTimeEstimate + boundaryTimeEstimate + recoaterTimeEstimate

Compound approach using Surface and Volume

In fact, it may be possible to deduce that much of this is unnecessary for finding the approximate scanning time. Instead, a simpler formulation can be derived. The scan time can be deduced from simply the volume Vand the total surface area of the part S

t_{total}=\frac{V}{L_t h_d v_{bulk}} + \frac{S}{L_t v_{contour}} + N*t_{recoat},

where N=h_{build}/L_t. After realising this, further looking into literature, it was proposed by Giannatsis et al. back in 2001 for SLA time estimation [6]. Surprisingly, I haven’t come across this before. They propose that taking the vertical projection of the surface better represents the true area of the boundary, under the slicing process.

t_{total}=\frac{V}{L_t h_d v_{bulk}} + \frac{S_P}{L_t v_{contour}} + N*t_{recoat}

The projected area is calculated by taking the dot product with the vertical vector v_{up} = (0.,0.,1.0)^T and the surface normal \hat{n} using the relation: a\cdot b = \|a\| \|b\| \cos(\theta) for each triangle and calculating the sine component using the identity (\cos^2(\theta) + \sin^2(\theta) = 1) to project the triangle area across the vertical extent.

""" Projected Area"""
# Calculate the vertical face angles
v0 = np.array([[0., 0., 1.0]])
v1 = solidPart.geometry.face_normals

sin_theta = np.sqrt((1-np.dot(v0, v1.T)**2))
triAreas = solidPart.geometry.area_faces *sin_theta
projectedArea = np.sum(triAreas)

Comparison between build time estimation approaches

The difference in scan time with the approximation is relatively close for a simple example:

  • Discretised Layer Scan Time – 4.996 hr
  • Approximate Scan Time – 5.126 hr
  • Approximate Scan Time (with projection) – 4.996 hr

Arriving at the rather simple result may not be interesting, but given the frequency of most cost models not stating this hopefully may be useful for some. It is useful in that it can account for the complexity of the boundary rather than simply the volume and the build-height, whilst factoring in the laser parameters used – typically available for most materials on commercial systems .

The second part of the posting will share more details about more precisely measuring the scan time using the analysis tools available in PySLM.


1 Baumers, M., Tuck, C., Wildman, R., Ashcroft, I., & Hague, R. (2017). Shape Complexity and Process Energy Consumption in Electron Beam Melting: A Case of Something for Nothing in Additive Manufacturing? Journal of Industrial Ecology, 21(S1), S157–S167. https://doi.org/10.1111/jiec.12397
2 Baumers, M., Dickens, P., Tuck, C., & Hague, R. (2016). The cost of additive manufacturing: Machine productivity, economies of scale and technology-push. Technological Forecasting and Social Change, 102, 193–201. https://doi.org/10.1016/j.techfore.2015.02.015
3 Faludi, J., Baumers, M., Maskery, I., & Hague, R. (2017). Environmental Impacts of Selective Laser Melting: Do Printer, Powder, Or Power Dominate? Journal of Industrial Ecology, 21(S1), S144–S156. https://doi.org/10.1111/jiec.12528
4 Liu, Z. Y., Li, C., Fang, X. Y., & Guo, Y. B. (2018). Energy Consumption in Additive Manufacturing of Metal Parts. Procedia Manufacturing, 26, 834–845. https://doi.org/10.1016/j.promfg.2018.07.104
5 Leach, R., & Carmignato, S. (2020). Precision Metal Additive Manufacturing (R. Leach & S. Carmignato. https://doi.org/10.1201/9780429436543
6 Giannatsis, J., Dedoussis, V., & Laios, L. (2001). A study of the build-time estimation problem for Stereolithography systems. Robotics and Computer-Integrated Manufacturing, 17(4), 295–304. https://doi.org/10.1016/S0736-5845(01)00007-2