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:
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.
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.
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.
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
- Inside DED_Post Folder, include the two python scripts
- 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.