Skip to main content

Calculating hotspots in the Additive Manufacturing DED process

abel.ramos@ansys.com | 04.28.2025

Introduction

This example demonstrates the creation of a complete Mechanical Extension to showcase the use of Ansys scripting capabilities for calculating residual hotspots in the Additive Manufacturing Direct Energy Deposition (DED) process. All relevant code is made available in this blog post under the MIT license.

Requirements

It is required that users have a good understanding of:

  • Ansys DED AM Extension
  • Ansys Mechanical Scripting
  • Ansys Mechanical ACT (Ansys Customization Toolkit)
  • Data Processing Framework (DPF)
  • Code shared tested on Ansys 23R2. Modifications might be required for other versions.

Extension Description

The following extension has been developed as an example on how dpf, ACT and Mechanical scripting can be used in combination to create specific customized results.

In this example, the hotspot areas after a DED process are calculated in two different ways. It can be executed on the Thermal Analysis in the DED process.

Let's start by defining what is understood as "hotspot" in the context of this article: In some manufacturing methods, as AM-DED, it might be interesting to control the residual heat remaining in each depositioned cluster of elements before the next cluster is activated. The residual temperature at this moment of the simulation is denominated "hotspot", and is described in the image below:

DED Hot Spot Extension

Create DED post File

Create DED post File button in the Extension Toolbar will generate a txt file in the solution folder named "ClusterHotspot.txt" that will contain the maximum temperature reached on the last cluster depositioned just before the new cluster of elements is alived in the Mechanical model.

DED Hot Spot Extension

All the details can be reviewed in the script section: CreateHotspotDedFile.py

Plot HotSpot Results

A new custom result is also available to postprocess the hotspot in the geometry for each cluster.

This result automatically stores the last results set for each cluster before aliving the next set of elements. DED Hot Spot Extension

Once the clusters hotspot temperatures have been stored, it plots all of them in the geometry to allow the user identy the different residual heat on each region during the printing process. DED Hot Spot Extension

All the details can be reviewed in the script section: PlotHotspot3d.py

Code

This section includes all the files needed to execute the extension in Ansys Mechanical.

Compile a Mechanical Extension

Reader is advised to review in the detail Mechanical ACT training course to get all the deatils on how to compile an extension and/or how to execute it from the scripts.

Nevertheless, a brief summary of the steps needed to execute the extension are collected below:

  • Save the scripts in this article on three different files:
    • DED_Post.xml
    • CreateHotspotDedFile.py
    • PlotHotspot3d.py
  • Create the following Folder Structure
    • Folder with the Extension Name
    • DED_Post.xml file with a folder with the same name DED_Post
    • DED Hot Spot Extension
    • Inside DED_Post Folder, include the two python scripts
    • DED Hot Spot Extension
  • Add the folder directory where the extension is saved to Workbench
    • ACT Start Page > Manage Extensions > Manage Your Settings > + Add Folder

Scripts

XML File

<extension version="1" name="DED_Post">
<!-- Author:abel.ramos@ansys.com -->
  <guid shortid="DED_Post">A1BB39CD-B8C6-43EC-BB98-334ADC00740D</guid>
  <script src="CreateHotspotDedFile.py" />
  <script src="PlotHotspot3d.py" />

  <interface context="Mechanical">
    <images>images</images>

    <!-- Create a toolbar and add a button to insert a ACT load -->
    <toolbar name="DEDPost" caption="DEDPost">
      <entry name="CreateDedPostFile" icon="Thermal">
        <callbacks>
          <onclick>createHotspotFile</onclick>
        </callbacks>
      </entry>
    </toolbar>
  </interface>

  <!--................................. -->
  <!--    MECHANICAL CUSTOM OBJECTS     -->
  <!--................................. -->

  <simdata context="Mechanical">
    <result name="HotspotUserResult" version="1" caption="HotspotUserResult" icon="result" location="node" type="scalar">
      <callbacks>
        <evaluate>EvalHotSpotUserResult</evaluate>
      </callbacks>
      <property name="Geometry" caption="Geometry" control="scoping"></property>
      <property name="Feedback"  caption= "Feedback" control="text" default="abel.ramos@ansys.com" readonly="true" ></property>
    </result>
  </simdata>
</extension>

Script CreateHotspotDedFile.py

#Author: abel.ramos@ansys.com
import os


def createHotspotFile(createHotspotFile):
    import os
    import mech_dpf
    import Ans.DataProcessing as dpf
    mech_dpf.setExtAPI(ExtAPI)

    analysis = ExtAPI.DataModel.Project.Model.Analyses[0]

    dataSources = dpf.DataSources(analysis.ResultFileName)

    
    totalNamedSelections =ExtAPI.DataModel.GetObjectsByType(DataModelObjectCategory.NamedSelection)
    clusterCount = 0
    for nameSelection in totalNamedSelections:
        if 'el_loop_' in nameSelection.Name:
            clusterCount += 1

    ClusterHotpot = []

    for i in range(2,clusterCount+1):
        masterName = 'EL_LOOP_' + str(i)
        previousName = 'EL_LOOP_' + str(i-1)
        clusterName,clusterHotspotTempDist = GetHotpotTemperatureDistribution(masterName,previousName,dpf,dataSources)
        clusterHotPotTemp = GetMaxInAField(dpf,clusterHotspotTempDist)
        ClusterHotpot.append([clusterName,clusterHotPotTemp])

    outputfilePath = analysis.WorkingDir
    fileName = 'ClusterHotspot.txt'

    filepath = os.path.join(outputfilePath,fileName)

    with open(filepath,"w") as outputFile:
        for line in ClusterHotpot:
            outputFile.write(str(line[0]) + ";" + str(line[1]) + "\n")
        outputFile.close()


def GetMaxInAField(dpf,dpfField):
    #Max of previous cluster
    MaxTempPrevOp = dpf.operators.min_max.min_max()
    MaxTempPrevOp.inputs.field.Connect(dpfField)
    MaxTempPrevCluster = MaxTempPrevOp.outputs.field_max.GetData()
    
    return MaxTempPrevCluster.Data[0]

Script PlotHotspot3d.py

#Author: abel.ramos@ansys.com
def EvalHotSpotUserResult(result, stepInfo, collector):
    nodesIds = collector.Ids
    import os
    import mech_dpf
    import Ans.DataProcessing as dpf
    mech_dpf.setExtAPI(ExtAPI)

    analysis = ExtAPI.DataModel.Project.Model.Analyses[0]

    dataSources = dpf.DataSources(analysis.ResultFileName)

    #Calculate total number of sets:
    timeScopOp1 = dpf.operators.metadata.time_freq_provider()
    timeScopOp1.inputs.data_sources.Connect(dataSources)
    timeFreq1 = timeScopOp1.outputs.time_freq_support.GetData()
    timeResultsSets1 = timeFreq1.NumberSets

    if stepInfo.Set >= timeResultsSets1:
        totalNamedSelections =ExtAPI.DataModel.GetObjectsByType(DataModelObjectCategory.NamedSelection)
        clusterCount = 0
        for nameSelection in totalNamedSelections:
            if 'el_loop_' in nameSelection.Name:
                clusterCount += 1

        ClusterHotpot = []

        for i in range(2,clusterCount+1):
            masterName = 'EL_LOOP_' + str(i)
            previousName = 'EL_LOOP_' + str(i-1)
            clusterName,clusterHotPotTemp = GetHotpotTemperatureDistribution(masterName,previousName,dpf,dataSources)

            for nodeID in clusterHotPotTemp[0].ScopingIds:
                nodeTemp = clusterHotPotTemp[0].GetEntityDataById(nodeID)[0]
                collector.SetValues(nodeID, [float(nodeTemp)])

        #Last Cluster Forward Estimation
        indexLastMaster = clusterCount
        masterName = 'EL_LOOP_' + str(indexLastMaster)
        previousName = 'EL_LOOP_' + str(indexLastMaster-1)
        clusterName,clusterHotPotTempLast = GetLastClusterHotspotTempDistribution(masterName,previousName,dpf,dataSources)
        for nodeID in clusterHotPotTempLast[0].ScopingIds:
            nodeTemp = clusterHotPotTempLast[0].GetEntityDataById(nodeID)[0]
            collector.SetValues(nodeID, [float(nodeTemp)])

    return



def GetHotpotTemperatureDistribution(masterName,previousName,dpf,dataSources):
    #########################################
    ###           MASTER CLUSTER          ###
    #########################################
    #Time Sets in Results File
    timeScopOp1 = dpf.operators.metadata.time_freq_provider()
    timeScopOp1.inputs.data_sources.Connect(dataSources)
    timeFreq1 = timeScopOp1.outputs.time_freq_support.GetData()
    timeResultsSets1 = timeFreq1.NumberSets
    
    #Time Scoping
    timeScopingOp1 = dpf.Scoping()
    timeScopingOp1.Ids = range(1,timeResultsSets1+1)
    timeScopingOp1.Location = "Time"
    
    #Name Selection Operator
    nameSelOp = dpf.operators.scoping.on_named_selection()
    nameSelOp.inputs.data_sources.Connect(dataSources)
    nameSelOp.inputs.requested_location.Connect('Nodal')
    nameSelOp.inputs.named_selection_name.Connect(masterName)
    meshScoping=nameSelOp.outputs.mesh_scoping
    
    #Temperature Results
    tempResultsOp = dpf.operators.result.temperature()
    tempResultsOp.inputs.data_sources.Connect(dataSources)
    tempResultsOp.inputs.mesh_scoping.Connect(meshScoping)
    tempResultsOp.inputs.time_scoping.Connect(timeScopingOp1)
    tempClustMaster = tempResultsOp.outputs.fields_container.GetData()
    
    #Time when max occurs
    timeMaxOp = dpf.operators.min_max.min_max_over_time_by_entity()
    timeMaxOp.inputs.fields_container.Connect(tempClustMaster)
    myMax = timeMaxOp.outputs.max.GetData()
    timeMax = timeMaxOp.outputs.time_freq_of_max.GetData()
    
    #Time of max removing interface nodes
    timeOfMaxAbsoluteOp = dpf.operators.min_max.min_max()
    timeOfMaxAbsoluteOp.inputs.field.Connect(timeMax)
    timeMaxAbsolute = timeOfMaxAbsoluteOp.outputs.field_max.GetData()
    
    #########################################
    ###        PREVIOUS CLUSTER           ###
    #########################################
    #Find the time index
    timeIndex = 0
    for t_i in range(0,timeResultsSets1):
        if timeMaxAbsolute.Data[0]-timeFreq1.GetTimeFreq(t_i) <= 1e-10:
            timeIndex = t_i
            break
    
    #Time Scoping
    timeScopingPrevOp = dpf.Scoping()
    #If we use "timeIndex" we are actually using the previous step, since
    # the count start in "1". If we want to use the time step
    #at which the maximum occurs, then use timeIndex+1
    timeScopingPrevOp.Ids = [timeIndex] 
    timeScopingPrevOp.Location = 'Time'
    
    #Name Selection Operator Previous Cluster
    nameSelPrevOp = dpf.operators.scoping.on_named_selection()
    nameSelPrevOp.inputs.data_sources.Connect(dataSources)
    nameSelPrevOp.inputs.requested_location.Connect('Nodal')
    nameSelPrevOp.inputs.named_selection_name.Connect(previousName)
    meshScopingPrev=nameSelPrevOp.outputs.mesh_scoping
    
    #Temperature Results
    tempResultsPrevOp = dpf.operators.result.temperature()
    tempResultsPrevOp.inputs.data_sources.Connect(dataSources)
    tempResultsPrevOp.inputs.mesh_scoping.Connect(meshScopingPrev)
    tempResultsPrevOp.inputs.time_scoping.Connect(timeScopingPrevOp)
    tempClustPrev = tempResultsPrevOp.outputs.fields_container.GetData()
    
    return previousName,tempClustPrev
########################################

def GetLastClusterHotspotTempDistribution(masterName,previousName,dpf,dataSources):
    #########################################
    ###           MASTER CLUSTER          ###
    #########################################

    clusterNames = [masterName,previousName]
    clustersTimes = []
    for i in range(0,2):
        #Time Sets in Results File
        timeScopOp1 = dpf.operators.metadata.time_freq_provider()
        timeScopOp1.inputs.data_sources.Connect(dataSources)
        timeFreq1 = timeScopOp1.outputs.time_freq_support.GetData()
        timeResultsSets1 = timeFreq1.NumberSets

        #Time Scoping
        timeScopingOp1 = dpf.Scoping()
        timeScopingOp1.Ids = range(1,timeResultsSets1+1)
        timeScopingOp1.Location = "Time"

        #Name Selection Operator
        nameSelOp = dpf.operators.scoping.on_named_selection()
        nameSelOp.inputs.data_sources.Connect(dataSources)
        nameSelOp.inputs.requested_location.Connect('Nodal')
        nameSelOp.inputs.named_selection_name.Connect(clusterNames[i])
        #nameSelOp.inputs.int_inclusive.Connect(1)
        meshScoping=nameSelOp.outputs.mesh_scoping

        #Temperature Results
        tempResultsOp = dpf.operators.result.temperature()
        tempResultsOp.inputs.data_sources.Connect(dataSources)
        tempResultsOp.inputs.mesh_scoping.Connect(meshScoping)
        tempResultsOp.inputs.time_scoping.Connect(timeScopingOp1)
        tempClustMaster = tempResultsOp.outputs.fields_container.GetData()

        #Time when max occurs
        timeMaxOp = dpf.operators.min_max.min_max_over_time_by_entity()
        timeMaxOp.inputs.fields_container.Connect(tempClustMaster)
        myMax = timeMaxOp.outputs.max.GetData()
        timeMax = timeMaxOp.outputs.time_freq_of_max.GetData()

        #Time of max removing interface nodes
        timeOfMaxAbsoluteOp = dpf.operators.min_max.min_max()
        timeOfMaxAbsoluteOp.inputs.field.Connect(timeMax)
        timeMaxAbsolute = timeOfMaxAbsoluteOp.outputs.field_max.GetData()
        clustersTimes.append(timeMaxAbsolute.Data[0])
    
    incTimeEstimation = clustersTimes[0]-clustersTimes[1]
    newTimeForward = clustersTimes[0] + incTimeEstimation
    
    #########################################
    ###        MAST CLUSTER FORW          ###
    #########################################
    #Find the time index
    timeIndex = 0
    timeDistancesList = []
    for t_i in range(0,timeResultsSets1):
        timeDistancesList.append(abs(timeFreq1.GetTimeFreq(t_i)-newTimeForward))

    timeIndex = timeDistancesList.IndexOf(min(timeDistancesList))

    #Time Scoping
    timeScopingPrevOp = dpf.Scoping()
    timeScopingPrevOp.Ids = [timeIndex] 
    timeScopingPrevOp.Location = 'Time'

    #Name Selection Operator Previous Cluster
    nameSelPrevOp = dpf.operators.scoping.on_named_selection()
    nameSelPrevOp.inputs.data_sources.Connect(dataSources)
    nameSelPrevOp.inputs.requested_location.Connect('Nodal')
    nameSelPrevOp.inputs.named_selection_name.Connect(masterName)
    meshScopingPrev=nameSelPrevOp.outputs.mesh_scoping

    #Temperature Results
    tempResultsPrevOp = dpf.operators.result.temperature()
    tempResultsPrevOp.inputs.data_sources.Connect(dataSources)
    tempResultsPrevOp.inputs.mesh_scoping.Connect(meshScopingPrev)
    tempResultsPrevOp.inputs.time_scoping.Connect(timeScopingPrevOp)
    tempClustMastForward = tempResultsPrevOp.outputs.fields_container.GetData()
    
    return masterName,tempClustMastForward
########################################

Summary and Conclusion

This example highlights how Ansys Mechanical Extensions, when combined with DPF, ACT, and scripting, can be leveraged to address specific challenges in Additive Manufacturing processes—specifically, the identification of residual hotspots in DED simulations. By implementing two alternative methods to compute these hotspots, the extension demonstrates a flexible and customizable approach to thermal result post-processing. The provided code serves as both a practical tool for immediate application and a learning resource for users aiming to deepen their knowledge of Ansys scripting workflows.