# -*- coding: utf-8 -*-
#
# This file is subject to the terms and conditions defined in
# file 'LICENSE.txt', which is part of this source code package.
#
"""Ansys CDB format writer."""
from typing import Optional, List, Dict
from io import TextIOWrapper
from pathlib import Path
import numpy as np
from Muscat.Types import MuscatIndex
import Muscat.MeshContainers.ElementsDescription as ED
from Muscat.MeshContainers.Mesh import Mesh
from Muscat.MeshContainers.Filters.FilterObjects import ElementFilter
from Muscat.IO.WriterBase import WriterBase
[docs]
def WriteMeshToCDB(fileName: str, mesh: Mesh, file: Optional[Path] = None, remotePoints: Optional[dict] = None, writeShells: bool = False, writeBeams: bool = False) -> None:
"""Write a mesh in the CDB format (Ansys).
A file is created using the path and the name given by ``fileName``.
Parameters
----------
fileName : str
Name (with path, relative or absolute) of the file to be created.
mesh : Mesh
The mesh to be exported.
file : Path, optional
Path to an existing CDB file to be updated instead of being written
from scratch, by default None.
remotePoints : dict, optional
Dictionary describing the remote points to be exported, by default
None.
writeShells : bool, optional
If True the surface elements (dimensionality ``dim - 1``) and their
associated tags are written in the CDB file, by default False.
writeBeams : bool, optional
If True the beam elements (dimensionality ``dim - 2``) and their
associated tags are written in the CDB file, by default False.
"""
if remotePoints is None:
remotePoints = {}
ansysWriter = AnsysWriter()
ansysWriter.writeShells = writeShells
ansysWriter.writeBeams = writeBeams
ansysWriter.Open(fileName)
try:
if file:
ansysWriter.WriteFromFile(mesh, file)
else:
ansysWriter.Write(mesh, remotePoints)
finally:
ansysWriter.Close()
[docs]
class AnsysWriter(WriterBase):
"""Ansys CDB format writer.
Attributes
----------
AnsysFormulation : dict
Mapping between the dimensionality of the elements and the Ansys
physics keyword (``"SOLID3D"`` for 3D, ``"SOLID2D"`` for 2D). Read the
file ``AnsysTools.py`` (variable ``MuscatToAnsysFormulation``) for more
details.
writeShells : bool
If True the surface elements (dimensionality ``dim - 1``) are written
in the CDB file using the Ansys ``SHELL`` formulation, together with
their associated element tags, by default False.
writeBeams : bool
If True the beam elements (dimensionality ``dim - 2``) are written in
the CDB file using the Ansys ``BEAM`` formulation, together with their
associated element tags, by default False.
shellThickness : float
Thickness used in the ``SECDATA`` card of the shell sections, by
default 0.01.
"""
# Mapping between the Muscat element types and the local node ordering used
# in the Ansys EBLOCK connectivity (repeated indices duplicate a node so
# that a lower-order element fills the 8-node Ansys slots).
exportMask = {
ED.Hexahedron_8: [0, 1, 2, 3, 4, 5, 6, 7],
ED.Wedge_6: [0, 1, 2, 2, 3, 4, 2, 5],
ED.Tetrahedron_4: [0, 1, 2, 2, 3, 3, 3, 3],
ED.Pyramid_5: [0, 1, 2, 3, 4, 4, 5, 5],
ED.Triangle_3: [0, 1, 2, 2],
ED.Quadrangle_4: [0, 1, 2, 3],
ED.Triangle_6: [0, 1, 2, 2, 3, 4, 2, 5],
ED.Tetrahedron_10: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
ED.Bar_3: [0, 1, 2],
ED.Bar_2: [0, 1],
}
def __init__(self) -> None:
super().__init__()
self.canHandleBinaryChange = False
self.AnsysFormulation = {3: "SOLID3D", 2: "SOLID2D"}
self.writeShells = False
self.writeBeams = False
self.shellThickness = 0.01
[docs]
def WriteNamedSelection(self, name: str, kind: str, indices: np.ndarray) -> None:
"""Write a single ``CMBLOCK`` named selection.
Parameters
----------
name : str
Name of the named selection (component).
kind : str
Kind of entities referenced, either ``"NODE"`` or ``"ELEM"``.
indices : np.ndarray
One-based ids of the entities of the named selection.
"""
linesOfIndices = self.CompactIndices(indices)
totalCount = sum(len(line) for line in linesOfIndices)
self.filePointer.write(f"/com,*********** Create Named Selection of {kind} {name} ***********\n")
self.filePointer.write(f"CMBLOCK,{name},{kind},{totalCount}\n")
self.filePointer.write("(8i10)\n")
for line in linesOfIndices:
formattedLine = ''.join(f'{str(value):>10}' for value in line)
self.filePointer.write(f"{formattedLine}".rjust(10))
self.filePointer.write("\n")
[docs]
def CompactIndices(self, indices: np.ndarray) -> List[List[int]]:
"""Convert a list of indices into the Ansys CDB compact form.
Parameters
----------
indices : np.ndarray
Indices of the elements, one by one.
Returns
-------
list
List of lines where consecutive indices are replaced by ``begin``
and ``-end``, each line containing at most 8 elements.
"""
sortedIndices = np.sort(indices)
differences = np.diff(sortedIndices)
splitPositions = np.where(differences != 1)[0] + 1
sections = np.split(sortedIndices, splitPositions)
compactedIndices = []
for section in sections:
if section.shape[0] > 2:
compactedIndices.extend([section[0], -section[-1]])
elif section.shape[0] == 2:
compactedIndices.extend([section[0], section[1]])
elif section.shape[0] == 1:
compactedIndices.extend([section[0]])
lines = [compactedIndices[i:i + 8] for i in range(0, len(compactedIndices), 8)]
return lines
[docs]
def SliceRidges(self, mesh: Mesh) -> List[List[int]]:
"""Build ordered ridge-node chains from ``Bar_2`` elements tagged ``Ridges``.
Consecutive nodes in each output chain always belong to the same ridge
bar element. Chains are split into chunks of at most 100 nodes so they
can be used by the Ansys ``SPLINE`` command.
Parameters
----------
mesh : Mesh
Mesh containing the ``Bar_2`` elements with a ``Ridges`` tag.
Returns
-------
list of list of int
List of node chains, each chain being a list of node ids of length
at most 100. An empty list is returned when no ``Ridges`` tag is
found.
"""
connectivity = None
for elements in mesh.elements.values():
if "Ridges" in elements.tags:
ridgeIds = elements.tags["Ridges"].GetIds()
connectivity = elements.connectivity[ridgeIds]
break
if connectivity is None:
return []
nodeToBars: Dict[int, List[int]] = {}
for barId, bar in enumerate(connectivity):
for nodeId in bar[:2]:
if nodeId not in nodeToBars:
nodeToBars[nodeId] = []
nodeToBars[nodeId].append(barId)
used = [False] * len(connectivity[:, :2])
def ConsumeChain(nodeId: int, startBarId: int) -> List[int]:
"""Walk through connected bars to build a single ordered chain.
Parameters
----------
nodeId : int
Node id from which the chain starts.
startBarId : int
Id of the first bar of the chain.
Returns
-------
list of int
Ordered list of node ids forming the chain.
"""
start, end = connectivity[startBarId, :2]
if start != nodeId:
start, end = end, start
chain = [nodeId, end]
used[startBarId] = True
currentEnd = end
while len(nodeToBars[currentEnd]) == 2:
candidates = [barId for barId in nodeToBars.get(currentEnd, []) if not used[barId]]
if len(candidates) == 0:
break
nextBarId = candidates[0]
start, end = connectivity[nextBarId, :2]
if start != currentEnd:
start, end = end, start
used[nextBarId] = True
chain.append(end)
currentEnd = end
chain = [int(value) for value in chain]
return chain
chains = []
# Start from non-regular nodes (endpoints/branches), then process loops.
for nodeId, bars in nodeToBars.items():
if len(bars) != 2:
for barId in bars:
if not used[barId]:
chains.append(ConsumeChain(nodeId, barId))
# Then consume remaining bars (cycles/branches/disconnected pieces).
for barId in range(len(connectivity[:, :2])):
if not used[barId]:
chains.append(ConsumeChain(int(connectivity[barId, 0]), barId))
ridges = []
for chain in chains:
for start in range(0, len(chain), 100):
ridges.append(chain[start:start + 100])
return ridges
[docs]
def Write(self, mesh: Mesh, remotePoints: Optional[dict] = None) -> None:
"""Write a CDB file from the mesh information.
Parameters
----------
mesh : Mesh
The mesh to be exported.
remotePoints : dict, optional
Dictionary describing the remote points to be exported, by default
None.
Notes
-----
When remote points are provided, this method mutates the input
dictionary by storing the generated pilot node id under the ``"ID"``
key of each remote point.
"""
if remotePoints is None:
remotePoints = {}
mesh.PrepareForOutput()
self.filePointer.write("! ****** Written by Muscat package ******\n")
self.filePointer.write("! ****** Please Disconnect 'Check Valid Blocked CDB File' ******\n")
# self.filePointer.write("! ****** Named selections must be read inside mechanical by reimporting this cdb file' ******\n")
self.filePointer.write("/PREP7\n/NOPR\n")
dim = mesh.GetElementsDimensionality()
numberOfPoints = mesh.GetNumberOfNodes()
from Muscat.IO.AnsysTools import MuscatToAnsysFormulation
# Decide which element dimensionalities are written and with which
# Ansys formulation. The body elements (dimensionality ``dim``) are
# always written; the surface (``dim - 1``) and beam (``dim - 2``)
# elements are only written when the corresponding flag is enabled.
writtenFormulations = {dim: self.AnsysFormulation[dim]}
if self.writeShells:
writtenFormulations[dim - 1] = "SHELL"
if self.writeBeams:
writtenFormulations[dim - 2] = "BEAM"
def IsWritten(elementType: ED.ElementType) -> bool:
"""Return True when the element type is written in the CDB file."""
elementDimensionality = ED.dimensionality[elementType]
formulationName = writtenFormulations.get(elementDimensionality)
if formulationName is None:
return False
return elementType in MuscatToAnsysFormulation[formulationName]
# NUMOFF,TYPE is the number of element types (ET cards) and NUMOFF,ELEM
# the number of elements of the model. Both must reflect only what is
# actually written in the file (body elements, plus shells/beams when
# the corresponding flags are enabled). Remote points are isolated pilot
# nodes carrying constraints, not elements, so they are not counted.
numberOfWrittenTypes = 0
numberOfWrittenElements = 0
for elementType, elements in mesh.elements.items():
if not IsWritten(elementType):
continue
numberOfWrittenTypes += 1
numberOfWrittenElements += elements.GetNumberOfElements()
self.filePointer.write(f"NUMOFF,NODE,{numberOfPoints:>9}\n")
self.filePointer.write(f"NUMOFF,ELEM,{numberOfWrittenElements:>9}\n")
self.filePointer.write(f"NUMOFF,TYPE,{numberOfWrittenTypes}\n")
self.filePointer.write("/com,*********** Nodes for the whole assembly ***********\n")
self.filePointer.write(f"NBLOCK,{dim},,{numberOfPoints}\n")
self.filePointer.write("(1i9,3e20.9e3)\n")
nodePositions = mesh.GetPosOfNodes()
for nodeIndex in range(numberOfPoints):
self.filePointer.write(f"{nodeIndex+1}".rjust(9))
for axis in range(dim):
self.filePointer.write(f"{nodePositions[nodeIndex, axis]:1.9E}".rjust(20))
self.filePointer.write("\n")
self.filePointer.write("-1\n")
if len(remotePoints.keys()) > 0:
numberOfMeshNodes = numberOfPoints
self.filePointer.write("/com,*********** Nodes for all Remote Points ***********\n")
self.filePointer.write(f"NBLOCK,{dim}\n")
self.filePointer.write("(1i9,3e20.9e3)\n")
for remoteIndex, value in enumerate(remotePoints.values()):
self.filePointer.write(f"{remoteIndex+numberOfMeshNodes+1}".rjust(9))
for axis in range(dim):
self.filePointer.write(f"{value['coords'][axis]:1.9E}".rjust(20))
value["ID"] = remoteIndex + numberOfMeshNodes + 1
self.filePointer.write("\n")
self.filePointer.write("-1\n")
exportMask = self.exportMask
# Group the written element types by dimensionality so that each
# dimensionality is exported in its own EBLOCK (SOLID, SHELL, BEAM).
# The element numbering (elementCounter) is shared across every block
# so the tags computed later reference the correct global ids.
blockDefinitions = [(dim, "SOLID", 19)]
if self.writeShells:
blockDefinitions.append((dim - 1, "SHELL", 11))
if self.writeBeams:
blockDefinitions.append((dim - 2, "BEAM", 11))
# Compute once the ordered list of written element types per block. This
# single source of truth is reused for the ET cards, the EBLOCK bodies
# and the element tag offsets, guaranteeing a consistent numbering.
writtenTypesByBlock = {
blockDimensionality: [
elementType for elementType in mesh.elements.keys()
if ED.dimensionality[elementType] == blockDimensionality and IsWritten(elementType)
]
for blockDimensionality, _, _ in blockDefinitions
}
elementCounter = 1
self.filePointer.write("/com,*********** Elements for Body ***********\n")
# Only the element types actually written in the CDB file get an Ansys
# element type number. The numbering starts at 1, is contiguous, and
# follows the writing order of the EBLOCK blocks (SOLID, then SHELL,
# then BEAM) so that the ET cards match the order of the elements.
elemTypeNumber = 0
elementTypeNumbers = {}
for blockDimensionality, _, _ in blockDefinitions:
formulationName = writtenFormulations[blockDimensionality]
for elementType in writtenTypesByBlock[blockDimensionality]:
elemTypeNumber += 1
elementTypeNumbers[elementType] = elemTypeNumber
ansysFormulation = MuscatToAnsysFormulation[formulationName].get(elementType, 0)
self.filePointer.write(f"ET,{elemTypeNumber},{ansysFormulation}\n")
if formulationName == "SHELL":
self.filePointer.write(f"SECTYPE,{elemTypeNumber},SHELL\n")
self.filePointer.write(f"SECDATA,{self.shellThickness}\n")
for blockDimensionality, blockKey, numberOfFields in blockDefinitions:
numberOfElements = mesh.GetNumberOfElements(dim=blockDimensionality)
if numberOfElements == 0:
continue
self.filePointer.write(f"EBLOCK,{numberOfFields},{blockKey},,{numberOfElements}\n")
self.filePointer.write(f"({numberOfFields}i9)\n")
for elementType in writtenTypesByBlock[blockDimensionality]:
elements = mesh.elements[elementType]
numberOfNodesPerElement = elements.GetNumberOfNodesPerElement()
mask = exportMask[elementType]
header = "1".rjust(9) # 1 The material number
header += f"{elementTypeNumbers[elementType]}".rjust(9) # 2 The element type number
header += "1".rjust(9) # 3 The real constant number
header += "1".rjust(9) # 4 The real constant number
header += "0".rjust(9) # 5 Element coordinate system number
header += "0".rjust(9) # 6 the birth/death flag
header += "0".rjust(9) # 7 the solid model reference number
header += "0".rjust(9) # 8 the element shape flag
header += f"{len(mask)}".rjust(9) # 9 number of nodes defining this element
header += "0".rjust(9) # 10 Not used (p-elements)
for elementIndex in range(elements.GetNumberOfElements()):
self.filePointer.write(header)
self.filePointer.write(f"{elementCounter}".rjust(9)) # 11 the element number
elementCounter += 1
if numberOfNodesPerElement <= 8:
for localNode in mask:
self.filePointer.write(f"{elements.connectivity[elementIndex, localNode]+1}".rjust(9)) # 12-18 The node ids
self.filePointer.write("\n")
else:
begin = ''.join(f'{str(nodeId):>9}' for nodeId in elements.connectivity[elementIndex, :8] + 1)
end = ''.join(f'{str(nodeId):>9}' for nodeId in elements.connectivity[elementIndex, 8:] + 1)
self.filePointer.write(f"{begin}\n") # 12-18 The node ids
self.filePointer.write(f"{end}\n") # 19+ The remaining node ids
self.filePointer.write("-1\n")
registeredTags = []
for tag in mesh.nodesTags:
if len(tag) == 0:
continue
tagName = tag.name
suffixIndex = 0
while tagName in registeredTags:
tagName = tag.name + "_NODAL_" + str(suffixIndex)
suffixIndex += 1
self.WriteNamedSelection(tagName, "NODE", tag.GetIds() + 1)
registeredTags.append(tagName)
# Only the element types actually written in the EBLOCK(s) above get a
# global id. The offset of each written type must match the sequential
# numbering used while writing the blocks (SOLID, then SHELL, then
# BEAM). Tags defined on element types that are not written must be
# ignored, otherwise the CMBLOCK would reference element ids that do
# not exist in the file.
writtenElementOffsets = {}
writtenElementCounter = 0
for blockDimensionality, _, _ in blockDefinitions:
for elementType in writtenTypesByBlock[blockDimensionality]:
writtenElementOffsets[elementType] = writtenElementCounter
writtenElementCounter += mesh.elements[elementType].GetNumberOfElements()
for elementType, elements in mesh.elements.items():
if elementType not in writtenElementOffsets:
continue
elementOffset = writtenElementOffsets[elementType]
for tag in elements.tags:
if len(tag) == 0:
continue
tagName = tag.name
suffixIndex = 0
while tagName in registeredTags:
tagName = tag.name + "_ELEM_" + str(suffixIndex)
suffixIndex += 1
self.WriteNamedSelection(tagName, "ELEM", tag.GetIds() + elementOffset + 1)
registeredTags.append(tagName)
if remotePoints:
nbRemotePoints = 0
elementTypeId = 5
# self.filePointer.write("""
# ! --------------------------------------------------
# ! Remote Point native element definitions
# ! --------------------------------------------------
# ET,170,170 ! Pilot element
# ET,174,174 ! Contact / MPC element
# ! --- Pilot element options
# KEYOPT,170,2,1 ! Do not fix pilot node
# KEYOPT,170,4,0 ! All DOFs active
# ! --- Contact / MPC options (Rigid Remote Point)
# KEYOPT,174,12,5 ! Bonded
# KEYOPT,174,4,2 ! Rigid CERIG-style
# KEYOPT,174,2,2 ! MPC formulation
# """)
for remotePoint in remotePoints.values():
pilotNodeId = remotePoint["ID"]
self.filePointer.write(f"/com,*********** Create Remote Point {remotePoint['tag']} ***********\n")
self.filePointer.write(f"""*set,tid,{elementTypeId+1}
*set,cid,{elementTypeId}
et,cid,174
et,tid,170
keyo,tid,2,1 ! Don't fix the pilot node
keyo,tid,4,0 ! Activate all DOF's due to large deformation
keyo,cid,12,5 ! Bonded Contact
keyo,cid,4,2 ! Rigid CERIG style load
keyo,cid,2,2 ! MPC style contact
MAT,1 $ TYPE,1 $ REAL,1 $ SECNUM,1 $ ESYS,0
""")
for faceElementType, faceData, faceGlobalIds in ElementFilter(dimensionality=dim-1)(mesh):
mask = exportMask[faceElementType]
tag = faceData.GetTag(remotePoint["tag"])
self.filePointer.write(f"EBLOCK,11,COMPACT,,{tag.cpt}\n")
self.filePointer.write("(11i9)\n")
for faceId in tag.ids:
self.filePointer.write(f"{elementCounter}".rjust(9))
elementCounter += 1
for localNode in mask:
self.filePointer.write(f"{faceData.connectivity[faceId, localNode]+1}".rjust(9)) # 12-18 the node ids
self.filePointer.write("\n")
self.filePointer.write("-1\n")
break
self.filePointer.write(f"""secnum,1
*set,_npilot,{pilotNodeId}
*_npilot{10500+3*nbRemotePoints}=_npilot
type,tid
mat ,cid
real,cid
tshape,pilo
en,{elementCounter},_npilot
tshape
""")
nbRemotePoints += 1
elementCounter += 1
elementTypeId += 2
# *set,tid,6
# *set,cid,5
# et,cid,174
# et,tid,170
# keyo,tid,2,1 ! Don't fix the pilot node
# keyo,tid,4,0 ! Activate all DOF's due to large deformation
# keyo,cid,12,5 ! Bonded Contact
# keyo,cid,4,2 ! Rigid CERIG style load
# keyo,cid,2,2 ! MPC style contact
# MAT,5 $ TYPE,5 $ REAL,5 $ SECNUM,5 $ ESYS,0
# eblock,11,COMPACT,,176
# (11i9)
# [...]
# -1
# secnum,1 ! reset the section ID to default
# *set,_npilot,3664313
# _npilot10436=_npilot ??? 36 39 42 45 48 51
# type,tid
# mat ,cid
# real,cid
# tshape,pilo
# en,2873820,_npilot ! nombre d'éléments max +1
# tshape
self.filePointer.write("/GO\nFINISH\n")
[docs]
def WriteFromFile(self, mesh: Mesh, file: Path) -> None:
"""Update an existing CDB file with new mesh information.
The nodes block and the element block of the input file are replaced
with the data coming from ``mesh``; all other lines are copied as is.
Parameters
----------
mesh : Mesh
The mesh providing the new node positions and connectivity.
file : Path
Path to the existing CDB file to update.
"""
from Muscat.IO.AnsysTools import MuscatToAnsysFormulation
mesh.PrepareForOutput()
dim = mesh.GetElementsDimensionality()
with open(file, 'r') as inputFile:
lines = inputFile.readlines()
lineIndex = 0
while lineIndex < len(lines):
if lines[lineIndex].startswith(f"nblock,{dim},,"):
numberOfPoints = mesh.GetNumberOfNodes()
self.filePointer.write(f"nblock,{dim},,{numberOfPoints}\n")
self.filePointer.write("(1i9,3e20.9e3)\n")
nodePositions = mesh.GetPosOfNodes()
for nodeIndex in range(numberOfPoints):
self.filePointer.write(f"{nodeIndex+1}".rjust(9))
for axis in range(dim):
self.filePointer.write(f"{nodePositions[nodeIndex, axis]:1.9E}".rjust(20))
self.filePointer.write("\n")
for tag in mesh.nodesTags:
if len(tag) == 0:
continue
self.filePointer.write(f"CMBLOCK,{tag.name},NODE,{len(tag)}\n")
self.filePointer.write("(8i10)\n")
indices = tag.GetIds()
for tagNodeIndex in range(len(tag)):
self.filePointer.write(f"{indices[tagNodeIndex]+1}".rjust(10))
self.filePointer.write("\n")
lineIndex += int(lines[lineIndex].split(",")[-1]) + 2
elif lines[lineIndex].startswith("et,1,"):
elementCounter = 1
# Only the body element types get an ET card, with a
# contiguous numbering starting at 1 (consistent with the
# numbering used by the Write method).
elemTypeNumber = 0
for elementType, elements in mesh.elements.items():
numberOfElements = elements.GetNumberOfElements()
numberOfNodesPerElement = elements.GetNumberOfNodesPerElement()
elementDimensionality = ED.dimensionality[elementType]
if dim == elementDimensionality:
elemTypeNumber += 1
self.filePointer.write(f"et,{elemTypeNumber},{MuscatToAnsysFormulation[self.AnsysFormulation[dim]].get(elementType, 0)}\n")
self.filePointer.write(lines[lineIndex + 1])
solidKey = "COMPACT"
numberOfFields = 11
header = ""
sectionHeader = ""
self.filePointer.write(f"eblock,{numberOfFields},{solidKey},,{numberOfElements}\n")
self.filePointer.write(f"({numberOfFields}i9)\n")
for elementIndex in range(numberOfElements):
self.filePointer.write(header) # 1 The material number
self.filePointer.write(f"{elementCounter}".rjust(9)) # 11 the element number
self.filePointer.write(sectionHeader)
elementCounter += 1
for localNode in range(numberOfNodesPerElement):
self.filePointer.write(f"{elements.connectivity[elementIndex, localNode]+1}".rjust(9)) # 12-18 the node ids
self.filePointer.write("\n")
lineIndex += int(lines[lineIndex + 1].split(",")[-1]) + 4
else:
self.filePointer.write(lines[lineIndex])
lineIndex += 1
[docs]
def CheckIntegrityCompactIndices() -> str:
"""Check the compaction of element indices into the CDB compact form.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
writer = AnsysWriter()
# A consecutive run of more than two indices is compacted to ``begin, -end``.
lines = writer.CompactIndices(np.array([1, 2, 3, 4, 5]))
assert lines == [[1, -5]], f"unexpected compaction for a run: {lines}"
# A pair of consecutive indices is kept as is.
lines = writer.CompactIndices(np.array([7, 8]))
assert lines == [[7, 8]], f"unexpected compaction for a pair: {lines}"
# An isolated index is kept as is.
lines = writer.CompactIndices(np.array([42]))
assert lines == [[42]], f"unexpected compaction for a single index: {lines}"
# Indices are sorted before compaction.
lines = writer.CompactIndices(np.array([3, 1, 2]))
assert lines == [[1, -3]], f"indices were not sorted before compaction: {lines}"
# The result is split into lines of at most 8 values.
isolatedIndices = np.array([1, 3, 5, 7, 9, 11, 13, 15, 17])
lines = writer.CompactIndices(isolatedIndices)
assert all(len(line) <= 8 for line in lines), f"a line exceeds 8 values: {lines}"
assert sum(len(line) for line in lines) == isolatedIndices.size
return "ok"
[docs]
def CheckIntegritySliceRidges() -> str:
"""Check the construction of ridge-node chains from ``Bar_2`` elements.
The open chain, the empty mesh (no ``Ridges`` tag) and the closed loop
(cycle) cases are all exercised.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshContainers.Mesh import Mesh
writer = AnsysWriter()
# An empty mesh without a "Ridges" tag returns an empty list.
assert writer.SliceRidges(Mesh()) == [], "an empty mesh should give no ridge"
# Build an open chain of 150 consecutive bars (151 nodes).
numberOfNodes = 151
mesh = Mesh()
mesh.nodes = np.zeros((numberOfNodes, 3), dtype=float)
mesh.nodes[:, 0] = np.arange(numberOfNodes)
mesh.originalIDNodes = np.arange(numberOfNodes)
connectivity = np.array([[i, i + 1] for i in range(numberOfNodes - 1)])
bars = mesh.GetElementsOfType(ED.Bar_2)
bars.AddNewElements(connectivity, np.arange(connectivity.shape[0]))
bars.tags.CreateTag("Ridges").SetIds(np.arange(connectivity.shape[0]))
mesh.PrepareForOutput()
ridges = writer.SliceRidges(mesh)
# Every node of the chain must appear and each ridge is at most 100 nodes.
assert all(len(ridge) <= 100 for ridge in ridges), f"a ridge exceeds 100 nodes: {ridges}"
totalNodes = sum(len(ridge) for ridge in ridges)
assert totalNodes >= numberOfNodes, f"missing nodes in ridges: {totalNodes} < {numberOfNodes}"
# Consecutive nodes in a ridge must be connected by a bar element.
for ridge in ridges:
for first, second in zip(ridge[:-1], ridge[1:]):
assert abs(first - second) == 1, f"non consecutive nodes {first}, {second}"
# Build a closed loop (cycle) of 6 bars where every node has 2 bars.
numberOfLoopNodes = 6
loopMesh = Mesh()
loopMesh.nodes = np.zeros((numberOfLoopNodes, 3), dtype=float)
loopMesh.nodes[:, 0] = np.arange(numberOfLoopNodes)
loopMesh.originalIDNodes = np.arange(numberOfLoopNodes)
loopConnectivity = np.array([[i, (i + 1) % numberOfLoopNodes] for i in range(numberOfLoopNodes)])
loopBars = loopMesh.GetElementsOfType(ED.Bar_2)
loopBars.AddNewElements(loopConnectivity, np.arange(loopConnectivity.shape[0]))
loopBars.tags.CreateTag("Ridges").SetIds(np.arange(loopConnectivity.shape[0]))
loopMesh.PrepareForOutput()
loopRidges = writer.SliceRidges(loopMesh)
assert len(loopRidges) >= 1, "the closed loop produced no ridge"
assert sum(len(ridge) for ridge in loopRidges) >= numberOfLoopNodes
return "ok"
[docs]
def CheckIntegrityWrite() -> str:
"""Check the writing of a tetrahedral mesh in the CDB format.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshTools.MeshCreationTools import CreateCube
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
mesh = CreateCube(dimensions=[3, 2, 2], ofTetras=True)
mesh.GenerateManufacturedOriginalIDs()
tempDirectory = TemporaryDirectory.GetTempPath()
fileName = tempDirectory + "AnsysWriterCheckIntegrity_write.cdb"
WriteMeshToCDB(fileName, mesh)
with open(fileName, "r") as fileHandle:
content = fileHandle.read()
assert "NBLOCK" in content, "no NBLOCK block in the written file"
assert "EBLOCK" in content, "no EBLOCK block in the written file"
assert f"NUMOFF,NODE,{mesh.GetNumberOfNodes():>9}" in content, "wrong node count"
# NUMOFF,ELEM must reflect only the body elements actually written (the
# skin faces of the cube are not written when the shell flag is off).
numberOfBodyElements = mesh.GetNumberOfElements(dim=mesh.GetElementsDimensionality())
assert f"NUMOFF,ELEM,{numberOfBodyElements:>9}" in content, "wrong written element count"
assert content.strip().endswith("FINISH"), "the file does not end with FINISH"
# Tags defined on the body elements (e.g. "3D") are exported as element
# named selections, while tags defined on the skin faces (e.g. "X0") are
# not, because those face elements are not written in the EBLOCK.
assert "CMBLOCK,3D,ELEM" in content, "the body element tag was not exported"
assert "CMBLOCK,X0,ELEM" not in content, "a face element tag was wrongly exported"
return "ok"
[docs]
def CheckIntegrityWriteQuadratic() -> str:
"""Check the writing of quadratic elements (more than 8 nodes per element).
This exercises the ``Tetrahedron_10`` branch where the connectivity is
written on two lines.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshContainers.Mesh import Mesh
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
# A single 10-node tetrahedron (4 corners + 6 edge nodes).
nodes = np.array([
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.5, 0.0, 0.0],
[0.5, 0.5, 0.0],
[0.0, 0.5, 0.0],
[0.0, 0.0, 0.5],
[0.5, 0.0, 0.5],
[0.0, 0.5, 0.5],
], dtype=float)
mesh = Mesh()
mesh.nodes = nodes
mesh.originalIDNodes = np.arange(nodes.shape[0])
elements = mesh.GetElementsOfType(ED.Tetrahedron_10)
elements.AddNewElement(np.arange(10), 0)
mesh.PrepareForOutput()
tempDirectory = TemporaryDirectory.GetTempPath()
fileName = tempDirectory + "AnsysWriterCheckIntegrity_quadratic.cdb"
WriteMeshToCDB(fileName, mesh)
with open(fileName, "r") as fileHandle:
lines = fileHandle.readlines()
# The element connectivity of a Tetrahedron_10 spans two lines: the first
# holds the header and 8 nodes, the second holds the 2 remaining nodes.
blockLines = [line for line in lines if line.strip() and line.strip()[0].isdigit()]
assert len(blockLines) >= 2, "the quadratic element was not written on two lines"
return "ok"
[docs]
def CheckIntegrityWriteMultipleTypes() -> str:
"""Check the writing of several element types of the same dimensionality.
A mesh mixing hexahedra and tetrahedra (two 3D types) must produce two
``ET`` cards, a single SOLID ``EBLOCK`` and ``NUMOFF,TYPE,2``.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshContainers.Mesh import Mesh
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
nodes = np.zeros((12, 3), dtype=float)
nodes[:, 0] = np.arange(12)
mesh = Mesh()
mesh.nodes = nodes
mesh.originalIDNodes = np.arange(12)
mesh.GetElementsOfType(ED.Hexahedron_8).AddNewElement([0, 1, 2, 3, 4, 5, 6, 7], 0)
mesh.GetElementsOfType(ED.Tetrahedron_4).AddNewElement([8, 9, 10, 11], 1)
mesh.PrepareForOutput()
tempDirectory = TemporaryDirectory.GetTempPath()
fileName = tempDirectory + "AnsysWriterCheckIntegrity_multitype.cdb"
WriteMeshToCDB(fileName, mesh)
with open(fileName, "r") as fileHandle:
content = fileHandle.read()
assert "NUMOFF,TYPE,2" in content, "the two element types were not counted"
assert "ET,1," in content and "ET,2," in content, "the two ET cards were not written"
# The two 3D types share a single SOLID EBLOCK holding both elements.
assert "EBLOCK,19,SOLID,,2" in content, "the two 3D types were not written in one block"
return "ok"
[docs]
def CheckIntegrityWriteWithRemotePoints() -> str:
"""Check the writing of a mesh together with remote points.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshTools.MeshCreationTools import CreateCube
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
mesh = CreateCube(dimensions=[3, 2, 2], ofTetras=True)
mesh.GenerateManufacturedOriginalIDs()
# "X0" is a face (2D) tag created by CreateCube and used as remote point.
remotePoints = {
0: {"tag": "X0", "coords": (0.0, 0.0, 0.0)},
}
tempDirectory = TemporaryDirectory.GetTempPath()
fileName = tempDirectory + "AnsysWriterCheckIntegrity_remote.cdb"
WriteMeshToCDB(fileName, mesh, remotePoints=remotePoints)
with open(fileName, "r") as fileHandle:
content = fileHandle.read()
assert "Remote Point" in content, "no remote point block in the written file"
assert "tshape,pilo" in content, "the pilot node definition is missing"
# The pilot node id is stored back in the remote point dictionary.
assert "ID" in remotePoints[0], "the pilot node id was not stored in the dictionary"
return "ok"
[docs]
def CheckIntegrityWriteWithShells() -> str:
"""Check the writing of the surface elements and their tags.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshTools.MeshCreationTools import CreateCube
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
mesh = CreateCube(dimensions=[3, 2, 2], ofTetras=True)
mesh.GenerateManufacturedOriginalIDs()
tempDirectory = TemporaryDirectory.GetTempPath()
# Without the flag, the face tag "X0" is not exported.
withoutFileName = tempDirectory + "AnsysWriterCheckIntegrity_noshell.cdb"
WriteMeshToCDB(withoutFileName, mesh, writeShells=False)
with open(withoutFileName, "r") as fileHandle:
contentWithout = fileHandle.read()
assert "CMBLOCK,X0,ELEM" not in contentWithout, "faces should not be written without the flag"
# With the flag, a SHELL block is written and the face tag is exported.
withFileName = tempDirectory + "AnsysWriterCheckIntegrity_shell.cdb"
WriteMeshToCDB(withFileName, mesh, writeShells=True)
with open(withFileName, "r") as fileHandle:
contentWith = fileHandle.read()
assert "EBLOCK,11,SHELL" in contentWith, "no SHELL block written with the flag"
assert "SECTYPE," in contentWith, "no SECTYPE card written for the shells"
assert "CMBLOCK,X0,ELEM" in contentWith, "the face tag was not exported with the flag"
return "ok"
[docs]
def CheckIntegrityWriteWithBeams() -> str:
"""Check the writing of the beam elements and their tags.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshContainers.Mesh import Mesh
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
# A mesh with one body tetrahedron (dim 3) and one bar (dim 1 = dim - 2).
nodes = np.array([
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
], dtype=float)
mesh = Mesh()
mesh.nodes = nodes
mesh.originalIDNodes = np.arange(nodes.shape[0])
tetras = mesh.GetElementsOfType(ED.Tetrahedron_4)
tetras.AddNewElement([0, 1, 2, 3], 0)
bars = mesh.GetElementsOfType(ED.Bar_2)
bars.AddNewElement([0, 1], 1)
bars.tags.CreateTag("Cable").SetIds([0])
mesh.PrepareForOutput()
tempDirectory = TemporaryDirectory.GetTempPath()
# Without the flag, the bar tag "Cable" is not exported.
withoutFileName = tempDirectory + "AnsysWriterCheckIntegrity_nobeam.cdb"
WriteMeshToCDB(withoutFileName, mesh, writeBeams=False)
with open(withoutFileName, "r") as fileHandle:
contentWithout = fileHandle.read()
assert "CMBLOCK,Cable,ELEM" not in contentWithout, "beams should not be written without the flag"
# With the flag, a BEAM block is written and the bar tag is exported.
withFileName = tempDirectory + "AnsysWriterCheckIntegrity_beam.cdb"
WriteMeshToCDB(withFileName, mesh, writeBeams=True)
with open(withFileName, "r") as fileHandle:
contentWith = fileHandle.read()
assert "EBLOCK,11,BEAM" in contentWith, "no BEAM block written with the flag"
assert "CMBLOCK,Cable,ELEM" in contentWith, "the beam tag was not exported with the flag"
return "ok"
[docs]
def CheckIntegrityWriteFromFile() -> str:
"""Check the update of an existing CDB file with new mesh information.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.MeshTools.MeshCreationTools import CreateCube
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
from Muscat.TestData import GetTestDataPath
mesh = CreateCube(dimensions=[3, 2, 2], ofTetras=True)
mesh.GenerateManufacturedOriginalIDs()
tempDirectory = TemporaryDirectory.GetTempPath()
fileName = tempDirectory + "AnsysWriterCheckIntegrity_fromfile.cdb"
WriteMeshToCDB(fileName, mesh, GetTestDataPath() + "exemple.cdb")
with open(fileName, "r") as fileHandle:
content = fileHandle.read()
assert "nblock" in content.lower(), "no nblock block in the updated file"
return "ok"
[docs]
def CheckIntegrity(GUI: bool = False) -> str:
"""Run all the functional checks of the Ansys CDB writer.
Parameters
----------
GUI : bool, optional
Unused, kept for API compatibility with the other modules, by default
False.
Returns
-------
str
``"ok"`` if every check succeeds.
"""
CheckIntegrityCompactIndices()
CheckIntegritySliceRidges()
CheckIntegrityExportElementTags()
CheckIntegrityWrite()
CheckIntegrityWriteQuadratic()
CheckIntegrityWriteMultipleTypes()
CheckIntegrityWriteWithRemotePoints()
CheckIntegrityWriteWithShells()
CheckIntegrityWriteWithBeams()
CheckIntegrityWriteFromFile()
return "ok"
if __name__ == '__main__':
print(CheckIntegrity(True)) # pragma: no cover