# -*- 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 ``ds.dat`` reader.
This module provides helpers to extract information from an Ansys
``ds.dat`` input file:
* :func:`GetRemotePoints` recovers the remote points (pilot nodes) together
with their named selection and coordinates.
* :func:`GetPressureLoadingElementalSurface` recovers the surface effect
elements (``SURF154``) used to define pressure loadings.
"""
import re
import copy
from typing import Dict, List, Optional, Tuple
import numpy as np
#: Regular expression matching a node coordinate line
#: (``nodeId x y z``) of an ``nblock``.
_COORDINATE_PATTERN = re.compile(
r"^\s*(\d+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)"
)
#: Regular expression matching a ``Create Remote Point`` comment line.
_CREATE_REMOTE_POINT_PATTERN = re.compile(r'Create Remote Point\s+"([^"]+)"')
#: Regular expression matching a ``Remote Point Used by`` comment line.
_REMOTE_POINT_USED_BY_PATTERN = re.compile(r'Remote Point Used by\s+"([^"]+)"')
#: Regular expression matching a ``*set,_npilot,<id>`` command.
_PILOT_NODE_PATTERN = re.compile(r"\*set,_npilot,(\d+)\s*$")
def _ResolveDatFilePath(file: str) -> str:
"""Resolve the path of the ``ds.dat`` file to read.
The input may be a ``.rst`` result file, a directory, or a ``.dat`` file.
When a ``.rst`` file or a directory is given, the companion ``ds.dat`` file
located in the same directory is targeted instead.
Parameters
----------
file : str
Path to a ``.rst`` result file, a directory, or a ``.dat`` file.
Returns
-------
str
Path to the ``ds.dat`` file to read.
Raises
------
ValueError
If ``file`` has an extension other than ``.dat``.
"""
if ".rst" in file:
rstName = re.search(r"[\w.rst]*$", file).group()
return file.replace(rstName, "/ds.dat")
if ".dat" not in file and "." not in file:
return file + "/ds.dat"
if ".dat" not in file and "." in file:
raise ValueError(f"The file ({file}) must be a .dat file type!")
return file
def _ReadNodeBlockLines(file: str) -> List[str]:
"""Read and return the raw lines of the ``nblock`` of a ``ds.dat`` file.
Parameters
----------
file : str
Path to the ``ds.dat`` file to read.
Returns
-------
list of str
Lines contained in the ``nblock`` (node coordinate block), without the
trailing end-of-line characters.
"""
nodeBlockLines: List[str] = []
with open(file, "r") as fileHandle:
insideNodeBlock = False
for line in fileHandle:
if line.startswith("nblock"):
insideNodeBlock = True
continue
if insideNodeBlock and line.startswith("-1"):
insideNodeBlock = False
if insideNodeBlock:
nodeBlockLines.append(line.rstrip())
return nodeBlockLines
def _ReadRemotePointNames(file: str) -> Dict[int, str]:
"""Read the pilot node id of every remote point together with its name.
Parameters
----------
file : str
Path to the ``ds.dat`` file to read.
Returns
-------
dict
Mapping from a remote point pilot node id to its named selection.
"""
remotePointNames: Dict[int, str] = {}
with open(file, "r") as fileHandle:
lines = iter(fileHandle)
for line in lines:
match = _CREATE_REMOTE_POINT_PATTERN.search(line)
if not match:
continue
namedSelection = match.group(1)
for remoteLine in lines:
usedByMatch = _REMOTE_POINT_USED_BY_PATTERN.search(remoteLine)
if usedByMatch:
namedSelection = usedByMatch.group(1)
continue
pilotMatch = _PILOT_NODE_PATTERN.search(remoteLine)
if pilotMatch:
remotePointId = int(pilotMatch.group(1))
remotePointNames[remotePointId] = namedSelection
break
return remotePointNames
[docs]
def GetRemotePoints(file: str) -> Dict[int, Dict[str, object]]:
"""Read the remote points (pilot nodes) of an Ansys ``ds.dat`` file.
Parameters
----------
file : str
Path to a ``.rst`` result file, a directory, or a ``ds.dat`` file. When
a ``.rst`` file or a directory is given, the companion ``ds.dat`` file
located in the same directory is read.
Returns
-------
dict
Mapping from a remote point pilot node id to a dictionary with two
keys: ``"tag"`` (the named selection of the remote point) and
``"coords"`` (a ``(x, y, z)`` tuple with its coordinates).
Raises
------
ValueError
If ``file`` has an extension other than ``.dat``.
"""
file = _ResolveDatFilePath(file)
remotePointNames = _ReadRemotePointNames(file)
nodeBlockLines = _ReadNodeBlockLines(file)
coordinates: Dict[int, Dict[str, object]] = {}
for line in nodeBlockLines:
match = _COORDINATE_PATTERN.match(line)
if not match:
continue
nodeId = int(match.group(1))
if nodeId in remotePointNames:
x = float(match.group(2))
y = float(match.group(3))
z = float(match.group(4))
coordinates[nodeId] = {
"tag": remotePointNames[nodeId],
"coords": (x, y, z),
}
return coordinates
def _ReadSurfaceEffectElements(fileHandle, etLine: str) -> Tuple[str, int, Dict[int, List[int]], List[List[int]]]:
"""Read a block of surface effect elements (``SURF154``).
The Ansys element type is extracted from ``etLine`` (the ``et`` card of the
block), then the block is read up to the ``(15i9)`` connectivity format
line and the element connectivity lines are consumed.
Parameters
----------
fileHandle : TextIO
File handle positioned just after the ``et`` card of the block.
etLine : str
The ``et`` card line of the block (e.g. ``"et,3,154"``).
Returns
-------
tuple
A tuple ``(ansysElementType, numberOfElements, surfaceElements,
keyOptions)`` where ``ansysElementType`` is the Ansys element type
string, ``numberOfElements`` the number of surface elements,
``surfaceElements`` a mapping from a surface element id to its node
ids, and ``keyOptions`` the list of ``[keyOptionId, value]`` pairs.
"""
ansysElementType = re.search(r"[\w.-]+\n", etLine).group().replace("\n", "")
numberOfElements = 0
keyOptions: List[List[int]] = []
nextLine = fileHandle.readline()
while "(15i9)" not in nextLine:
if "eblock" in nextLine and ",,," in nextLine:
numberOfElements = int(re.search(r"[\w.-]+\n", nextLine).group())
if "keyop" in nextLine:
keys = re.search(r",[\w.,-]+\s", nextLine).group().split(",")
keyOptions.append([int(keys[2]), int(keys[3])])
nextLine = fileHandle.readline()
surfaceElements: Dict[int, List[int]] = {}
definedElements = 0
while definedElements < numberOfElements:
fields = np.array(fileHandle.readline().split())
surfaceElementId = int(fields[0])
surfaceElements[surfaceElementId] = [int(node) for node in fields[5:]]
definedElements += 1
return ansysElementType, numberOfElements, surfaceElements, keyOptions
def _ExtractLoadName(line: str) -> str:
"""Extract and normalise the load name of a comment line.
Parameters
----------
line : str
Comment line containing a quoted load name.
Returns
-------
str
The load name with the surrounding quotes removed and the spaces
replaced by underscores.
"""
return re.search(r"\"[\w ]+\"", line).group().replace('"', "").replace(" ", "_")
[docs]
def GetPressureLoadingElementalSurface(file: str) -> Optional[Tuple[List[dict], Dict[str, str]]]:
"""Read the pressure loading surface effect elements of a ``ds.dat`` file.
The surface effect elements (``SURF154``) used to define pressure loadings
(or imported loads) are recovered together with their key options and the
mapping between an imported pressure load and its table name.
Parameters
----------
file : str
Path to a ``.rst`` result file, a directory, or a ``ds.dat`` file. When
a ``.rst`` file or a directory is given, the companion ``ds.dat`` file
located in the same directory is read.
Returns
-------
tuple or None
A tuple ``(loadings, surfaceNames)`` where ``loadings`` is a list of
dictionaries describing each surface effect element block (keys
``"load_type_name"``, ``"n_elem"``, ``"el_type"``,
``"surface_elements"``, ``"key_opt"`` and ``"load_name"``), and
``surfaceNames`` a mapping from an imported pressure load name to its
table name. ``None`` is returned when nothing could be read.
Raises
------
ValueError
If ``file`` has an extension other than ``.dat``.
"""
file = _ResolveDatFilePath(file)
loadings: List[dict] = []
info: Dict[str, object] = {}
surfaceNames: Dict[str, str] = {}
with open(file, "r") as fileHandle:
for line in fileHandle:
if "Define Pressure Using Surface Effect Elements" in line:
loadName = _ExtractLoadName(line)
etLine = fileHandle.readline()
if "et" not in etLine or ",154" not in etLine:
continue
(
ansysElementType,
numberOfElements,
surfaceElements,
keyOptions,
) = _ReadSurfaceEffectElements(fileHandle, etLine)
nextLine = fileHandle.readline()
while "esel,all" not in nextLine:
if "keyop" in nextLine:
keys = re.search(r",[\w.,-]+\s", nextLine).group().split(",")
keyOptions.append([int(keys[2]), int(keys[3])])
nextLine = fileHandle.readline()
fileHandle.readline()
dimLine = fileHandle.readline()
if "*DIM," in dimLine:
tableName = re.search(r",[\w ]+,", dimLine).group().replace(",", "").strip()
info["load_name"] = tableName.upper()
info["load_type_name"] = loadName.upper()
info["n_elem"] = numberOfElements
info["el_type"] = ansysElementType
info["surface_elements"] = surfaceElements
info["key_opt"] = keyOptions
loadings.append(copy.copy(info))
elif "Define Surface Elements for Imported Load" in line:
loadName = _ExtractLoadName(line)
etLine = fileHandle.readline()
if "et" not in etLine or ",154" not in etLine:
continue
(
ansysElementType,
numberOfElements,
surfaceElements,
keyOptions,
) = _ReadSurfaceEffectElements(fileHandle, etLine)
info["load_type_name"] = loadName.upper()
info["n_elem"] = numberOfElements
info["el_type"] = ansysElementType
info["surface_elements"] = surfaceElements
info["key_opt"] = keyOptions
info["load_name"] = ""
loadings.append(copy.copy(info))
if "/com,*********** Create Imported Load" in line and "Pressure" in line:
loadName = _ExtractLoadName(line)
tableLine = fileHandle.readline()
tableName = re.search(r",[\w ]+,", tableLine).group().replace(",", "").strip()
surfaceNames[loadName.upper()] = tableName.upper()
if not loadings and not surfaceNames:
return None
return loadings, surfaceNames
[docs]
def CheckIntegrity() -> str:
"""Check the integrity of the DatReader module.
Every public and private helper of the module is exercised on synthetic
``ds.dat`` files created in a temporary directory.
Returns
-------
str
``"ok"`` if every assertion succeeds.
"""
from Muscat.Helpers.CheckTools import MustFailFunctionWith
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
# ------------------------------------------------------------------
# _ResolveDatFilePath
# ------------------------------------------------------------------
assert _ResolveDatFilePath("/some/dir/file.rst").endswith("/ds.dat")
assert _ResolveDatFilePath("/some/directory") == "/some/directory/ds.dat"
assert _ResolveDatFilePath("/some/dir/ds.dat").endswith("ds.dat")
MustFailFunctionWith(ValueError, _ResolveDatFilePath, "/some/file.txt")
# ------------------------------------------------------------------
# _ExtractLoadName
# ------------------------------------------------------------------
assert _ExtractLoadName('/com, "My Load"') == "My_Load"
tempDirectory = TemporaryDirectory.GetTempPath()
# ------------------------------------------------------------------
# GetRemotePoints
# ------------------------------------------------------------------
remoteContent = (
'/com, Create Remote Point "RP_A"\n'
'/com, Remote Point Used by "Selection_A"\n'
"*set,_npilot,101\n"
"/com, Create Remote Point \"RP_B\"\n"
"*set,_npilot,102\n"
"nblock,3,,3\n"
"(1i9,3e20.9e3)\n"
" 101 0.000000000E+00 1.000000000E+00 2.000000000E+00\n"
" 102 3.000000000E+00 4.000000000E+00 5.000000000E+00\n"
" 103 9.000000000E+00 9.000000000E+00 9.000000000E+00\n"
"-1\n"
)
remoteFileName = tempDirectory + "DatReaderCheckIntegrity_ds.dat"
with open(remoteFileName, "w", encoding="utf-8") as fileHandle:
fileHandle.write(remoteContent)
remotePoints = GetRemotePoints(remoteFileName)
assert set(remotePoints.keys()) == {101, 102}, remotePoints
assert remotePoints[101]["tag"] == "Selection_A"
assert remotePoints[102]["tag"] == "RP_B"
assert remotePoints[101]["coords"] == (0.0, 1.0, 2.0)
assert remotePoints[102]["coords"] == (3.0, 4.0, 5.0)
# The directory form must reach the same companion ds.dat file.
directoryRemotePoints = GetRemotePoints(tempDirectory + "DatReaderCheckIntegrity_ds.dat")
assert set(directoryRemotePoints.keys()) == {101, 102}
# ------------------------------------------------------------------
# GetPressureLoadingElementalSurface
# ------------------------------------------------------------------
pressureContent = (
'/com, Define Pressure Using Surface Effect Elements "Face Load"\n'
"et,3,154\n"
"keyop,3,1,2\n"
"eblock,10,solid,,,2\n"
"(15i9)\n"
" 1 1 1 1 1 11 12 13 14\n"
" 2 1 1 1 1 21 22 23 24\n"
"keyop,3,5,7\n"
"esel,all\n"
"skip\n"
"*DIM, PRESS_TABLE ,TABLE,1\n"
'/com,*********** Create Imported Load "Face Load" Pressure ***********\n'
", PRESS_TABLE ,\n"
'/com, Define Surface Elements for Imported Load "Imported"\n'
"et,4,154\n"
"eblock,10,solid,,,1\n"
"(15i9)\n"
" 3 1 1 1 1 31 32 33 34\n"
)
pressureFileName = tempDirectory + "DatReaderCheckIntegrity_pressure.dat"
with open(pressureFileName, "w", encoding="utf-8") as fileHandle:
fileHandle.write(pressureContent)
result = GetPressureLoadingElementalSurface(pressureFileName)
assert result is not None
loadings, surfaceNames = result
assert len(loadings) == 2, loadings
pressureLoading = loadings[0]
assert pressureLoading["load_type_name"] == "FACE_LOAD"
assert pressureLoading["n_elem"] == 2
assert pressureLoading["el_type"] == "154"
assert pressureLoading["surface_elements"][1] == [11, 12, 13, 14]
assert pressureLoading["surface_elements"][2] == [21, 22, 23, 24]
assert [1, 2] in pressureLoading["key_opt"]
assert [5, 7] in pressureLoading["key_opt"]
assert pressureLoading["load_name"] == "PRESS_TABLE"
importedLoading = loadings[1]
assert importedLoading["load_type_name"] == "IMPORTED"
assert importedLoading["n_elem"] == 1
assert importedLoading["surface_elements"][3] == [31, 32, 33, 34]
assert importedLoading["load_name"] == ""
assert surfaceNames == {"FACE_LOAD": "PRESS_TABLE"}, surfaceNames
# A file without any pressure information returns None.
emptyFileName = tempDirectory + "DatReaderCheckIntegrity_empty.dat"
with open(emptyFileName, "w", encoding="utf-8") as fileHandle:
fileHandle.write("/com, nothing relevant here\n")
assert GetPressureLoadingElementalSurface(emptyFileName) is None
return "ok"
if __name__ == "__main__": # pragma: no cover
print(CheckIntegrity())