Source code for Muscat.IO.AnsysWriter

# -*- 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] def ExportElementTagsInCDBFormat(mesh: Mesh, tagNames: List[str], fileName: Optional[str] = None, filePointer: Optional[TextIOWrapper] = None) -> None: """Export several element tags to the Ansys CDB format. Parameters ---------- mesh : Mesh Mesh from which the element ids of each tag are searched. tagNames : list of str List of the element tag names to export. fileName : str, optional File name (CDB format) where the element ids of each tag are exported, by default None. filePointer : TextIOWrapper, optional Already opened file pointer used to write the tags. When provided, ``fileName`` is ignored, by default None. """ appendMode = False for tagName in tagNames: ExportElementTagInCDBFormat(mesh, tagName, fileName=fileName, append=appendMode, filePointer=filePointer) appendMode = True
[docs] def ExportElementTagInCDBFormat(mesh: Mesh, tagName: str, fileName: Optional[str] = None, append: bool = False, filePointer: Optional[TextIOWrapper] = None) -> None: """Export a single element tag to the Ansys CDB format. The original ids of the elements are used. Parameters ---------- mesh : Mesh Mesh from which the element ids of the tag are searched. tagName : str Name of the element tag to export. fileName : str, optional File name (CDB format) where the element ids of the tag are exported. When None, ``tagName + ".cdb"`` is used, by default None. append : bool, optional If True the information is appended to the file (mode ``"a+"``), otherwise the file is overwritten (mode ``"w"``), by default False. filePointer : TextIOWrapper, optional Already opened file pointer used to write the tag. When provided, ``fileName`` and ``append`` are ignored, by default None. """ globalIds: List[MuscatIndex] = [] elementFilter = ElementFilter(eTag=tagName) for selection in elementFilter(mesh): globalIds.extend(selection.meshOffset + selection.indices) nbElementsInGroup = len(globalIds) if nbElementsInGroup == 0: # pragma: no cover print(f"Empty tag {tagName}") return if filePointer is None: if fileName is None: fileName = tagName + ".cdb" mode = "a+" if append else "w" fileHandle = open(fileName, mode, encoding="utf-8") else: fileHandle = filePointer try: fileHandle.write(f"CMBLOCK,{tagName},ELEM,{nbElementsInGroup}\n") fileHandle.write("(8i10)\n") ids = np.asarray(globalIds) for start in range(0, len(ids), 8): np.savetxt(fileHandle, ids[start:start + 8][np.newaxis], fmt="%10i", delimiter="") finally: if filePointer is None: fileHandle.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 CheckIntegrityExportElementTags() -> str: """Check the export of element tags to the Ansys CDB format. The file output, the append mode and the ``filePointer`` output are all exercised. 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() tagNames = mesh.elements.GetTagsNames() tempDirectory = TemporaryDirectory.GetTempPath() fileName = tempDirectory + "AnsysWriterCheckIntegrity_tags.cdb" ExportElementTagsInCDBFormat(mesh, tagNames, fileName=fileName) with open(fileName, "r") as fileHandle: content = fileHandle.read() assert "CMBLOCK" in content, "no CMBLOCK block written for the element tags" for tagName in tagNames: assert tagName in content, f"tag {tagName} missing in the exported file" # A single tag is written through an already opened file pointer. singleTag = tagNames[0] pointerFileName = tempDirectory + "AnsysWriterCheckIntegrity_tags_pointer.cdb" with open(pointerFileName, "w", encoding="utf-8") as fileHandle: ExportElementTagInCDBFormat(mesh, singleTag, filePointer=fileHandle) with open(pointerFileName, "r") as fileHandle: content = fileHandle.read() assert content.count("CMBLOCK") == 1, "the file pointer output is not as expected" # The append mode adds a second block to an existing file. appendFileName = tempDirectory + "AnsysWriterCheckIntegrity_tags_append.cdb" ExportElementTagInCDBFormat(mesh, singleTag, fileName=appendFileName, append=False) ExportElementTagInCDBFormat(mesh, singleTag, fileName=appendFileName, append=True) with open(appendFileName, "r") as fileHandle: content = fileHandle.read() assert content.count("CMBLOCK") == 2, "the append mode did not add a second block" 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