Hey everyone,
I'm working on a project which requires quite a few linearizations. I'm working to automate my workflow within 2024R2 Mechanical, but keep wanting all sorts of improvements that would be useful, but not necessarily easy to incorporate. I've looked around a bit, and found some similar discussions and adjacent questions, (like this,) but considering I want to look at the topic more broadly, I thought to make it into its own discussion.
my own script efforts are rather messy and depend on the setup of my model, so I'm only showing the pertinent code and describing the restraints its working under. Hopefully this will be clear enough.
What I've been doing are linearizations over stress range results (i.e. solution combinations rather than systems themselves), at hot spot locations for specific geometry surfaces.
Since the results of these are not expressly stored in '.rst' files, I can't just add a "dpf.DataSources(analysis.ResultFileName)" to refer the script to the results, like in this example. I have to rebuild the results, based on the setup of the 'Solution Combination', before I can look at my hot spots. Luckily for me, the combinations are all just the difference between two stress results, so I can keep it simple.
code
import mech_dpf
import Ans.DataProcessing as dpf
stress1 = dpf.operators.result.stress() # operator instantiation
stress2 = dpf.operators.result.stress() # operator instantiation
mins = dpf.operators.math.minus_fc() #operater instanciation
vonmises = dpf.operators.invariant.von_mises_eqv_fc() # operator instantiation
min_max = dpf.operators.min_max.min_max() #operater instanciation
project = ExtAPI.DataModel.Project
model = project.Model
mech_dpf.setExtAPI(ExtAPI)
within loop for Solution Combinations 'item'
dataSource1 = dpf.DataSources(item.Definition.GetBaseCaseAnalysis(0).ResultFileName)
dataSource2 = dpf.DataSources(item.Definition.GetBaseCaseAnalysis(1).ResultFileName)
mesh2=item.Definition.GetBaseCaseAnalysis(0).MeshData
within nested loop for folder called 'Autogen' in solution combination referred to as 'lin',
and for each equivalent stress item inside that folder referred to as 'lin2'
stress1.inputs.mesh_scoping.Connect(mech_dpf.GetNodeScopingByRefId(lin2.Location))
stress2.inputs.mesh_scoping.Connect(mech_dpf.GetNodeScopingByRefId(lin2.Location))
stress1.inputs.data_sources.Connect(dataSource1)
stress2.inputs.data_sources.Connect(dataSource2)
mins.inputs.field_or_fields_container_A.Connect(stress1.outputs.fields_container)
mins.inputs.field_or_fields_container_B.Connect(stress2.outputs.fields_container)
vonmises.inputs.fields_container.Connect(mins.outputs.fields_container)
min_max.inputs.field.Connect(vonmises.outputs.fields_container)
maxData = min_max.outputs.field_max.GetData()
maxValue=maxData.Data[0]
maxNodeId=maxData.ScopingIds[0]
node2=mesh2.NodeById(maxNodeId)
with the hot spot location and value determined, I move on to generating the direction of a linearization at this point. I use the normal of the largest surface at the hot spot point.
SurfaceSelect = ExtAPI.SelectionManager.CreateSelectionInfo(SelectionTypeEnum.GeometryEntities)
for Gid in node2.GeoEntityIds:
if ExtAPI.DataModel.GeoData.GeoEntityById(Gid).ToString().Contains("GeoFace"):
SurfaceSelect.Ids.Add(Gid)
if SurfaceSelect.Entities.Count != 0:
maxArea = SurfaceSelect.Entities[0].Area
maxitem = 0
for geomItem in SurfaceSelect.Entities:
if geomItem.Area > maxArea:
maxArea = geomItem.Area
maxitem = SurfaceSelect.Entities.IndexOf(geomItem)
SurfaceSelect.Ids = [SurfaceSelect.Entities[maxitem].Id]
SurfParam=SurfaceSelect.Entities[0].ParamAtPoint((node2.X,node2.Y,node2.Z))
Normal = Vector3D(SurfaceSelect.Entities[0].NormalAtParam(SurfParam[0],SurfParam[1]))
I set up a new coordinate system from which the linearization path can be derived.
variable 'newName' has been defined earlier with some string manipulations to represent the system properly
newSystem = model.CoordinateSystems.AddCoordinateSystem()
newSystem.Name = newName
newSystem.SetOriginLocation(Quantity(value=node2.X,unit=mesh2.Unit),Quantity(value=node2.Y,unit=mesh2.Unit),Quantity(value=node2.Z,unit=mesh2.Unit))
newSystem.PrimaryAxisDefineBy = CoordinateSystemAlignmentType.HitPoint
newSystem.PrimaryAxisDirection = -Normal
end of code
The initial thought was to use 'X-axis intersect' definition for the linearization paths, but as I'm dealing with a multibody part, this resulted in issues. It appears to run in the desired direction, but the path will stop at the next surface. If the hot spot occurs at the border between bodies, there's two surfaces at the same spot, and no line will result.
So later in my scripts, I generate my final paths by 'two points' starting at the generated coordinate systems, and with the endpoint determined using ray casting along their X-axis. These paths are used in turn to populate my Solution Combination systems with linearizations which will generate my required results, but that's outside of the discussion scope.
code
within loop over coordinate systems, locally counted using 'i', and with coordinate system data stored in the list 'coordinateSystems'
the code requires that the minimum length to the next surface is at least 1 mm.
since the raycast point on curved geometry can fall outside of the mesh, the endpoint is snapped to the closes node at the end.
newPath = model.ConstructionGeometry.AddPath()
newPath.Name = coordinateSystems[i][0]
newPath.PathType = PathScopingType.Points
newPath.StartXCoordinate = newSystem.OriginX
newPath.StartYCoordinate = newSystem.OriginY
newPath.StartZCoordinate = newSystem.OriginZ
minvalue = 1
Point1 = Point([newSystem.OriginX.Value,newSystem.OriginY.Value,newSystem.OriginZ.Value], newSystem.OriginZ.Unit)
Ray1 = Ansys.Mechanical.Math.BoundVector(Point1,newSystem.PrimaryAxisDirection)
SurfIntersects = Ansys.Mechanical.Selection.SelectionHelper.CastRayOnGeometry(Ray1)
for item in SurfIntersects:
Point2 = Ansys.Mechanical.Graphics.Point.ConvertUnit(item.HitVector.Origin, 'mm')
vector = Vector3D(Point2.Location[0]-Point1.Location[0],Point2.Location[1]-Point1.Location[1],Point2.Location[2]-Point1.Location[2])
if vector.Magnitude > minvalue:
newPath.EndXCoordinate = Quantity(Point2.Location[0],"mm")
newPath.EndYCoordinate = Quantity(Point2.Location[1],"mm")
newPath.EndZCoordinate = Quantity(Point2.Location[2],"mm")
break
newPath.SnapPathToMeshNodes()
end code
Things I would like to add and/or improve on:
- scaling the length of 'newPath' to fit within the mesh, instead of using snapping to ensure the ray-cast point falls within the mesh.
encoding the direction at the hotspot into the name of the equivalent stress result it is generated from. (currently only uses the location/geometry.)
- normal at the surface (as implemented now, but perhaps with better raycast implementation.)
- in the direction of a supplementary coordinate system (e.g. radial direction of the cylindrical coordinate system of a nozzle instead of the surface normal. For use at fillets at nozzle junctions.)
- aligned to generate the shortest distance to auxiliary geometry. (thinking to make the auxiliary geometry named geometry, so I can refer to it in the equivalent stress item name. For more irregular geometry.)
aligning the secondary axis of the coordinate systems I generate to those supplementary coordinate systems (again nozzles/shells) so e.g. the stress components over the path can be aligned with hoop stresses, axial stresses etc.
I don't think all of these can be implemented as quickly as I would like, although most seem reasonably doable.