Historical Background
An upcoming key feature in PySLM is the Iterators primarily useful for simulation studies, such as predicting thermo-mechanical behavior of scan strategies. Much of this builds upon ideas in former work that was done during my PhD for investigating the generation of residual stress in selective laser melting. In that study, MSC Marc, a commercial Finite Element analysis package was used to predict residual stresses generated during the process. The discretised position and laser parameters of the exposure from the laser was controlled by combination of Fortran User Subroutines and libSLM, the former c++ library.
Prior to running, a configuration file was passed to the simulation, specifying the SLM machine build file used for simulating the scan paths. libSLM parsed the compatible build-file and then based on the current time and increment would interpolate the position of the exposure point and laser parameters during firing. Beyond this main functionality, there were additional house keeping required for running the simulation, including passing information between the programs, and also additional tools to efficiently seek at an arbitrarily point in time, the state and position of the laser. This was necessary for restarting simulations on a HPC and for adaptive time-stepping required to keep numerical stability. For efficient seeking across the layers and each layer geometry structure was cached within a tree, that could be parsed on demand if necessary. Much of the infrastructure was excessive, although the implementation had to be written in c++ or Fortran to be used by integrated with the commercial solver.
Although it is difficult to perceive the full benefit of having a Python version of the same functionality, there are some instances and some analysis codes where this could be of benefit for modelling this and other processes as well.
Implementation of PySLM Iterators
The implementation builds upon the existing design from the original libSLM library. For all the Iterator classes, similar to most of the other pyslm.analysis
module’s tools, the list of Layers and Models with the Laser Parameters should be passed:
# Iterates across layer geometries
layerGeomIter = Iterator(models, layerList)
# Iterates across individual scan vectors - currently only ContourGeometry/HatchGeometry
scanVectorIter = ScanVectorIterator(models, layerList)
# Generates an scan exposure point iterator
scanIter = ScanIterator(models, layerList)
The first stage is building a time cache tree across each LayerGeometry
. In practice, the cache tree structure is not necessary if the scan iterator iteratively increments along in time. Having a cache structure enables non-linear movement of the iterator across the entire build . It also provides a fast random-access lookup to seek to a specific Layer or LayerGeometry
for use in simulations or analyses.
This structure is formed by iteratively measuring the total time taken to scan an individual LayerGeometry
group which is stored in a tree node (TimeNode
). The cumulative time taken to scan across each LayerGeometry
TimeNode to provide the total scan time across the Layer. The TimeNode
can be assigned child and parent nodes using the attributes (TimeNode.parent
and TimeNode.children
) in order to navigate across the entire tree. Each TimeNode provides a key-value pair (id, value) to store the reference LayerGeometry
or Layer
for simplified access.
The Cache Tree is generated and stored in the Base Class, Iterator
and is generated in the private method (Iterator._generateCache
) and stored in the attribute Iterator.tree
.
def _generateCache(self) -> None:
self._tree = TimeNode() for layerId, layer in enumerate(self.layers): # Create the layer layerNode = TimeNode(self._tree, id=layerId, value=layer) self._tree.children.append(layerNode) for layerGeomId, layerGeom in enumerate(layer.geometry): geomNode = TimeNode(layerNode, id=layerGeomId, value=layerGeom) geomNode.time = getLayerGeometryTime(layerGeom, self._models) layerNode.children.append(geomNode) self._cacheValid = True
The Iterator
class has many useful facilities, such as build-time estimation, seeking access to the Layer
or LayerGeometry
at an arbitrary point in time. The class stores additional info such as the layer dwellTime – this can be re-implemented in a derived class. For implementing the iterator behavior used across all dependent classes it also stores the current time and reference pointers to the current Layer
and LayerGeometry
. Essentially the Iterator class can be used to iterate across each LayerGeometry
within a build as a foundation to the other class. Each of these Iterator classes builds upon the magic methods available in Python: __iter__
and __next__
. The __iter__
method simply sets up the object and re-initialises the Iterators attributes. Once the cache tree is generated internally, it offers no penalty to generate a new iterator . Below is an excerpt taken from the ScanVectorIterator
:
def __iter__(self):
self._time = 0.0
self._layerGeomTime = 0.0
self._layerInc = 0
self._layerGeomInc = 0
return self
def __next__(self):
if self._layerScanVecIt < len( self._layerScanVectors):
scanVector = self._layerScanVectors[self._layerScanVecIt]
self._layerScanVecIt += 1
return scanVector
else:
# New layer
if self._layerIt < len(self._layers):
layerVectors = self.getLayerVectors(self._layers[self._layerIt])
self._layerScanVectors = self.reshapeVectors(layerVectors)
self._layerScanVecIt = 0
self._layerIt += 1
return self.next()
else:
raise StopIteration
The Iterator
class and ScanVectorIterator
class do not require much further attention, as the pointer to the geometry is incremented only. The ScanIterator
class, however, is more useful for simulation and will be discussed further.
Scan Iterator Class
The ScanIterator
class is used for incrementally advancing the exposure source across each scan vector. This is particularly important for visualising or simulating the AM process. The time increment is based on a chosen but adjustable timestep, and the laser parameters across each scan vector (i.e. the effective scan velocity) obtained from the assigned BuildStyle
.
The exposure point is linearly interpolated across each scan vector based on the current time within the LayerGeometry depending on the type. For identifying the position, the cumulative distance is captured and the current timeOffset for the layer geometry is used to estimate the distance covered by the exposure source across the entire LayerGeometry section. For simplicity this assumes no acceleration terms and uses a constant velocity profile. Based on the timeOffset, the scan vector is obtained and then the final position is interpolated across the scan vector.
laserVelocity = getEffectiveLaserSpeed(buildStyle)
# Find the cumulative distance across the scan vectors in the LayerGeometry (Contour)
delta = np.diff(layerGeom.coords, axis=0)
dist = np.hypot(delta[:,0], delta[:,1])
cumDist = np.cumsum(dist)
cumDist2 = np.insert(cumDist, 0,0)
# If the offsetDist calculated is outside of the cumulative distance then some error has occured
if offsetDist > cumDist2[-1]:
raise Exception('Error offset distance > cumDist {:.3f}, {:.3f}'.format(offsetDist, cumDist2[-1]))
id = 0
# Find the id of the current scan vector given the time offset
for i, vec in enumerate(cumDist2):
if offsetDist < vec:
id = i
break
# interpolate the position based on offset distance in the scan vector
linearOffset = (offsetDist - cumDist2[id-1]) / dist[id-1]
point = layerGeom.coords[id-1] + delta[id-1] * linearOffset
The above example is specifically for the contour geometry. Note the for loop is not particularly efficient but serves its purpose for identifying the Iterator’s current scan vector.
Iterator Use:
Each iterator can be subsequently called after using the iter
method in a variety of pythonic ways:
#Create a scan vector iterator
ScanVectorIterator(models, layerList)
# Create a python iter object from a ScanVectorIterator
scanIter = iter(scanVectorIter)
# Get a single scan vector
firstScanVec = more(scanIter)
# Collect all the remaining scan vectors
scanVectors = np.array([point for point in scanIter])
Current Limitations:
Note the current implementation of the iterators currently only consider ContourGeometry
and HatchGeometry
and does not include PointGeometry
groups. The jump vectors are ignored, which will have a small but in most situations a negligible effect on the the overall accuracy of the timing used for the iterators.
Another obvious limitation is that this only accounts for single exposure source systems. It is not known to myself, how multiple-exposure systems scan (i.e. are they truly in parallel based on the laser number) or is there is some built-in machine heuristic which balances the scanning across all laser sources and spatially – e.g. to prevent overheating. This depends on the SLM system such as if multiple exposure sources are limited by zones or have full areal access to the bed. Anyone’s comments or experiences on this aspect would be sincerely welcomed.
Example
An example showing the basic usage and functions available with the Iterator classes are available in the Github Repo examples/example_laser_iterator.py