Source code for Muscat.IO.DatReader

# -*- 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())