From 6c8fc284d303af83cccbd0ea3a0a249e98597d4c Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sat, 29 Jun 2024 01:01:01 -0400 Subject: [PATCH 01/28] Implemented tecplot ascii and binary readers and writers for n-dimensional data a multiple FE zone types --- baseclasses/utils/__init__.py | 10 +- baseclasses/utils/tecplotIO.py | 1537 ++++++++++++++++++++++++++++ tests/input/airfoil_000_slices.dat | 285 ++++++ tests/input/airfoil_000_surf.plt | Bin 0 -> 749920 bytes tests/test_tecplotIO.py | 679 ++++++++++++ 5 files changed, 2508 insertions(+), 3 deletions(-) create mode 100644 baseclasses/utils/tecplotIO.py create mode 100644 tests/input/airfoil_000_slices.dat create mode 100644 tests/input/airfoil_000_surf.plt create mode 100644 tests/test_tecplotIO.py diff --git a/baseclasses/utils/__init__.py b/baseclasses/utils/__init__.py index a79b2be..c35beb1 100644 --- a/baseclasses/utils/__init__.py +++ b/baseclasses/utils/__init__.py @@ -1,8 +1,9 @@ -from .containers import CaseInsensitiveSet, CaseInsensitiveDict +from .containers import CaseInsensitiveDict, CaseInsensitiveSet from .error import Error -from .utils import getPy3SafeString, pp, ParseStringFormat -from .fileIO import writeJSON, readJSON, writePickle, readPickle, redirectIO, redirectingIO +from .fileIO import readJSON, readPickle, redirectingIO, redirectIO, writeJSON, writePickle from .solverHistory import SolverHistory +from .tecplotIO import TecplotFEZone, TecplotOrderedZone, readTecplot, writeTecplot +from .utils import ParseStringFormat, getPy3SafeString, pp __all__ = [ "CaseInsensitiveSet", @@ -17,5 +18,8 @@ "redirectIO", "redirectingIO", "SolverHistory", + "TecplotFEZone", + "TecplotOrderedZone", + "writeTecplot", "ParseStringFormat", ] diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py new file mode 100644 index 0000000..f2edcb4 --- /dev/null +++ b/baseclasses/utils/tecplotIO.py @@ -0,0 +1,1537 @@ +import re +import struct +from abc import ABC, abstractmethod +from enum import Enum +from pathlib import Path +from typing import (Any, Dict, Generic, List, Literal, TextIO, Tuple, TypeVar, + Union) + +import numpy as np +import numpy.typing as npt + +T = TypeVar("T", bound="TecplotZone") + + +# ============================================================================== +# ENUMS +# ============================================================================== +class ZoneType(Enum): + ORDERED = 0 + FELINESEG = 1 + FETRIANGLE = 2 + FEQUADRILATERAL = 3 + FETETRAHEDRON = 4 + FEBRICK = 5 + + +class DataPacking(Enum): + BLOCK = 0 + POINT = 1 + + +class VariableLocation(Enum): + NODE = 0 + CELL_CENTER = 1 + NODE_AND_CELL_CENTER = 2 + + +class DataPrecision(Enum): + SINGLE = 1 + DOUBLE = 2 + + +class DTypePrecision(Enum): + SINGLE = np.float32 + DOUBLE = np.float64 + + +class FileType(Enum): + FULL = 0 + GRID = 1 + SOLUTION = 2 + + +class SectionMarkers(Enum): + ZONE = 299.0 # V11.2 marker + DATA = 357.0 + + +class BinaryFlags(Enum): + NONE = -1 + FALSE = 0 + TRUE = 1 + + +class StrandID(Enum): + PENDING = -2 + STATIC = -1 + + +# ============================================================================== +# DATA STRUCTURES +# ============================================================================== +class TecplotZone: + def __init__(self, name: str, data: Dict[str, npt.NDArray], solutionTime: float = 0.0, strandID: int = -1): + """Base class for Tecplot zones. + + Parameters + ---------- + name : str + The name of the zone. + data : Dict[str, npt.NDArray] + A dictionary of variable names and their corresponding data. + solutionTime : float, optional + The solution time of the zone, by default 0.0 + strandID : int, optional + The strand id of the zone, by default -1 + """ + self.name = name + self.data = data + self.solutionTime = solutionTime + self.strandID = strandID + self._validateShape() + + @property + def variables(self) -> List[str]: + return list(self.data.keys()) + + @property + def shape(self) -> Tuple[int, ...]: + return self.data[self.variables[0]].shape + + @property + def nNodes(self) -> int: + return np.multiply.reduce(self.shape) + + def _validateShape(self) -> None: + """Check that all variables have the same shape. + + Raises + ------ + ValueError + If all variable data arrays do not have the same shape. + """ + if not all(self.data[var].shape == self.shape for var in self.variables): + raise ValueError("All variables must have the same shape.") + + +class TecplotOrderedZone(TecplotZone): + def __init__( + self, + name: str, + data: Dict[str, npt.NDArray], + solutionTime: float = 0.0, + strandID: int = -1, + ): + """Tecplot ordered zone. These zones do not contain connectivity information + because the data is ordered in a structured grid. + + Parameters + ---------- + name : str + The name of the zone. + data : Dict[str, npt.NDArray] + A dictionary of variable names and their corresponding data. + solutionTime : float, optional + The solution time of the zone, by default 0.0 + strandID : int, optional + The strand id of the zone, by default -1 + """ + super().__init__(name, data, solutionTime=solutionTime, strandID=strandID) + + @property + def iMax(self) -> int: + return self.shape[0] + + @property + def jMax(self) -> int: + return self.shape[1] if len(self.shape) > 1 else 1 + + @property + def kMax(self) -> int: + return self.shape[2] if len(self.shape) > 2 else 1 + + +class TecplotFEZone(TecplotZone): + def __init__( + self, + zoneName: str, + data: Dict[str, npt.NDArray], + connectivity: npt.NDArray, + tetrahedral: bool = False, + solutionTime: float = 0.0, + strandID: int = -1, + ): + """Tecplot finite element zone. These zones contain connectivity information + to describe the elements in the zone. + + Parameters + ---------- + zoneName : str + The name of the zone. + data : Dict[str, npt.NDArray] + A dictionary of variable names and their corresponding data. + connectivity : npt.NDArray + The connectivity array that describes the elements in the zone. + tetrahedral : bool, optional + Flag to distinguish quadrilateral from tetrahedral zones, by default False + solutionTime : float, optional + The solution time of the zone, by default 0.0 + strandID : int, optional + The strand id of the zone, by default -1 + """ + super().__init__(zoneName, data, solutionTime=solutionTime, strandID=strandID) + self.connectivity = connectivity + self.tetrahedral = tetrahedral + + @property + def nElements(self) -> int: + return self.connectivity.shape[0] + + +# ============================================================================== +# ASCII WRITERS +# ============================================================================== +class TecplotZoneWriterASCII(Generic[T], ABC): + def __init__( + self, + zone: T, + datapacking: Literal["BLOCK", "POINT"], + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Abstract base class for writing Tecplot zones to ASCII files. + + Parameters + ---------- + zone : T + The Tecplot zone to write. + datapacking : Literal["BLOCK", "POINT"] + The data packing format. BLOCK is row-major, POINT is column-major. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + self.zone = zone + self.datapacking = datapacking + self.nDigits = 6 if precision == "SINGLE" else 12 + + @abstractmethod + def writeHeader(self, handle: TextIO): + pass + + @abstractmethod + def writeData(self, handle: TextIO): + pass + + @abstractmethod + def writeFooter(self, handle: TextIO): + pass + + +class TecplotOrderedZoneWriterASCII(TecplotZoneWriterASCII[TecplotOrderedZone]): + def __init__( + self, + zone: TecplotOrderedZone, + datapacking: Literal["BLOCK", "POINT"], + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Writer for Tecplot ordered zones in ASCII format. + + Parameters + ---------- + zone : TecplotOrderedZone + The ordered zone to write. + datapacking : Literal["BLOCK", "POINT"] + The data packing format. BLOCK is row-major, POINT is column-major. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + super().__init__(zone, datapacking, precision) + + def writeHeader(self, handle: TextIO): + """Write the zone header to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + # Write the zone header + zoneString = f'ZONE T="{self.zone.name}"' + zoneString += f", I={self.zone.iMax}" + + if self.zone.jMax > 1: + zoneString += f", J={self.zone.jMax}" + + if self.zone.kMax > 1: + zoneString += f", K={self.zone.kMax}" + + if self.zone.strandID is not None: + zoneString += f", STRANDID={self.zone.strandID}" + + if self.zone.solutionTime is not None: + zoneString += f", SOLUTIONTIME={self.zone.solutionTime}" + + zoneString += f", DATAPACKING={self.datapacking}\n" + + handle.write(zoneString) + + def writeData(self, handle: TextIO): + """Write the zone data to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + # Get the data into a single array + data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) + + if self.datapacking == "POINT": + # If the datapacking is POINT, then each variable is a column + np.savetxt(handle, data.reshape(-1, len(self.zone.variables)), fmt=f"%.{self.nDigits}E") + else: + # If the datapacking is BLOCK, then each variable is a row + np.savetxt(handle, data.reshape(-1, len(self.zone.variables)).T, fmt=f"%.{self.nDigits}E") + + def writeFooter(self, handle: TextIO): + """Write the zone footer to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + handle.write("\n") + + +class TecplotFEZoneWriterASCII(TecplotZoneWriterASCII[TecplotFEZone]): + def __init__( + self, + zone: TecplotFEZone, + datapacking: Literal["BLOCK", "POINT"], + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Writer for Tecplot finite element zones in ASCII format. + + Parameters + ---------- + zone : TecplotFEZone + The finite element zone to write. + datapacking : Literal["BLOCK", "POINT"] + The data packing format. BLOCK is row-major, POINT is column-major. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + super().__init__(zone, datapacking, precision) + self.zoneType = self._getZoneType() + + def _getZoneType(self) -> str: + """Get the Tecplot zone type based on the connectivity shape. + + Returns + ------- + str + The Tecplot zone type. + + Raises + ------ + ValueError + If the connectivity shape is invalid. + """ + if self.zone.connectivity.shape[1] == 2: + return ZoneType.FELINESEG.name + elif self.zone.connectivity.shape[1] == 3: + return ZoneType.FETRIANGLE.name + elif self.zone.connectivity.shape[1] == 4 and not self.zone.tetrahedral: + return ZoneType.FEQUADRILATERAL.name + elif self.zone.connectivity.shape[1] == 4 and self.zone.tetrahedral: + return ZoneType.FETETRAHEDRON.name + elif self.zone.connectivity.shape[1] == 8: + return ZoneType.FEBRICK.name + else: + raise ValueError("Invalid connectivity shape.") + + def writeHeader(self, handle: TextIO): + """Write the zone header to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + # Write the zone header + zoneString = f'ZONE T="{self.zone.name}"' + zoneString += f", DATAPACKING={self.datapacking}" + + # Write the node and element information + zoneString += f", NODES={np.multiply.reduce(self.zone.shape):d}" + zoneString += f", ELEMENTS={self.zone.nElements:d}" + zoneString += f", ZONETYPE={self.zoneType}\n" + + handle.write(zoneString) + + def writeData(self, handle: TextIO): + """Write the zone data to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + # Get the data into a single array + data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) + + if self.datapacking == "POINT": + # If the datapacking is POINT, then each variable is a column + np.savetxt(handle, data.reshape(-1, len(self.zone.variables)), fmt=f"%.{self.nDigits}E") + else: + # If the datapacking is BLOCK, then each variable is a row + np.savetxt(handle, data.reshape(-1, len(self.zone.variables)).T, fmt=f"%.{self.nDigits}E") + + def writeFooter(self, handle: TextIO): + """Write the zone footer to the file. This includes the connectivity information. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + connectivity = self.zone.connectivity + 1 + # Get the max characters in the connectivity + maxChars = len(str(connectivity.max())) + + np.savetxt(handle, connectivity, fmt=f"%{maxChars}d") + + handle.write("\n") + + +class TecplotWriterASCII: + def __init__( + self, + title: str, + zones: List[TecplotZone], + datapacking: Literal["BLOCK", "POINT"], + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Writer for Tecplot files in ASCII format. + + Parameters + ---------- + title : str + The title of the Tecplot file. + zones : List[TecplotZone] + A list of Tecplot zones to write. + datapacking : Literal["BLOCK", "POINT"] + The data packing format. BLOCK is row-major, POINT is column-major. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + self.title = title + self.zones = zones + self.datapacking = datapacking + self.precision = precision + self._checkVariables() + + def _checkVariables(self) -> None: + """Check that all zones have the same variables.""" + if not all(set(self.zones[0].variables) == set(zone.variables) for zone in self.zones): + raise ValueError("All zones must have the same variables.") + + def _writeVariables(self, handle: TextIO) -> None: + """Write the variable names to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + variables = [f'"{var}"' for var in self.zones[0].variables] + variableString = ", ".join(variables) + handle.write(f"VARIABLES = {variableString}\n") + + def _writeZone(self, handle: TextIO, zone: TecplotZone) -> None: + """Write a Tecplot zone to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + zone : TecplotZone + The zone to write. + + Raises + ------ + ValueError + If the zone type is invalid. + """ + if isinstance(zone, TecplotOrderedZone): + writer = TecplotOrderedZoneWriterASCII(zone, self.datapacking, self.precision) + elif isinstance(zone, TecplotFEZone): + writer = TecplotFEZoneWriterASCII(zone, self.datapacking, self.precision) + else: + raise ValueError("Invalid zone type.") + + writer.writeHeader(handle) + writer.writeData(handle) + writer.writeFooter(handle) + + def write(self, filename: Union[str, Path]) -> None: + """Write the Tecplot file to disk. + + Parameters + ---------- + filename : Union[str, Path] + The filename as a string or pathlib.Path object. + """ + with open(filename, "w") as handle: + handle.write(f'TITLE = "{self.title}"\n') + self._writeVariables(handle) + for zone in self.zones: + self._writeZone(handle, zone) + + +# ============================================================================== +# BINARY WRITERS +# ============================================================================== +def _writeInteger(handle: TextIO, value: int) -> None: + """Write an integer to a binary file as int32. + + Parameters + ---------- + handle : TextIO + The file handle to write to. + value : int + Integer value to write. + """ + handle.write(struct.pack("i", value)) + + +def _writeFloat32(handle: TextIO, value: float) -> None: + """Write a float to a binary file as float32. + + Parameters + ---------- + handle : TextIO + The file handle to write to. + value : float + Float value to write. + """ + handle.write(struct.pack("f", value)) + + +def _writeFloat64(handle: TextIO, value: float) -> None: + """Write a float to a binary file as float64. + + Parameters + ---------- + handle : TextIO + The file handle to write to. + value : float + Float value to write. + """ + handle.write(struct.pack("d", value)) + + +def _writeString(handle: TextIO, value: str) -> None: + """Write a string to a binary file as a string. + + Parameters + ---------- + handle : TextIO + The file handle to write to. + value : str + String value to write. + """ + for char in value: + asciiValue = ord(char) + handle.write(struct.pack("i", asciiValue)) + + handle.write(struct.pack("i", 0)) + + +class TecplotZoneWriterBinary(Generic[T], ABC): + def __init__( + self, + title: str, + zone: T, + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Abstract base class for writing Tecplot zones to binary files. + + Parameters + ---------- + title : str + The title of the Tecplot file. + zone : T + The Tecplot zone to write. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + self.title = title + self.zone = zone + self.datapacking = "BLOCK" + self.precision = precision + self.zoneType = self._getZoneType() + + def _getZoneType(self) -> int: + """Get the Tecplot zone type based on the zone object. + + Returns + ------- + int + The Tecplot zone type. + + Raises + ------ + ValueError + If the zone type is invalid. + ValueError + If the connectivity shape is invalid. + """ + if isinstance(self.zone, TecplotOrderedZone): + return ZoneType.ORDERED.value + elif isinstance(self.zone, TecplotFEZone): + if self.zone.connectivity.shape[1] == 2: + return ZoneType.FELINESEG.value + elif self.zone.connectivity.shape[1] == 3: + return ZoneType.FETRIANGLE.value + elif self.zone.connectivity.shape[1] == 4 and not self.zone.tetrahedral: + return ZoneType.FEQUADRILATERAL.value + elif self.zone.connectivity.shape[1] == 4 and self.zone.tetrahedral: + return ZoneType.FETETRAHEDRON.value + elif self.zone.connectivity.shape[1] == 8: + return ZoneType.FEBRICK.value + else: + raise ValueError("Invalid connectivity shape.") + else: + raise ValueError("Invalid zone type.") + + def _writeCommonHeader(self, handle: TextIO) -> None: + """Write the common header information for all zones. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + # Write the zone marker + _writeFloat32(handle, SectionMarkers.ZONE.value) # Write the zone marker + _writeString(handle, self.zone.name) # Write the zone name + _writeInteger(handle, BinaryFlags.NONE.value) # Write the parent zone + if self.zone.strandID is not None: + _writeInteger(handle, self.zone.strandID) # Write the strand ID + else: + _writeInteger(handle, StrandID.STATIC.value) + if self.zone.solutionTime is not None: + _writeFloat64(handle, self.zone.solutionTime) # Write the solution time + else: + _writeFloat64(handle, 0.0) + _writeInteger(handle, BinaryFlags.NONE.value) # Write the default color + _writeInteger(handle, self.zoneType) # Write the zone type + _writeInteger(handle, DataPacking.BLOCK.value) # Data Packing (Always block for binary) + _writeInteger(handle, VariableLocation.NODE.value) # Specify the variable location + _writeInteger(handle, BinaryFlags.FALSE.value) # Are raw 1-1 face neighbors supplied + + @abstractmethod + def writeHeader(self, handle: TextIO): + pass + + def writeData(self, handle: TextIO): + """Write the zone data to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + # Get the data into a single array + data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) + + # Flatten the data such that each variable is a row + data = data.reshape(-1, len(self.zone.variables)).T + + _writeFloat32(handle, SectionMarkers.ZONE.value) # Write the zone marker + + # Write the variable data format for each variable + for _ in range(len(self.zone.variables)): + _writeInteger(handle, DataPrecision[self.precision].value) + + _writeInteger(handle, BinaryFlags.FALSE.value) # No passive variables + _writeInteger(handle, BinaryFlags.FALSE.value) # No variable sharing + _writeInteger(handle, BinaryFlags.NONE.value) # No connectivity sharing + + # Write the min/max values for the variables + for i in range(len(self.zone.variables)): + _writeFloat64(handle, data[i, ...].min()) + _writeFloat64(handle, data[i, ...].max()) + + # Write the data using the specified data format (single or double) + data.astype(DTypePrecision[self.precision].value).tofile(handle) + + @abstractmethod + def writeFooter(self, handle: TextIO): + pass + + +class TecplotOrderedZoneWriterBinary(TecplotZoneWriterBinary[TecplotOrderedZone]): + def __init__( + self, + title: str, + zone: TecplotOrderedZone, + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Writer for Tecplot ordered zones in binary format. + + Parameters + ---------- + title : str + The title of the Tecplot file. + zone : TecplotOrderedZone + The ordered zone to write. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + super().__init__(title, zone, precision) + + def writeHeader(self, handle: TextIO): + """Write the zone header to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + self._writeCommonHeader(handle) # Write the common header information + + # --- Specific to Ordered Zones --- + _writeInteger(handle, self.zone.iMax) # Write the I dimension + _writeInteger(handle, self.zone.jMax) # Write the J dimension + _writeInteger(handle, self.zone.kMax) # Write the K dimension + _writeInteger(handle, BinaryFlags.FALSE.value) # No aux data + + def writeFooter(self, handle: TextIO): + """Write the zone footer to the file. This is not used for ordered zones. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + pass + + +class TecplotFEZoneWriterBinary(TecplotZoneWriterBinary[TecplotFEZone]): + def __init__( + self, + title: str, + zone: TecplotFEZone, + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Writer for Tecplot finite element zones in binary format. + + Parameters + ---------- + title : str + The title of the Tecplot file. + zone : TecplotFEZone + The finite element zone to write. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + super().__init__(title, zone, precision) + + def writeHeader(self, handle: TextIO): + """Write the zone header to the file. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + self._writeCommonHeader(handle) # Write the common header information + + # --- Specific to FE Zones --- + _writeInteger(handle, self.zone.nNodes) # Write the number of nodes + _writeInteger(handle, self.zone.nElements) # Write the number of elements + _writeInteger(handle, 0) # iCellDim (future use, set to 0) + _writeInteger(handle, 0) # jCellDim (future use, set to 0) + _writeInteger(handle, 0) # kCellDim (future use, set to 0) + _writeInteger(handle, BinaryFlags.FALSE.value) # No aux data + + def writeFooter(self, handle: TextIO): + """Write the zone footer to the file. This includes the connectivity information. + + Parameters + ---------- + handle : TextIO + The file handle. + """ + self.zone.connectivity.astype("int32").tofile(handle) + + +class TecplotWriterBinary: + def __init__( + self, + title: str, + zones: List[TecplotZone], + precision: Literal["SINGLE", "DOUBLE"], + ) -> None: + """Writer for Tecplot files in binary format. + + Parameters + ---------- + title : str + The title of the Tecplot file. + zones : List[TecplotZone] + A list of Tecplot zones to write. + precision : Literal["SINGLE", "DOUBLE"] + The floating point precision to write the data. + """ + self.title = title + self.zones = zones + self.precision = precision + self._checkVariables() + + def _checkVariables(self) -> None: + """Check that all zones have the same variables.""" + if not all(set(self.zones[0].variables) == set(zone.variables) for zone in self.zones): + raise ValueError("All zones must have the same variables.") + + def _getZoneWriter(self, zone: TecplotZone) -> TecplotZoneWriterBinary: + """Get the appropriate zone writer based on the zone type. + + Parameters + ---------- + zone : TecplotZone + The Tecplot zone to write. + + Returns + ------- + TecplotZoneWriterBinary + The appropriate zone writer object. + + Raises + ------ + ValueError + If the zone type is invalid. + """ + if isinstance(zone, TecplotOrderedZone): + return TecplotOrderedZoneWriterBinary(self.title, zone, self.precision) + elif isinstance(zone, TecplotFEZone): + return TecplotFEZoneWriterBinary(self.title, zone, self.precision) + else: + raise ValueError("Invalid zone type.") + + def write(self, filename: Union[str, Path]) -> None: + """Write the Tecplot file to disk. + + Parameters + ---------- + filename : Union[str, Path] + The filename as a string or pathlib.Path object. + """ + with open(filename, "wb") as handle: + handle.write(b"#!TDV112") # Magic number + _writeInteger(handle, 1) # Byte order + _writeInteger(handle, FileType.FULL.value) # Full filetype + _writeString(handle, self.title) # Write the title + _writeInteger(handle, len(self.zones[0].variables)) # Write the number of variables + + for var in self.zones[0].variables: + _writeString(handle, var) # Write the variable names + + # Write the zone headers + for zone in self.zones: + writer = self._getZoneWriter(zone) + writer.writeHeader(handle) + + # Write the data marker + _writeFloat32(handle, SectionMarkers.DATA.value) + + # Write the data and footer for each zone + for zone in self.zones: + writer = self._getZoneWriter(zone) + writer.writeData(handle) + writer.writeFooter(handle) + + +# ============================================================================== +# ASCII READERS +# ============================================================================== +class TecplotASCIIReader: + def __init__(self, filename: Union[str, Path]) -> None: + """Reader for Tecplot files in ASCII format. + + Parameters + ---------- + filename : Union[str, Path] + The filename as a string or pathlib.Path object. + """ + self.filename = filename + + def _readZoneHeader(self, lines: List[str], iCurrent: int) -> Tuple[Dict[str, Any], int]: + """Read the zone header information from a line in a Tecplot file. + + Parameters + ---------- + line : str + The line containing the zone header information. + + Returns + ------- + Dict[str, Any] + A dictionary containing the parsed zone header information. + """ + # Get all the header lines into a single string + header = [] + + # Loop until the line starts with a number which denotes the start of the data section + exitPattern = re.compile(r"^\s*\d") + while not exitPattern.match(lines[iCurrent]): + header.append(lines[iCurrent].strip("\n")) + iCurrent += 1 + + # Join the header lines into a single string + headerString = ", ".join(header) + + # Use regex to parse the header information + zoneNameMatch = re.search(r'zone t\s*=\s*"(.+)"', headerString, re.IGNORECASE) + zoneName = zoneNameMatch.group(1) if zoneNameMatch else None + + zoneTypeMatch = re.search(r"zonetype\s*=\s*(\w+)", headerString, re.IGNORECASE) + zoneType = zoneTypeMatch.group(1) if zoneTypeMatch else "ORDERED" + + datapackingMatch = re.search(r"datapacking\s*=\s*(\w+)", headerString, re.IGNORECASE) + datapacking = datapackingMatch.group(1) if datapackingMatch else None + + nNodesMatch = re.search(r"nodes\s*=\s*(\d+)", headerString, re.IGNORECASE) + nNodes = int(nNodesMatch.group(1)) if nNodesMatch else None + + nElementsMatch = re.search(r"elements\s*=\s*(\d+)", headerString, re.IGNORECASE) + nElements = int(nElementsMatch.group(1)) if nElementsMatch else None + + iMaxMatch = re.search(r"i\s*=\s*(\d+)", headerString, re.IGNORECASE) + iMax = int(iMaxMatch.group(1)) if iMaxMatch else 1 + + jMaxMatch = re.search(r"j\s*=\s*(\d+)", headerString, re.IGNORECASE) + jMax = int(jMaxMatch.group(1)) if jMaxMatch else 1 + + kMaxMatch = re.search(r"k\s*=\s*(\d+)", headerString, re.IGNORECASE) + kMax = int(kMaxMatch.group(1)) if kMaxMatch else 1 + + solutionTimeMatch = re.search(r"solutiontime\s*=\s*(\d+\.\d+)", headerString, re.IGNORECASE) + solutionTime = float(solutionTimeMatch.group(1)) if solutionTimeMatch else 0.0 + + strandIDMatch = re.search(r"strandid\s*=\s*(\d+)", headerString, re.IGNORECASE) + strandID = int(strandIDMatch.group(1)) if strandIDMatch else -1 + + headerDict = { + "zoneName": zoneName, + "zoneType": zoneType, + "datapacking": datapacking, + "nNodes": nNodes, + "nElements": nElements, + "iMax": iMax, + "jMax": jMax, + "kMax": kMax, + "solutionTime": solutionTime, + "strandID": strandID, + } + + return headerDict, iCurrent + + def _readOrderedZoneData( + self, iCurrent: int, variables: List[str], zoneHeaderDict: Dict[str, Any] + ) -> Tuple[TecplotOrderedZone, int]: + """Read the data for an ordered Tecplot zone. + + Parameters + ---------- + iCurrent : int + The current line number in the file. + variables : List[str] + The list of variable names. + zoneHeaderDict : Dict[str, Any] + The zone header information. + + Returns + ------- + Tuple[TecplotOrderedZone, int] + The ordered zone object and the number of lines read. + """ + iMax = zoneHeaderDict["iMax"] + jMax = zoneHeaderDict["jMax"] + kMax = zoneHeaderDict["kMax"] + nNodes = iMax * jMax * kMax + shape = (iMax, jMax, kMax, len(variables)) + + if zoneHeaderDict["datapacking"] == "POINT": + # Point data is column-major + nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=nNodes, dtype=float) + nodalData = nodalData.reshape(shape).squeeze() + nodeOffset = nNodes + else: + # Block data is row-major + nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=len(variables), dtype=float) + nodalData = nodalData.T.reshape(shape).squeeze() + nodeOffset = len(variables) + + data = {var: nodalData[..., i] for i, var in enumerate(variables)} + zone = TecplotOrderedZone( + zoneHeaderDict["zoneName"], + data, + solutionTime=zoneHeaderDict["solutionTime"], + strandID=zoneHeaderDict["strandID"], + ) + + return zone, nodeOffset + + def _readFEZoneData( + self, iCurrent: int, variables: List[str], zoneHeaderDict: Dict[str, Any] + ) -> Tuple[TecplotFEZone, int]: + """Read the data for a finite element Tecplot zone. + + Parameters + ---------- + iCurrent : int + The current line number in the file. + variables : List[str] + The list of variable names. + zoneHeaderDict : Dict[str, Any] + The zone header information. + + Returns + ------- + Tuple[TecplotFEZone, int] + The finite element zone object and the number of lines read. + """ + nNodes = zoneHeaderDict["nNodes"] + nElements = zoneHeaderDict["nElements"] + + if zoneHeaderDict["datapacking"] == "POINT": + # Point data is column-major + nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=nNodes, dtype=float) + nodeOffset = nNodes + else: + # Block data is row-major + nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=len(variables), dtype=float) + nodalData = np.atleast_2d(nodalData).T + nodeOffset = len(variables) + + connectivity = np.loadtxt(self.filename, skiprows=iCurrent + nodeOffset, max_rows=nElements, dtype=int) + + # Check if the nodal data is 1D + if nodalData.ndim == 1: + nodalData = nodalData.reshape(-1, len(variables)) + + data = {var: nodalData[..., i] for i, var in enumerate(variables)} + zone = TecplotFEZone( + zoneHeaderDict["zoneName"], + data, + connectivity - 1, + solutionTime=zoneHeaderDict["solutionTime"], + strandID=zoneHeaderDict["strandID"], + tetrahedral=zoneHeaderDict["zoneType"] == "FETETRAHEDRON", + ) + + return zone, nodeOffset + nElements + + def _readZoneData(self, lines: List[str], iCurrent: int, variables: List[str]) -> Tuple[TecplotZone, int]: + """Read the data for a Tecplot zone. + + Parameters + ---------- + lines : List[str] + The list of lines in the Tecplot file. + iCurrent : int + The current line number in the file. + variables : List[str] + The list of variable names. + + Returns + ------- + Tuple[TecplotZone, int] + The Tecplot zone object and the number of lines read. + """ + zoneHeaderDict, iCurrent = self._readZoneHeader(lines, iCurrent) + + if zoneHeaderDict["zoneType"] == "ORDERED": + zone, iOffset = self._readOrderedZoneData(iCurrent, variables, zoneHeaderDict) + else: + zone, iOffset = self._readFEZoneData(iCurrent, variables, zoneHeaderDict) + + return zone, iCurrent + iOffset + + def read(self) -> Tuple[str, List[TecplotZone]]: + """Read the Tecplot file and return the title and zones. + + Returns + ------- + Tuple[str, List[TecplotZone]] + The title of the Tecplot file and a list of Tecplot zones. + + Raises + ------ + ValueError + If the file is not a valid Tecplot file. + ValueError + If the title is missing. + """ + with open(self.filename, "r") as handle: + lines = handle.readlines() + + zones = [] + + # Get the title + title = re.search(r'title\s*=\s*"(.*)"', lines[0], re.IGNORECASE) + if title is None: + raise ValueError("Tecplot file must have a title on the first line.") + title = title.group(1) + + # Get the variable names + variables = re.findall(r'"([^"]*)"', lines[1]) + + iCurrent = 2 + while iCurrent < len(lines): + zone, iCurrent = self._readZoneData(lines, iCurrent, variables) + zones.append(zone) + + # Skip any empty lines + while iCurrent < len(lines) and not lines[iCurrent].strip(): + iCurrent += 1 + + return title, zones + + +# ============================================================================== +# BINARY READERS +# ============================================================================== +class TecplotBinaryReader: + def __init__(self, filename: Union[str, Path]) -> None: + """Reader for Tecplot files in binary format. + + Parameters + ---------- + filename : Union[str, Path] + The filename as a string or pathlib.Path object + """ + self.filename = filename + self._nVariables = 0 + self._variables = [] + + def _readString(self, handle: TextIO) -> str: + """Read a string from a binary file that is null-terminated. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + + Returns + ------- + str + The string read from the file. + """ + result = [] + while True: + data = handle.read(4) + integer = struct.unpack_from("i", data, 0)[0] + + if integer == 0: + break + + result.append(chr(integer)) + + return "".join(result) + + def _readInteger(self, handle: TextIO, offset: int = 0) -> int: + """Read an integer from a binary file as int32. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + offset : int, optional + The offset (in bytes) from the file's current position, by default 0 + + Returns + ------- + int + The integer read from the file. + """ + return np.fromfile(handle, dtype=np.int32, count=1, offset=offset)[0] + + def _readIntegerArray(self, handle: TextIO, nValues: int, offset: int = 0) -> np.ndarray: + """Read an array of integers from a binary file as int32. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + nValues : int + The number of values to read. + offset : int, optional + The offset (in bytes) from the file's current position, by default 0 + + Returns + ------- + np.ndarray + The integer array read from the file. + """ + return np.fromfile(handle, dtype=np.int32, count=nValues, offset=offset) + + def _readFloat32(self, handle: TextIO, offset: int = 0) -> float: + """Read a float from a binary file as float32. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + offset : int, optional + The offset (in bytes) from the file's current position, by default 0 + + Returns + ------- + float + The float read from the file. + """ + return np.fromfile(handle, dtype=np.float32, count=1, offset=offset)[0] + + def _readFloat32Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np.ndarray: + """Read an array of floats from a binary file as float32. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + nValues : int + The number of values to read. + offset : int, optional + The offset (in bytes) from the file's current position, by default 0 + + Returns + ------- + np.ndarray + The float array read from the file. + """ + return np.fromfile(handle, dtype=np.float32, count=nValues, offset=offset) + + def _readFloat64(self, handle: TextIO, offset: int = 0) -> float: + """Read a float from a binary file as float64. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + offset : int, optional + The offset (in bytes) from the file's current position, by default 0 + + Returns + ------- + float + The float read from the file. + """ + return np.fromfile(handle, dtype=np.float64, count=1, offset=offset)[0] + + def _readFloat64Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np.ndarray: + """Read an array of floats from a binary file as float64. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + nValues : int + The number of values to read. + offset : int, optional + The offset (in bytes) from the file's current position, by default 0 + + Returns + ------- + np.ndarray + The float array read from the file. + """ + return np.fromfile(handle, dtype=np.float64, count=nValues, offset=offset) + + def _readOrderedZone(self, handle: TextIO, zoneName: str, strandID: int, solutionTime: float) -> TecplotOrderedZone: + """Read an ordered Tecplot zone header from a binary file. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + zoneName : str + The name of the zone. + strandID : int + The strand ID. + solutionTime : float + The solution time. + + Returns + ------- + TecplotOrderedZone + The ordered Tecplot zone object. + """ + iMax = self._readInteger(handle) + jMax = self._readInteger(handle) + kMax = self._readInteger(handle) + + return TecplotOrderedZone( + zoneName, + {var: np.zeros((iMax, jMax, kMax)).squeeze() for _, var in enumerate(self._variables)}, + solutionTime=solutionTime, + strandID=strandID, + ) + + def _readFEZone( + self, handle: TextIO, zoneName: str, zoneType: int, strandID: int, solutionTime: float + ) -> TecplotFEZone: + """Read a finite element Tecplot zone header from a binary file. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + zoneName : str + The name of the zone. + zoneType : int + The zone type. + strandID : int + The strand ID. + solutionTime : float + The solution time. + + Returns + ------- + TecplotFEZone + The finite element Tecplot zone object. + + Raises + ------ + ValueError + If the zone type is invalid. + """ + nNodes = self._readInteger(handle) + nElements = self._readInteger(handle) + iCellDim = self._readInteger(handle) # NOQA: F841 + jCellDim = self._readInteger(handle) # NOQA: F841 + kCellDim = self._readInteger(handle) # NOQA: F841 + + if zoneType == ZoneType.FELINESEG.value: + connectivity = np.zeros((nElements, 2), dtype=int) + elif zoneType == ZoneType.FETRIANGLE.value: + connectivity = np.zeros((nElements, 3), dtype=int) + elif zoneType == ZoneType.FEQUADRILATERAL.value: + connectivity = np.zeros((nElements, 4), dtype=int) + elif zoneType == ZoneType.FETETRAHEDRON.value: + connectivity = np.zeros((nElements, 4), dtype=int) + elif zoneType == ZoneType.FEBRICK.value: + connectivity = np.zeros((nElements, 8), dtype=int) + else: + raise ValueError("Invalid zone type.") + + return TecplotFEZone( + zoneName, + {var: np.zeros(nNodes) for _, var in enumerate(self._variables)}, + connectivity, + tetrahedral=zoneType == ZoneType.FETETRAHEDRON.value, + solutionTime=solutionTime, + strandID=strandID, + ) + + def _readZoneHeader(self, handle: TextIO) -> Union[TecplotOrderedZone, TecplotFEZone]: + """Read a Tecplot zone header from a binary file. + + Parameters + ---------- + handle : TextIO + The file handle to read from. + + Returns + ------- + Union[TecplotOrderedZone, TecplotFEZone] + The Tecplot zone object, either ordered or finite element. + """ + zoneName = self._readString(handle) + parentZone = self._readInteger(handle) # NOQA: F841 + strandID = self._readInteger(handle) + solutionTime = self._readFloat64(handle) + defaultColor = self._readInteger(handle) # NOQA: F841 + zoneType = self._readInteger(handle) + datapacking = self._readInteger(handle) # NOQA: F841 + variableLocation = self._readInteger(handle) # NOQA: F841 + rawFaceNeighbors = self._readInteger(handle) # NOQA: F841 + + if zoneType == ZoneType.ORDERED.value: + zone = self._readOrderedZone(handle, zoneName, strandID, solutionTime) + else: + zone = self._readFEZone(handle, zoneName, zoneType, strandID, solutionTime) + + return zone + + def read(self) -> Tuple[str, List[TecplotZone]]: + """Read the Tecplot file and return the title and zones. + + Returns + ------- + Tuple[str, List[TecplotZone]] + The title of the Tecplot file and a list of Tecplot zones. + + Raises + ------ + ValueError + If the file is not a valid Tecplot file. + """ + file = open(self.filename, "rb") + file.seek(0, 2) + fileSize = file.tell() + file.seek(0) + + magic = file.read(8).decode("utf-8") + if magic != "#!TDV112": + raise ValueError("Invalid Tecplot binary file version.") + + byteOrder = self._readInteger(file) # NOQA: F841 + filetype = self._readInteger(file) # NOQA: F841 + title = self._readString(file) + self._nVariables = self._readInteger(file) + self._variables = [self._readString(file) for _ in range(self._nVariables)] + zones: List[Union[TecplotOrderedZone, TecplotFEZone]] = [] + + # Read all the zone headers + while True: + marker = self._readFloat32(file) + + if marker == SectionMarkers.ZONE.value: + # Initialize zone from the header + zone = self._readZoneHeader(file) + zones.append(zone) + if marker == SectionMarkers.DATA.value: + break + + # Read the data for each zone + izone = 0 + while file.tell() < fileSize: + zoneMarker = self._readFloat32(file) # NOQA: F841 + dataFormats = [self._readInteger(file) for _ in range(self._nVariables)] + passiveVariables = self._readInteger(file) # NOQA: F841 + variableSharing = self._readInteger(file) # NOQA: F841 + connSharing = self._readInteger(file) # NOQA: F841 + minMaxArray = self._readFloat64Array(file, 2 * self._nVariables).reshape(self._nVariables, 2) # NOQA: F841 + + if isinstance(zones[izone], TecplotOrderedZone): + iMax = zones[izone].iMax + jMax = zones[izone].jMax + kMax = zones[izone].kMax + + for i in range(self._nVariables): + if dataFormats[i] == DataPrecision.SINGLE.value: + readData = self._readFloat32Array(file, iMax * jMax * kMax).reshape(iMax, jMax, kMax).squeeze() + else: + readData = self._readFloat64Array(file, iMax * jMax * kMax).reshape(iMax, jMax, kMax).squeeze() + + zones[izone].data[self._variables[i]][...] = readData + + if isinstance(zones[izone], TecplotFEZone): + nNodes = zones[izone].nNodes + nElements = zones[izone].nElements + + for i in range(self._nVariables): + if dataFormats[i] == DataPrecision.SINGLE.value: + readData = self._readFloat32Array(file, nNodes) + else: + readData = self._readFloat64Array(file, nNodes) + + zones[izone].data[self._variables[i]][...] = readData + + connectivitySize = zones[izone].connectivity.size + connectivity = self._readIntegerArray(file, connectivitySize).reshape(nElements, -1) + zones[izone].connectivity = connectivity + + izone += 1 + + file.close() + + return title, zones + + +# ============================================================================== +# PUBLIC FUNCTIONS +# ============================================================================== +def writeTecplot( + filename: Union[str, Path], + title: str, + zones: List[TecplotZone], + datapacking: Literal["BLOCK", "POINT"] = "POINT", + precision: Literal["SINGLE", "DOUBLE"] = "SINGLE", +) -> None: + """Write a Tecplot file to disk. The file format is determined by the + file extension. If the extension is .plt, the file will be written in + binary format. If the extension is .dat, the file will be written in + ASCII format. + + Note + ---- + - ASCII files can be written with either BLOCK or POINT data packing. + - Binary files are always written with BLOCK data packing. + + Parameters + ---------- + filename : Union[str, Path] + The filename as a string or pathlib.Path object. + title : str + The title of the Tecplot file. + zones : List[TecplotZone] + A list of Tecplot zones to write + datapacking : Literal["BLOCK", "POINT"], optional + The data packing format. BLOCK is row-major, POINT is column-major, by default "POINT" + precision : Literal["SINGLE", "DOUBLE"], optional + The floating point precision to write the data, by default "SINGLE" + + Raises + ------ + ValueError + If the file extension is invalid. + """ + filepath = Path(filename) + if filepath.suffix == ".plt": + writer = TecplotWriterBinary(title, zones, precision) + writer.write(filepath) + elif filepath.suffix == ".dat": + writer = TecplotWriterASCII(title, zones, datapacking, precision) + writer.write(filename) + else: + raise ValueError("Invalid file extension. Must be .plt (binary) or .dat (ASCII).") + + +def readTecplot(filename: Union[str, Path]) -> Tuple[str, List[Union[TecplotOrderedZone, TecplotFEZone]]]: + """Read a Tecplot file from disk. The file format is determined by the + file extension. If the extension is .plt, the file will be read in + binary format. If the extension is .dat, the file will be read in + ASCII format. + + Parameters + ---------- + filename : Union[str, Path] + The filename as a string or pathlib.Path object. + + Returns + ------- + Tuple[str, List[Union[TecplotOrderedZone, TecplotFEZone]]] + The title of the Tecplot file and a list of Tecplot zones. + + Raises + ------ + ValueError + If the file extension is invalid. + """ + filepath = Path(filename) + if filepath.suffix == ".plt": + reader = TecplotBinaryReader(filename) + title, zones = reader.read() + elif filepath.suffix == ".dat": + reader = TecplotASCIIReader(filename) + title, zones = reader.read() + else: + raise ValueError("Invalid file extension. Must be .plt (binary) or .dat (ASCII).") + + return title, zones diff --git a/tests/input/airfoil_000_slices.dat b/tests/input/airfoil_000_slices.dat new file mode 100644 index 0000000..a9f7eef --- /dev/null +++ b/tests/input/airfoil_000_slices.dat @@ -0,0 +1,285 @@ + Title = "ADflow Slice Data" +Variables = "CoordinateX" "CoordinateY" "CoordinateZ" "XoC" "YoC" "ZoC" "Density" "VelocityX" "VelocityY" "CoefPressure" "Mach" "SkinFrictionMagnitude" "SkinFrictionX" +Zone T= "Slice_0001 None Absolute Regular z = 0.50000000" + Nodes = 142 Elements= 138 ZONETYPE=FELINESEG + DATAPACKING=POINT + 7.823774E-01 1.691034E-02 5.000000E-01 7.823466E-01 1.690968E-02 0.000000E+00 8.769829E-01 1.730982E-02 -1.106485E-03 -8.302255E-02 0.000000E+00 2.180030E-03 2.175599E-03 + 7.622641E-01 1.816350E-02 5.000000E-01 7.622340E-01 1.816278E-02 0.000000E+00 8.795820E-01 1.661022E-02 -9.950567E-04 -7.566586E-02 0.000000E+00 2.147108E-03 2.143266E-03 + 7.400762E-01 1.943594E-02 5.000000E-01 7.400470E-01 1.943518E-02 0.000000E+00 8.840950E-01 1.568833E-02 -9.324715E-04 -6.295219E-02 0.000000E+00 2.082592E-03 2.078989E-03 + 7.157196E-01 2.090906E-02 5.000000E-01 7.156914E-01 2.090823E-02 0.000000E+00 8.812874E-01 1.467928E-02 -1.014514E-03 -7.089337E-02 0.000000E+00 2.002484E-03 1.997728E-03 + 6.891330E-01 2.295514E-02 5.000000E-01 6.891058E-01 2.295424E-02 0.000000E+00 8.696365E-01 1.400634E-02 -1.156615E-03 -1.038830E-01 0.000000E+00 1.963482E-03 1.956759E-03 + 6.602989E-01 2.550903E-02 5.000000E-01 6.602729E-01 2.550802E-02 0.000000E+00 8.576853E-01 1.397531E-02 -1.201179E-03 -1.380027E-01 0.000000E+00 2.009371E-03 2.001965E-03 + 6.292542E-01 2.810639E-02 5.000000E-01 6.292294E-01 2.810529E-02 0.000000E+00 8.473740E-01 1.434909E-02 -1.171540E-03 -1.677053E-01 0.000000E+00 2.110641E-03 2.103706E-03 + 5.961002E-01 3.072295E-02 5.000000E-01 5.960767E-01 3.072174E-02 0.000000E+00 8.339816E-01 1.472225E-02 -1.155850E-03 -2.060563E-01 0.000000E+00 2.211227E-03 2.204499E-03 + 5.610108E-01 3.344136E-02 5.000000E-01 5.609887E-01 3.344005E-02 0.000000E+00 8.182221E-01 1.495282E-02 -1.125149E-03 -2.509287E-01 0.000000E+00 2.288083E-03 2.281663E-03 + 5.242375E-01 3.611316E-02 5.000000E-01 5.242168E-01 3.611174E-02 0.000000E+00 8.046896E-01 1.497465E-02 -1.054999E-03 -2.893953E-01 0.000000E+00 2.327903E-03 2.322113E-03 + 4.861092E-01 3.873006E-02 5.000000E-01 4.860900E-01 3.872853E-02 0.000000E+00 7.989295E-01 1.474552E-02 -9.504222E-04 -3.059910E-01 0.000000E+00 2.321257E-03 2.316348E-03 + 4.470274E-01 4.112937E-02 5.000000E-01 4.470098E-01 4.112775E-02 0.000000E+00 7.977514E-01 1.429271E-02 -8.107427E-04 -3.098850E-01 0.000000E+00 2.269757E-03 2.266117E-03 + 4.074550E-01 4.316362E-02 5.000000E-01 4.074389E-01 4.316192E-02 0.000000E+00 7.788086E-01 1.374141E-02 -6.616893E-04 -3.640119E-01 0.000000E+00 2.191907E-03 2.189512E-03 + 3.678986E-01 4.480381E-02 5.000000E-01 3.678841E-01 4.480205E-02 0.000000E+00 7.199728E-01 1.368958E-02 -5.040894E-04 -5.307141E-01 0.000000E+00 2.182102E-03 2.180701E-03 + 3.288871E-01 4.595042E-02 5.000000E-01 3.288741E-01 4.594861E-02 0.000000E+00 6.318774E-01 1.547437E-02 -3.337712E-04 -7.804645E-01 0.000000E+00 2.449687E-03 2.449031E-03 + 2.909459E-01 4.656835E-02 5.000000E-01 2.909344E-01 4.656652E-02 0.000000E+00 5.566506E-01 2.033488E-02 -1.234061E-04 -9.951881E-01 0.000000E+00 3.177438E-03 3.177259E-03 + 2.545710E-01 4.660779E-02 5.000000E-01 2.545610E-01 4.660596E-02 0.000000E+00 5.244056E-01 2.735012E-02 2.374738E-04 -1.089817E+00 0.000000E+00 4.198999E-03 4.198697E-03 + 2.202045E-01 4.605987E-02 5.000000E-01 2.201958E-01 4.605806E-02 0.000000E+00 5.277605E-01 3.342428E-02 8.403877E-04 -1.083680E+00 0.000000E+00 5.024009E-03 5.022085E-03 + 1.882140E-01 4.494129E-02 5.000000E-01 1.882066E-01 4.493952E-02 0.000000E+00 5.442207E-01 3.669597E-02 1.644182E-03 -1.038860E+00 0.000000E+00 5.379277E-03 5.373338E-03 + 1.588780E-01 4.328696E-02 5.000000E-01 1.588718E-01 4.328525E-02 0.000000E+00 5.608540E-01 3.811115E-02 2.573374E-03 -9.919544E-01 0.000000E+00 5.423373E-03 5.410392E-03 + 1.323788E-01 4.116000E-02 5.000000E-01 1.323735E-01 4.115838E-02 0.000000E+00 5.760369E-01 3.947691E-02 3.681957E-03 -9.485354E-01 0.000000E+00 5.426116E-03 5.401896E-03 + 1.088015E-01 3.862899E-02 5.000000E-01 1.087972E-01 3.862747E-02 0.000000E+00 5.923225E-01 4.177502E-02 5.127649E-03 -9.021378E-01 0.000000E+00 5.520156E-03 5.478123E-03 + 8.814071E-02 3.576593E-02 5.000000E-01 8.813724E-02 3.576452E-02 0.000000E+00 6.122635E-01 4.500841E-02 7.060907E-03 -8.457289E-01 0.000000E+00 5.696456E-03 5.626584E-03 + 7.031156E-02 3.264592E-02 5.000000E-01 7.030879E-02 3.264464E-02 0.000000E+00 6.376871E-01 4.874236E-02 9.594187E-03 -7.739780E-01 0.000000E+00 5.896192E-03 5.784006E-03 + 5.516427E-02 2.934346E-02 5.000000E-01 5.516210E-02 2.934231E-02 0.000000E+00 6.694468E-01 5.250913E-02 1.278610E-02 -6.841842E-01 0.000000E+00 6.073830E-03 5.899995E-03 + 4.249863E-02 2.594613E-02 5.000000E-01 4.249695E-02 2.594511E-02 0.000000E+00 7.073704E-01 5.578594E-02 1.658521E-02 -5.764983E-01 0.000000E+00 6.200159E-03 5.941386E-03 + 3.203145E-02 2.253495E-02 5.000000E-01 3.203019E-02 2.253406E-02 0.000000E+00 7.516866E-01 5.776977E-02 2.081023E-02 -4.499086E-01 0.000000E+00 6.239092E-03 5.867480E-03 + 2.331672E-02 1.909245E-02 5.000000E-01 2.331580E-02 1.909169E-02 0.000000E+00 8.025574E-01 5.712975E-02 2.494108E-02 -3.034481E-01 0.000000E+00 6.103029E-03 5.589531E-03 + 1.591438E-02 1.552938E-02 5.000000E-01 1.591375E-02 1.552877E-02 0.000000E+00 8.601468E-01 5.237826E-02 2.801593E-02 -1.361041E-01 0.000000E+00 5.656655E-03 4.982834E-03 + 9.658338E-03 1.178916E-02 5.000000E-01 9.657958E-03 1.178869E-02 0.000000E+00 9.328105E-01 4.272251E-02 2.900778E-02 7.562705E-02 0.000000E+00 4.796930E-03 3.963653E-03 + 4.674145E-03 7.870585E-03 5.000000E-01 4.673961E-03 7.870275E-03 0.000000E+00 1.044568E+00 2.814464E-02 2.629317E-02 3.982327E-01 0.000000E+00 3.457744E-03 2.529153E-03 + 1.281341E-03 3.839618E-03 5.000000E-01 1.281290E-03 3.839466E-03 0.000000E+00 1.206823E+00 1.132702E-02 1.599617E-02 8.615307E-01 0.000000E+00 1.678967E-03 9.668870E-04 + 2.352898E-18 -5.130713E-19 5.000000E-01 0.000000E+00 0.000000E+00 0.000000E+00 1.290470E+00 6.340877E-03 -6.986974E-03 1.099159E+00 0.000000E+00 1.229277E-03 4.491090E-04 + 1.281240E-03 -3.074255E-03 5.000000E-01 1.281190E-03 -3.074134E-03 0.000000E+00 1.160849E+00 2.173498E-02 -2.338417E-02 7.311508E-01 0.000000E+00 2.482573E-03 1.645839E-03 + 4.673777E-03 -5.479147E-03 5.000000E-01 4.673593E-03 -5.478931E-03 0.000000E+00 9.857179E-01 3.948160E-02 -2.127343E-02 2.324595E-01 0.000000E+00 3.598765E-03 3.162125E-03 + 9.657579E-03 -7.462679E-03 5.000000E-01 9.657198E-03 -7.462385E-03 0.000000E+00 9.183535E-01 4.666479E-02 -1.538191E-02 3.750595E-02 0.000000E+00 4.131688E-03 3.918618E-03 + 1.000039E+00 4.335176E-04 5.000000E-01 9.999999E-01 4.335005E-04 0.000000E+00 9.178565E-01 3.218372E-02 -5.635738E-03 3.712361E-02 0.000000E+00 1.164162E-03 9.562320E-04 + 9.998942E-01 4.463465E-04 5.000000E-01 9.998548E-01 4.463289E-04 0.000000E+00 9.532969E-01 3.939878E-02 -6.167425E-03 1.362680E-01 0.000000E+00 1.434919E-03 1.429344E-03 + 9.996793E-01 4.653449E-04 5.000000E-01 9.996399E-01 4.653266E-04 0.000000E+00 9.334895E-01 2.687128E-02 -1.948203E-03 8.086840E-02 0.000000E+00 1.185505E-03 1.180902E-03 + 9.993613E-01 4.934446E-04 5.000000E-01 9.993219E-01 4.934251E-04 0.000000E+00 9.375248E-01 3.241384E-02 -2.952429E-03 9.193923E-02 0.000000E+00 1.512094E-03 1.506228E-03 + 9.988916E-01 5.349283E-04 5.000000E-01 9.988522E-01 5.349072E-04 0.000000E+00 9.376982E-01 3.108600E-02 -2.733864E-03 9.228407E-02 0.000000E+00 1.663165E-03 1.656720E-03 + 9.981996E-01 5.960037E-04 5.000000E-01 9.981603E-01 5.959802E-04 0.000000E+00 9.374421E-01 2.603351E-02 -2.293258E-03 9.144503E-02 0.000000E+00 1.650891E-03 1.644504E-03 + 9.971840E-01 6.855608E-04 5.000000E-01 9.971447E-01 6.855338E-04 0.000000E+00 9.371065E-01 2.062899E-02 -1.817270E-03 9.033775E-02 0.000000E+00 1.582297E-03 1.576190E-03 + 9.957013E-01 8.161055E-04 5.000000E-01 9.956620E-01 8.160733E-04 0.000000E+00 9.366509E-01 1.628945E-02 -1.435026E-03 8.885614E-02 0.000000E+00 1.522780E-03 1.516925E-03 + 9.935539E-01 1.004757E-03 5.000000E-01 9.935147E-01 1.004718E-03 0.000000E+00 9.356150E-01 1.325044E-02 -1.167588E-03 8.572860E-02 0.000000E+00 1.495616E-03 1.489896E-03 + 9.904791E-01 1.273998E-03 5.000000E-01 9.904401E-01 1.273948E-03 0.000000E+00 9.332168E-01 1.141662E-02 -9.996309E-04 7.876507E-02 0.000000E+00 1.497252E-03 1.491572E-03 + 9.861469E-01 1.651517E-03 5.000000E-01 9.861080E-01 1.651452E-03 0.000000E+00 9.305094E-01 1.060698E-02 -9.212272E-04 7.092268E-02 0.000000E+00 1.509788E-03 1.504120E-03 + 9.821027E-01 2.001929E-03 5.000000E-01 9.820640E-01 2.001851E-03 0.000000E+00 9.285245E-01 1.051754E-02 -9.105222E-04 6.511007E-02 0.000000E+00 1.526705E-03 1.521040E-03 + 9.778355E-01 2.369477E-03 5.000000E-01 9.777970E-01 2.369384E-03 0.000000E+00 9.265370E-01 1.082341E-02 -9.310436E-04 5.930848E-02 0.000000E+00 1.554483E-03 1.548785E-03 + 9.733502E-01 2.753336E-03 5.000000E-01 9.733118E-01 2.753227E-03 0.000000E+00 9.243767E-01 1.130566E-02 -9.654937E-04 5.304624E-02 0.000000E+00 1.590739E-03 1.584986E-03 + 9.686497E-01 3.152840E-03 5.000000E-01 9.686116E-01 3.152715E-03 0.000000E+00 9.222332E-01 1.184954E-02 -1.005224E-03 4.685132E-02 0.000000E+00 1.628497E-03 1.622689E-03 + 9.637331E-01 3.567703E-03 5.000000E-01 9.636952E-01 3.567562E-03 0.000000E+00 9.200808E-01 1.240052E-02 -1.044457E-03 4.064388E-02 0.000000E+00 1.665345E-03 1.659492E-03 + 9.585926E-01 3.998262E-03 5.000000E-01 9.585548E-01 3.998105E-03 0.000000E+00 9.179270E-01 1.293799E-02 -1.081959E-03 3.443700E-02 0.000000E+00 1.701687E-03 1.695793E-03 + 9.532109E-01 4.445688E-03 5.000000E-01 9.531734E-01 4.445513E-03 0.000000E+00 9.157741E-01 1.344964E-02 -1.116836E-03 2.823230E-02 0.000000E+00 1.737908E-03 1.731976E-03 + 9.475603E-01 4.912065E-03 5.000000E-01 9.475229E-01 4.911872E-03 0.000000E+00 9.136108E-01 1.392595E-02 -1.148354E-03 2.199604E-02 0.000000E+00 1.773831E-03 1.767864E-03 + 9.416026E-01 5.400229E-03 5.000000E-01 9.415655E-01 5.400017E-03 0.000000E+00 9.114136E-01 1.436534E-02 -1.176172E-03 1.566132E-02 0.000000E+00 1.809263E-03 1.803266E-03 + 9.352941E-01 5.913251E-03 5.000000E-01 9.352572E-01 5.913018E-03 0.000000E+00 9.091619E-01 1.477763E-02 -1.200471E-03 9.170598E-03 0.000000E+00 1.844320E-03 1.838304E-03 + 9.285925E-01 6.453568E-03 5.000000E-01 9.285559E-01 6.453313E-03 0.000000E+00 9.068578E-01 1.518004E-02 -1.221647E-03 2.530993E-03 0.000000E+00 1.879184E-03 1.873170E-03 + 9.214652E-01 7.022170E-03 5.000000E-01 9.214289E-01 7.021894E-03 0.000000E+00 9.045332E-01 1.558881E-02 -1.240143E-03 -4.165837E-03 0.000000E+00 1.913765E-03 1.907782E-03 + 9.138872E-01 7.618947E-03 5.000000E-01 9.138512E-01 7.618646E-03 0.000000E+00 9.022295E-01 1.601212E-02 -1.256568E-03 -1.080105E-02 0.000000E+00 1.947607E-03 1.941684E-03 + 9.058310E-01 8.244078E-03 5.000000E-01 9.057953E-01 8.243753E-03 0.000000E+00 8.999812E-01 1.644836E-02 -1.271797E-03 -1.727622E-02 0.000000E+00 1.980050E-03 1.974206E-03 + 8.972635E-01 8.898770E-03 5.000000E-01 8.972281E-01 8.898419E-03 0.000000E+00 8.978009E-01 1.688845E-02 -1.286974E-03 -2.355606E-02 0.000000E+00 2.010552E-03 2.004790E-03 + 8.881456E-01 9.585664E-03 5.000000E-01 8.881106E-01 9.585287E-03 0.000000E+00 8.956373E-01 1.731879E-02 -1.303141E-03 -2.978710E-02 0.000000E+00 2.039106E-03 2.033411E-03 + 8.784302E-01 1.030898E-02 5.000000E-01 8.783955E-01 1.030858E-02 0.000000E+00 8.933518E-01 1.772203E-02 -1.319517E-03 -3.636127E-02 0.000000E+00 2.066522E-03 2.060875E-03 + 8.680511E-01 1.107397E-02 5.000000E-01 8.680169E-01 1.107353E-02 0.000000E+00 8.908439E-01 1.807475E-02 -1.330781E-03 -4.356164E-02 0.000000E+00 2.094151E-03 2.088557E-03 + 8.569008E-01 1.188539E-02 5.000000E-01 8.568670E-01 1.188492E-02 0.000000E+00 8.883060E-01 1.834260E-02 -1.327444E-03 -5.084494E-02 0.000000E+00 2.122633E-03 2.117151E-03 + 8.448105E-01 1.274679E-02 5.000000E-01 8.447772E-01 1.274629E-02 0.000000E+00 8.862150E-01 1.848015E-02 -1.302759E-03 -5.686511E-02 0.000000E+00 2.150373E-03 2.145099E-03 + 8.315513E-01 1.366267E-02 5.000000E-01 8.315185E-01 1.366213E-02 0.000000E+00 8.846171E-01 1.844526E-02 -1.262203E-03 -6.148111E-02 0.000000E+00 2.173458E-03 2.168446E-03 + 8.168677E-01 1.464627E-02 5.000000E-01 8.168355E-01 1.464569E-02 0.000000E+00 8.824909E-01 1.822369E-02 -1.220767E-03 -6.754388E-02 0.000000E+00 2.187738E-03 2.182920E-03 + 8.005336E-01 1.572371E-02 5.000000E-01 8.005021E-01 1.572309E-02 0.000000E+00 8.791559E-01 1.783739E-02 -1.178774E-03 -7.694478E-02 0.000000E+00 2.190819E-03 2.186112E-03 + 7.823774E-01 1.691034E-02 5.000000E-01 7.823466E-01 1.690968E-02 0.000000E+00 8.769829E-01 1.730982E-02 -1.106485E-03 -8.302255E-02 0.000000E+00 2.180030E-03 2.175599E-03 + 8.568334E-01 -1.615150E-03 5.000000E-01 8.567996E-01 -1.615086E-03 0.000000E+00 9.418312E-01 2.095896E-02 3.658859E-04 1.005031E-01 0.000000E+00 2.420404E-03 2.420054E-03 + 8.679829E-01 -1.431087E-03 5.000000E-01 8.679487E-01 -1.431030E-03 0.000000E+00 9.425060E-01 2.084891E-02 3.425828E-04 1.024651E-01 0.000000E+00 2.410123E-03 2.409815E-03 + 8.783611E-01 -1.270333E-03 5.000000E-01 8.783265E-01 -1.270283E-03 0.000000E+00 9.431044E-01 2.064718E-02 3.181916E-04 1.041987E-01 0.000000E+00 2.401830E-03 2.401560E-03 + 8.880758E-01 -1.129756E-03 5.000000E-01 8.880408E-01 -1.129712E-03 0.000000E+00 9.436463E-01 2.039254E-02 2.932791E-04 1.057626E-01 0.000000E+00 2.394905E-03 2.394672E-03 + 8.971929E-01 -1.007254E-03 5.000000E-01 8.971576E-01 -1.007214E-03 0.000000E+00 9.441251E-01 2.012229E-02 2.683172E-04 1.071401E-01 0.000000E+00 2.389094E-03 2.388895E-03 + 9.057598E-01 -9.011546E-04 5.000000E-01 9.057241E-01 -9.011191E-04 0.000000E+00 9.445325E-01 1.986380E-02 2.438155E-04 1.083097E-01 0.000000E+00 2.384381E-03 2.384213E-03 + 9.138154E-01 -8.099012E-04 5.000000E-01 9.137794E-01 -8.098693E-04 0.000000E+00 9.448696E-01 1.963215E-02 2.202656E-04 1.092763E-01 0.000000E+00 2.380714E-03 2.380574E-03 + 9.213928E-01 -7.319502E-04 5.000000E-01 9.213565E-01 -7.319213E-04 0.000000E+00 9.451449E-01 1.943061E-02 1.980562E-04 1.100646E-01 0.000000E+00 2.377825E-03 2.377711E-03 + 9.285195E-01 -6.657734E-04 5.000000E-01 9.284829E-01 -6.657472E-04 0.000000E+00 9.453682E-01 1.925154E-02 1.773417E-04 1.107029E-01 0.000000E+00 2.375301E-03 2.375208E-03 + 9.352206E-01 -6.099201E-04 5.000000E-01 9.351837E-01 -6.098960E-04 0.000000E+00 9.455488E-01 1.907820E-02 1.577954E-04 1.112177E-01 0.000000E+00 2.372875E-03 2.372802E-03 + 9.415286E-01 -5.631533E-04 5.000000E-01 9.414915E-01 -5.631311E-04 0.000000E+00 9.456946E-01 1.888898E-02 1.387161E-04 1.116305E-01 0.000000E+00 2.370603E-03 2.370545E-03 + 9.474858E-01 -5.244859E-04 5.000000E-01 9.474484E-01 -5.244653E-04 0.000000E+00 9.458081E-01 1.866310E-02 1.195926E-04 1.119468E-01 0.000000E+00 2.368645E-03 2.368602E-03 + 9.531360E-01 -4.931011E-04 5.000000E-01 9.530984E-01 -4.930817E-04 0.000000E+00 9.458866E-01 1.838528E-02 1.004076E-04 1.121561E-01 0.000000E+00 2.366900E-03 2.366869E-03 + 9.585172E-01 -4.682654E-04 5.000000E-01 9.584794E-01 -4.682470E-04 0.000000E+00 9.459277E-01 1.804749E-02 8.156609E-05 1.122485E-01 0.000000E+00 2.364747E-03 2.364727E-03 + 9.636574E-01 -4.492731E-04 5.000000E-01 9.636194E-01 -4.492554E-04 0.000000E+00 9.459282E-01 1.764847E-02 6.362841E-05 1.122142E-01 0.000000E+00 2.360922E-03 2.360909E-03 + 9.685736E-01 -4.354232E-04 5.000000E-01 9.685354E-01 -4.354061E-04 0.000000E+00 9.458838E-01 1.719666E-02 4.712843E-05 1.120444E-01 0.000000E+00 2.353896E-03 2.353889E-03 + 9.732737E-01 -4.260205E-04 5.000000E-01 9.732353E-01 -4.260037E-04 0.000000E+00 9.457942E-01 1.672631E-02 3.215995E-05 1.117490E-01 0.000000E+00 2.343695E-03 2.343692E-03 + 9.777586E-01 -4.203871E-04 5.000000E-01 9.777201E-01 -4.203705E-04 0.000000E+00 9.456877E-01 1.633673E-02 2.027334E-05 1.114240E-01 0.000000E+00 2.336193E-03 2.336192E-03 + 9.820255E-01 -4.178789E-04 5.000000E-01 9.819868E-01 -4.178624E-04 0.000000E+00 9.453731E-01 1.623640E-02 9.331972E-06 1.105504E-01 0.000000E+00 2.346100E-03 2.346099E-03 + 9.860694E-01 -4.178996E-04 5.000000E-01 9.860305E-01 -4.178832E-04 0.000000E+00 9.451589E-01 1.681199E-02 -1.387892E-06 1.100018E-01 0.000000E+00 2.380906E-03 2.380906E-03 + 9.904012E-01 -4.203243E-04 5.000000E-01 9.903622E-01 -4.203077E-04 0.000000E+00 9.455695E-01 1.858514E-02 -7.756270E-06 1.112473E-01 0.000000E+00 2.423344E-03 2.423344E-03 + 9.934757E-01 -4.234474E-04 5.000000E-01 9.934366E-01 -4.234308E-04 0.000000E+00 9.457769E-01 2.197832E-02 -1.742003E-05 1.119648E-01 0.000000E+00 2.466043E-03 2.466041E-03 + 9.956230E-01 -4.262769E-04 5.000000E-01 9.955837E-01 -4.262601E-04 0.000000E+00 9.452957E-01 2.719731E-02 -3.534340E-05 1.107957E-01 0.000000E+00 2.527701E-03 2.527698E-03 + 9.971056E-01 -4.285279E-04 5.000000E-01 9.970663E-01 -4.285110E-04 0.000000E+00 9.447832E-01 3.426382E-02 -5.186382E-05 1.095708E-01 0.000000E+00 2.612713E-03 2.612709E-03 + 9.981211E-01 -4.302056E-04 5.000000E-01 9.980818E-01 -4.301887E-04 0.000000E+00 9.445112E-01 4.283458E-02 -6.408236E-05 1.090059E-01 0.000000E+00 2.697456E-03 2.697452E-03 + 9.988130E-01 -4.314106E-04 5.000000E-01 9.987737E-01 -4.313936E-04 0.000000E+00 9.445384E-01 5.106542E-02 -1.014609E-04 1.092347E-01 0.000000E+00 2.707156E-03 2.707152E-03 + 9.992827E-01 -4.322566E-04 5.000000E-01 9.992433E-01 -4.322396E-04 0.000000E+00 9.449224E-01 5.442339E-02 2.618544E-05 1.105295E-01 0.000000E+00 2.505741E-03 2.505737E-03 + 9.996007E-01 -4.328422E-04 5.000000E-01 9.995613E-01 -4.328251E-04 0.000000E+00 9.389288E-01 4.678001E-02 -5.834615E-04 9.422103E-02 0.000000E+00 2.013439E-03 2.013435E-03 + 9.998156E-01 -4.332438E-04 5.000000E-01 9.997762E-01 -4.332267E-04 0.000000E+00 9.652188E-01 6.378814E-02 3.115090E-03 1.674000E-01 0.000000E+00 2.308594E-03 2.308590E-03 + 9.999607E-01 -4.335176E-04 5.000000E-01 9.999213E-01 -4.335005E-04 0.000000E+00 9.194356E-01 4.924938E-02 7.209547E-03 3.962806E-02 0.000000E+00 1.592428E-03 1.449650E-03 + 9.999738E-01 -2.890117E-04 5.000000E-01 9.999344E-01 -2.890003E-04 0.000000E+00 8.834192E-01 1.656528E-04 -3.672523E-02 -6.044585E-02 0.000000E+00 1.647927E-03 -1.488262E-04 + 9.999869E-01 -1.445059E-04 5.000000E-01 9.999475E-01 -1.445002E-04 0.000000E+00 9.200216E-01 -6.768082E-03 -5.688034E-02 4.276053E-02 0.000000E+00 2.152794E-03 -1.944214E-04 + 1.000000E+00 5.460349E-14 5.000000E-01 9.999606E-01 5.460185E-14 0.000000E+00 9.146963E-01 -1.246480E-03 -8.292641E-03 2.920251E-02 0.000000E+00 9.568096E-04 -2.843208E-05 + 1.000013E+00 1.445059E-04 5.000000E-01 9.999737E-01 1.445002E-04 0.000000E+00 9.191401E-01 2.078684E-03 3.861437E-02 4.127059E-02 0.000000E+00 1.463157E-03 1.321394E-04 + 1.000026E+00 2.890117E-04 5.000000E-01 9.999868E-01 2.890003E-04 0.000000E+00 8.910295E-01 5.427128E-03 3.024639E-02 -3.787454E-02 0.000000E+00 1.366723E-03 1.234304E-04 + 1.000039E+00 4.335176E-04 5.000000E-01 9.999999E-01 4.335005E-04 0.000000E+00 9.178565E-01 3.218372E-02 -5.635738E-03 3.712361E-02 0.000000E+00 1.164162E-03 9.562320E-04 + 9.657579E-03 -7.462679E-03 5.000000E-01 9.657198E-03 -7.462385E-03 0.000000E+00 9.183535E-01 4.666479E-02 -1.538191E-02 3.750595E-02 0.000000E+00 4.131688E-03 3.918618E-03 + 1.591313E-02 -9.124254E-03 5.000000E-01 1.591250E-02 -9.123894E-03 0.000000E+00 9.025181E-01 4.806517E-02 -1.097433E-02 -1.093925E-02 0.000000E+00 4.324674E-03 4.213519E-03 + 2.331488E-02 -1.055139E-02 5.000000E-01 2.331396E-02 -1.055098E-02 0.000000E+00 8.965480E-01 4.685386E-02 -7.923268E-03 -3.005983E-02 0.000000E+00 4.370616E-03 4.308254E-03 + 3.202893E-02 -1.182037E-02 5.000000E-01 3.202767E-02 -1.181991E-02 0.000000E+00 8.896745E-01 4.398983E-02 -5.632399E-03 -5.053972E-02 0.000000E+00 4.312881E-03 4.277367E-03 + 4.249529E-02 -1.297016E-02 5.000000E-01 4.249361E-02 -1.296965E-02 0.000000E+00 8.832212E-01 4.035761E-02 -3.862412E-03 -6.914083E-02 0.000000E+00 4.199755E-03 4.180288E-03 + 5.515994E-02 -1.399362E-02 5.000000E-01 5.515776E-02 -1.399307E-02 0.000000E+00 8.797259E-01 3.665546E-02 -2.528761E-03 -7.906228E-02 0.000000E+00 4.066228E-03 4.056307E-03 + 7.030603E-02 -1.485197E-02 5.000000E-01 7.030326E-02 -1.485139E-02 0.000000E+00 8.795595E-01 3.316821E-02 -1.550091E-03 -7.943075E-02 0.000000E+00 3.917887E-03 3.913398E-03 + 8.813378E-02 -1.550534E-02 5.000000E-01 8.813031E-02 -1.550473E-02 0.000000E+00 8.819338E-01 3.000358E-02 -8.520976E-04 -7.254173E-02 0.000000E+00 3.757152E-03 3.755484E-03 + 1.087929E-01 -1.592600E-02 5.000000E-01 1.087886E-01 -1.592538E-02 0.000000E+00 8.859163E-01 2.726018E-02 -3.688623E-04 -6.105432E-02 0.000000E+00 3.597488E-03 3.597062E-03 + 1.323683E-01 -1.609599E-02 5.000000E-01 1.323631E-01 -1.609536E-02 0.000000E+00 8.908008E-01 2.498222E-02 -4.091464E-05 -4.697405E-02 0.000000E+00 3.452021E-03 3.451967E-03 + 1.588655E-01 -1.600922E-02 5.000000E-01 1.588593E-01 -1.600859E-02 0.000000E+00 8.962510E-01 2.312065E-02 1.747640E-04 -3.126929E-02 0.000000E+00 3.323192E-03 3.323082E-03 + 1.881992E-01 -1.568312E-02 5.000000E-01 1.881918E-01 -1.568250E-02 0.000000E+00 9.019354E-01 2.158914E-02 3.096616E-04 -1.490025E-02 0.000000E+00 3.206942E-03 3.206618E-03 + 2.201872E-01 -1.514454E-02 5.000000E-01 2.201785E-01 -1.514395E-02 0.000000E+00 9.074908E-01 2.033350E-02 3.860694E-04 1.083300E-03 0.000000E+00 3.101968E-03 3.101426E-03 + 2.545510E-01 -1.444234E-02 5.000000E-01 2.545410E-01 -1.444177E-02 0.000000E+00 9.126535E-01 1.933639E-02 4.179903E-04 1.591620E-02 0.000000E+00 3.011081E-03 3.010400E-03 + 2.909230E-01 -1.363838E-02 5.000000E-01 2.909115E-01 -1.363785E-02 0.000000E+00 9.170070E-01 1.857688E-02 4.230656E-04 2.841757E-02 0.000000E+00 2.935557E-03 2.934819E-03 + 3.288612E-01 -1.277407E-02 5.000000E-01 3.288483E-01 -1.277356E-02 0.000000E+00 9.203483E-01 1.802618E-02 4.164445E-04 3.801695E-02 0.000000E+00 2.874478E-03 2.873731E-03 + 3.678697E-01 -1.188383E-02 5.000000E-01 3.678552E-01 -1.188336E-02 0.000000E+00 9.228653E-01 1.765932E-02 4.040552E-04 4.524603E-02 0.000000E+00 2.826600E-03 2.825878E-03 + 4.074229E-01 -1.099858E-02 5.000000E-01 4.074069E-01 -1.099815E-02 0.000000E+00 9.247671E-01 1.744995E-02 3.883316E-04 5.070356E-02 0.000000E+00 2.789766E-03 2.789092E-03 + 4.469923E-01 -1.014522E-02 5.000000E-01 4.469746E-01 -1.014482E-02 0.000000E+00 9.260519E-01 1.736175E-02 3.758366E-04 5.440127E-02 0.000000E+00 2.759775E-03 2.759145E-03 + 4.860709E-01 -9.318470E-03 5.000000E-01 4.860518E-01 -9.318103E-03 0.000000E+00 9.268147E-01 1.735837E-02 3.730613E-04 5.662453E-02 0.000000E+00 2.732195E-03 2.731580E-03 + 5.241963E-01 -8.507160E-03 5.000000E-01 5.241756E-01 -8.506825E-03 0.000000E+00 9.275234E-01 1.742258E-02 3.766485E-04 5.869957E-02 0.000000E+00 2.705529E-03 2.704913E-03 + 5.609667E-01 -7.720093E-03 5.000000E-01 5.609446E-01 -7.719789E-03 0.000000E+00 9.284599E-01 1.754804E-02 3.811533E-04 6.142411E-02 0.000000E+00 2.679941E-03 2.679326E-03 + 5.960533E-01 -6.967906E-03 5.000000E-01 5.960298E-01 -6.967632E-03 0.000000E+00 9.295134E-01 1.772769E-02 3.856693E-04 6.448824E-02 0.000000E+00 2.655437E-03 2.654827E-03 + 6.292047E-01 -6.257197E-03 5.000000E-01 6.291799E-01 -6.256950E-03 0.000000E+00 9.305617E-01 1.795580E-02 3.901036E-04 6.754712E-02 0.000000E+00 2.632162E-03 2.631559E-03 + 6.602470E-01 -5.594318E-03 5.000000E-01 6.602210E-01 -5.594098E-03 0.000000E+00 9.315928E-01 1.822770E-02 3.939439E-04 7.056301E-02 0.000000E+00 2.610339E-03 2.609749E-03 + 6.890788E-01 -4.983729E-03 5.000000E-01 6.890517E-01 -4.983533E-03 0.000000E+00 9.325991E-01 1.853630E-02 3.977322E-04 7.351174E-02 0.000000E+00 2.589769E-03 2.589193E-03 + 7.156633E-01 -4.425241E-03 5.000000E-01 7.156351E-01 -4.425067E-03 0.000000E+00 9.335855E-01 1.887325E-02 4.018549E-04 7.640634E-02 0.000000E+00 2.570030E-03 2.569468E-03 + 7.400180E-01 -3.918171E-03 5.000000E-01 7.399888E-01 -3.918017E-03 0.000000E+00 9.345871E-01 1.923058E-02 4.056590E-04 7.934534E-02 0.000000E+00 2.550773E-03 2.550227E-03 + 7.622041E-01 -3.461570E-03 5.000000E-01 7.621741E-01 -3.461434E-03 0.000000E+00 9.356022E-01 1.959702E-02 4.094466E-04 8.232463E-02 0.000000E+00 2.531354E-03 2.530823E-03 + 7.823159E-01 -3.051539E-03 5.000000E-01 7.822851E-01 -3.051419E-03 0.000000E+00 9.366271E-01 1.995737E-02 4.138259E-04 8.533382E-02 0.000000E+00 2.511033E-03 2.510515E-03 + 8.004707E-01 -2.683917E-03 5.000000E-01 8.004391E-01 -2.683811E-03 0.000000E+00 9.377133E-01 2.029576E-02 4.167475E-04 8.851532E-02 0.000000E+00 2.489863E-03 2.489360E-03 + 8.168035E-01 -2.357672E-03 5.000000E-01 8.167713E-01 -2.357579E-03 0.000000E+00 9.388784E-01 2.059302E-02 4.142201E-04 9.191304E-02 0.000000E+00 2.468922E-03 2.468444E-03 + 8.314859E-01 -2.072892E-03 5.000000E-01 8.314531E-01 -2.072810E-03 0.000000E+00 9.400241E-01 2.082186E-02 4.039887E-04 9.524550E-02 0.000000E+00 2.449731E-03 2.449290E-03 + 8.447441E-01 -1.827142E-03 5.000000E-01 8.447108E-01 -1.827070E-03 0.000000E+00 9.410212E-01 2.095083E-02 3.870335E-04 9.814534E-02 0.000000E+00 2.433453E-03 2.433057E-03 + 8.568334E-01 -1.615150E-03 5.000000E-01 8.567996E-01 -1.615086E-03 0.000000E+00 9.418312E-01 2.095896E-02 3.658859E-04 1.005031E-01 0.000000E+00 2.420404E-03 2.420054E-03 + 1 2 + 2 3 + 3 4 + 4 5 + 5 6 + 6 7 + 7 8 + 8 9 + 9 10 + 10 11 + 11 12 + 12 13 + 13 14 + 14 15 + 15 16 + 16 17 + 17 18 + 18 19 + 19 20 + 20 21 + 21 22 + 22 23 + 23 24 + 24 25 + 25 26 + 26 27 + 27 28 + 28 29 + 29 30 + 30 31 + 31 32 + 32 33 + 33 34 + 34 35 + 35 36 + 37 38 + 38 39 + 39 40 + 40 41 + 41 42 + 42 43 + 43 44 + 44 45 + 45 46 + 46 47 + 47 48 + 48 49 + 49 50 + 50 51 + 51 52 + 52 53 + 53 54 + 54 55 + 55 56 + 56 57 + 57 58 + 58 59 + 59 60 + 60 61 + 61 62 + 62 63 + 63 64 + 64 65 + 65 66 + 66 67 + 67 68 + 68 69 + 69 70 + 70 71 + 72 73 + 73 74 + 74 75 + 75 76 + 76 77 + 77 78 + 78 79 + 79 80 + 80 81 + 81 82 + 82 83 + 83 84 + 84 85 + 85 86 + 86 87 + 87 88 + 88 89 + 89 90 + 90 91 + 91 92 + 92 93 + 93 94 + 94 95 + 95 96 + 96 97 + 97 98 + 98 99 + 99 100 + 100 101 + 101 102 + 102 103 + 103 104 + 104 105 + 105 106 + 106 107 + 108 109 + 109 110 + 110 111 + 111 112 + 112 113 + 113 114 + 114 115 + 115 116 + 116 117 + 117 118 + 118 119 + 119 120 + 120 121 + 121 122 + 122 123 + 123 124 + 124 125 + 125 126 + 126 127 + 127 128 + 128 129 + 129 130 + 130 131 + 131 132 + 132 133 + 133 134 + 134 135 + 135 136 + 136 137 + 137 138 + 138 139 + 139 140 + 140 141 + 141 142 diff --git a/tests/input/airfoil_000_surf.plt b/tests/input/airfoil_000_surf.plt new file mode 100644 index 0000000000000000000000000000000000000000..cea16955e25b07312671461378f3d849529a93fc GIT binary patch literal 749920 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�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%0Df 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

sAur@BFW$#M6$u}7bymR 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

!2;d5 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 YO3QAA-)Pybv4)o225V{AZSgz(FOucRLI3~& literal 0 HcmV?d00001 diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py new file mode 100644 index 0000000..6bce371 --- /dev/null +++ b/tests/test_tecplotIO.py @@ -0,0 +1,679 @@ +import tempfile +import unittest +from itertools import product +from pathlib import Path +from typing import List, Tuple + +import numpy as np +import numpy.typing as npt + +from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, readTecplot, writeTecplot + + +def createBrickGrid(nx: int, ny: int, nz: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: + # Create node coordinates + x = np.linspace(0, 1, nx + 1) + y = np.linspace(0, 1, ny + 1) + z = np.linspace(0, 1, nz + 1) + xx, yy, zz = np.meshgrid(x, y, z) + nodes = np.column_stack((xx.flatten(), yy.flatten(), zz.flatten())) + + # Create element connectivity + connectivity = [] + for k in range(nz): + for j in range(ny): + for i in range(nx): + # Get the eight corners of the hexahedron + n0 = i + j * (nx + 1) + k * (nx + 1) * (ny + 1) + n1 = n0 + 1 + n2 = n1 + (nx + 1) + n3 = n2 - 1 + n4 = n0 + (nx + 1) * (ny + 1) + n5 = n4 + 1 + n6 = n5 + (nx + 1) + n7 = n6 - 1 + + connectivity.append([n0, n1, n2, n3, n4, n5, n6, n7]) + + connectivity = np.array(connectivity) + + return nodes, connectivity + + +def createTetGrid(nx: int, ny: int, nz: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: + # Create node coordinates + x = np.linspace(0, 1, nx + 1) + y = np.linspace(0, 1, ny + 1) + z = np.linspace(0, 1, nz + 1) + xx, yy, zz = np.meshgrid(x, y, z) + nodes = np.column_stack((xx.flatten(), yy.flatten(), zz.flatten())) + + # Create element connectivity + connectivity = [] + for k in range(nz): + for j in range(ny): + for i in range(nx): + # Get the eight corners of the hexahedron + n0 = i + j * (nx + 1) + k * (nx + 1) * (ny + 1) + n1 = n0 + 1 + n2 = n1 + (nx + 1) + n3 = n2 - 1 + n4 = n0 + (nx + 1) * (ny + 1) + n5 = n4 + 1 + n6 = n5 + (nx + 1) + n7 = n6 - 1 + + # Basic subdivision (always add these) + connectivity.extend( + [ + [n0, n1, n3, n7], + [n0, n1, n4, n7], + [n1, n2, n3, n7], + [n1, n2, n6, n7], + [n1, n4, n5, n7], + [n1, n5, n6, n7], + ] + ) + + # Additional tetrahedra for boundary faces + if i == 0: + connectivity.append([n0, n3, n4, n7]) + if i == nx - 1: + connectivity.append([n1, n2, n5, n6]) + if j == 0: + connectivity.append([n0, n1, n4, n5]) + if j == ny - 1: + connectivity.append([n2, n3, n6, n7]) + if k == 0: + connectivity.append([n0, n1, n2, n3]) + if k == nz - 1: + connectivity.append([n4, n5, n6, n7]) + + connectivity = np.array(connectivity) + + return nodes, connectivity + + +# Create a finite element mesh with connectivity +def createQuadGrid(nx: int, ny: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: + # Create node coordinates + x = np.linspace(0, 1, nx + 1) + y = np.linspace(0, 1, ny + 1) + xx, yy = np.meshgrid(x, y) + nodes = np.column_stack((xx.flatten(), yy.flatten())) + + # Create element connectivity + connectivity = [] + for j in range(ny): + for i in range(nx): + n1 = i + j * (nx + 1) + n2 = n1 + 1 + n3 = n2 + nx + 1 + n4 = n3 - 1 + connectivity.append([n1, n2, n3, n4]) + + connectivity = np.array(connectivity) + + return nodes, connectivity + + +def createTriGrid(nx: int, ny: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: + # Create node coordinates + x = np.linspace(0, 1, nx + 1) + y = np.linspace(0, 1, ny + 1) + xx, yy = np.meshgrid(x, y) + nodes = np.column_stack((xx.flatten(), yy.flatten())) + + # Create element connectivity + connectivity = [] + for j in range(ny): + for i in range(nx): + n1 = i + j * (nx + 1) + n2 = n1 + 1 + n3 = n2 + nx + 1 + connectivity.append([n1, n2, n3]) + + n1 = i + j * (nx + 1) + n2 = n1 + nx + 1 + n3 = n2 + 1 + connectivity.append([n1, n2, n3]) + + connectivity = np.array(connectivity) + + return nodes, connectivity + + +def createLineSegGrid(nx: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: + # Create node coordinates + x = np.linspace(0, 1, nx + 1) + nodes = np.column_stack((x, x**2)) + + # Create element connectivity + connectivity = np.column_stack((np.arange(nx), np.arange(1, nx + 1))) + + return nodes, connectivity + + +class TestTecplotIO(unittest.TestCase): + N_PROCS = 1 + + def setUp(self): + np.random.seed(123) + + thisDir = Path(__file__).parent + self.externalFileAscii = thisDir / "input" / "airfoil_000_slices.dat" + self.externalFileBinary = thisDir / "input" / "airfoil_000_surf.plt" + + def assertOrderedZonesEqual(self, zones1: List[TecplotOrderedZone], zones2: List[TecplotOrderedZone]): + self.assertEqual(len(zones1), len(zones2)) + for zone1, zone2 in zip(zones1, zones2): + self.assertEqual(zone1.name, zone2.name) + self.assertEqual(zone1.shape, zone2.shape) + self.assertListEqual(zone1.variables, zone2.variables) + self.assertEqual(zone1.iMax, zone2.iMax) + self.assertEqual(zone1.jMax, zone2.jMax) + self.assertEqual(zone1.kMax, zone2.kMax) + self.assertEqual(zone1.solutionTime, zone2.solutionTime) + self.assertEqual(zone1.strandID, zone2.strandID) + + def assertFEZonesEqual(self, zones1: List[TecplotFEZone], zones2: List[TecplotFEZone]): + self.assertEqual(len(zones1), len(zones2)) + for zone1, zone2 in zip(zones1, zones2): + self.assertEqual(zone1.name, zone2.name) + self.assertEqual(zone1.shape, zone2.shape) + self.assertListEqual(zone1.variables, zone2.variables) + self.assertEqual(zone1.nElements, zone2.nElements) + self.assertEqual(zone1.nNodes, zone2.nNodes) + self.assertEqual(zone1.solutionTime, zone2.solutionTime) + self.assertEqual(zone1.strandID, zone2.strandID) + self.assertEqual(zone1.tetrahedral, zone2.tetrahedral) + + def test_orderedZone(self): + # Create a 3D grid of shape (nx, ny, nz, 3) + nx, ny, nz = 10, 10, 10 + X = np.random.rand(nx, ny, nz) + Y = np.random.rand(nx, ny, nz) + Z = np.random.rand(nx, ny, nz) + + # Create a Tecplot zone + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + + self.assertEqual(zone.name, "Grid") + self.assertEqual(zone.shape, (nx, ny, nz)) + self.assertEqual(len(zone.variables), 3) + self.assertListEqual(zone.variables, ["X", "Y", "Z"]) + self.assertEqual(zone.iMax, nx) + self.assertEqual(zone.jMax, ny) + self.assertEqual(zone.kMax, nz) + self.assertEqual(zone.solutionTime, 0.0) + self.assertEqual(zone.strandID, -1) + + def test_FEZone(self): + # Create a quad grid + nx, ny = 10, 10 + nodes, connectivity = createQuadGrid(nx, ny) + + solutionTime = 10.0 + strandID = 4 + # Create a Tecplot zone with Quad elements + zone = TecplotFEZone( + "QuadGrid", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity, solutionTime=solutionTime, strandID=strandID + ) + + self.assertEqual(zone.name, "QuadGrid") + self.assertEqual(zone.shape, ((nx + 1) * (ny + 1),)) + self.assertEqual(len(zone.variables), 2) + self.assertListEqual(zone.variables, ["X", "Y"]) + self.assertEqual(zone.nElements, connectivity.shape[0]) + self.assertEqual(zone.nNodes, (nx + 1) * (ny + 1)) + self.assertEqual(zone.solutionTime, solutionTime) + self.assertEqual(zone.strandID, strandID) + + nx, ny, nz = 10, 10, 10 + nodes, connectivity = createTetGrid(nx, ny, nz) + + # Create a Tecplot zone with Tet elements + zone = TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + solutionTime=solutionTime, + strandID=strandID, + tetrahedral=True, + ) + + self.assertEqual(zone.name, "TetGrid") + self.assertEqual(zone.shape, ((nx + 1) * (ny + 1) * (nz + 1),)) + self.assertEqual(len(zone.variables), 3) + self.assertListEqual(zone.variables, ["X", "Y", "Z"]) + self.assertEqual(zone.nElements, connectivity.shape[0]) + self.assertEqual(zone.nNodes, (nx + 1) * (ny + 1) * (nz + 1)) + self.assertEqual(zone.solutionTime, solutionTime) + self.assertEqual(zone.strandID, strandID) + + def test_ASCIIReadWriteOrderedZones(self): + zones: List[TecplotOrderedZone] = [] + + X = np.random.rand(10) + Y = np.random.rand(10) + Z = np.random.rand(10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + X = np.random.rand(10, 10) + Y = np.random.rand(10, 10) + Z = np.random.rand(10, 10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + X = np.random.rand(10, 10) + Y = np.random.rand(10, 10) + Z = np.random.rand(10, 10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + X = np.random.rand(10, 10, 10) + Y = np.random.rand(10, 10, 10) + Z = np.random.rand(10, 10, 10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + title = "ASCII ORDERED ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertOrderedZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLEs") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertOrderedZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertOrderedZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + def test_BinaryReadWriteOrderedZones(self): + zones: List[TecplotOrderedZone] = [] + + X = np.random.rand(10) + Y = np.random.rand(10) + Z = np.random.rand(10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + X = np.random.rand(10, 10) + Y = np.random.rand(10, 10) + Z = np.random.rand(10, 10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + X = np.random.rand(10, 10) + Y = np.random.rand(10, 10) + Z = np.random.rand(10, 10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + X = np.random.rand(10, 10, 10) + Y = np.random.rand(10, 10, 10) + Z = np.random.rand(10, 10, 10) + zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) + zones.append(zone) + + title = "BINARY ORDERED ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertOrderedZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertOrderedZonesEqual(zones, zonesRead) + + def test_ASCIIReadWriteFELineSegZones(self): + zones: List[TecplotFEZone] = [] + + for nx in range(2, 10): + nodes, connectivity = createLineSegGrid(nx) + zone = TecplotFEZone(f"LineSegGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) + zones.append(zone) + + title = "ASCII FELINESEG ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_BinaryReadWriteFELineSegZones(self): + zones: List[TecplotFEZone] = [] + + for nx in range(2, 10): + nodes, connectivity = createLineSegGrid(nx) + zone = TecplotFEZone(f"LineSegGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) + zones.append(zone) + + title = "BINARY FELINESEG ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_ASCIIReadWriteFETriZones(self): + zones: List[TecplotFEZone] = [] + dims = product(range(2, 10), range(2, 10)) + + for nx, ny in dims: + nodes, connectivity = createTriGrid(nx, ny) + zone = TecplotFEZone(f"TriGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) + zones.append(zone) + + title = "ASCII FETRIANGLE ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_BinaryReadWriteFETriZones(self): + zones: List[TecplotFEZone] = [] + dims = product(range(2, 10), range(2, 10)) + + for nx, ny in dims: + nodes, connectivity = createTriGrid(nx, ny) + zone = TecplotFEZone(f"TriGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) + zones.append(zone) + + title = "BINARY FETRIANGLE ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_ASCIIReadWriteFEQuadZones(self): + zones: List[TecplotFEZone] = [] + dims = product(range(2, 10), range(2, 10)) + + for nx, ny in dims: + nodes, connectivity = createQuadGrid(nx, ny) + zone = TecplotFEZone(f"QuadGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) + zones.append(zone) + + title = "ASCII FEQUADRILATERAL ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_BinaryReadWriteFEQuadZones(self): + zones: List[TecplotFEZone] = [] + dims = product(range(2, 10), range(2, 10)) + + for nx, ny in dims: + nodes, connectivity = createQuadGrid(nx, ny) + zone = TecplotFEZone(f"QuadGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) + zones.append(zone) + + title = "BINARY FEQUADRILATERAL ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_ASCIIReadWriteFETetZones(self): + zones: List[TecplotFEZone] = [] + npts = 5 + dims = product(range(2, npts), range(2, npts), range(2, npts)) + + for nx, ny, nz in dims: + nodes, connectivity = createTetGrid(nx, ny, nz) + zone = TecplotFEZone( + f"TetGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity, tetrahedral=True + ) + zones.append(zone) + + title = "ASCII FETETRAHEDRAL ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_BinaryReadWriteFETetZones(self): + zones: List[TecplotFEZone] = [] + npts = 5 + dims = product(range(2, npts), range(2, npts), range(2, npts)) + + for nx, ny, nz in dims: + nodes, connectivity = createTetGrid(nx, ny, nz) + zone = TecplotFEZone( + f"TetGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity, tetrahedral=True + ) + zones.append(zone) + + title = "BINARY FETETRAHEDRAL ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_ASCIIReadWriteFEBrickZones(self): + zones: List[TecplotFEZone] = [] + npts = 5 + dims = product(range(2, npts), range(2, npts), range(2, npts)) + + for nx, ny, nz in dims: + nodes, connectivity = createBrickGrid(nx, ny, nz) + zone = TecplotFEZone( + f"BrickGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity + ) + zones.append(zone) + + title = "ASCII FEBRICK ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_BinaryReadWriteFEBrickZones(self): + zones: List[TecplotFEZone] = [] + npts = 5 + dims = product(range(2, npts), range(2, npts), range(2, npts)) + + for nx, ny, nz in dims: + nodes, connectivity = createBrickGrid(nx, ny, nz) + zone = TecplotFEZone( + f"BrickGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity + ) + zones.append(zone) + + title = "BINARY FEBRICK ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="SINGLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + titleRead, zonesRead = readTecplot(tmpfile.name) + + self.assertEqual(titleRead, title) + self.assertFEZonesEqual(zones, zonesRead) + + def test_ASCIIReadWriteExternal(self): + try: + title, zones = readTecplot(self.externalFileAscii) + except FileNotFoundError: + self.fail(f"Reading external ASCII file {self.externalFileAscii} failed.") + + def test_BinaryReadWriteExternal(self): + try: + title, zones = readTecplot(self.externalFileBinary) + except FileNotFoundError: + self.fail(f"Reading external binary file {self.externalFileBinary} failed.") From 16191ce94d086f9ae8a41d527b64df28a8115d7f Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sat, 29 Jun 2024 01:21:42 -0400 Subject: [PATCH 02/28] Fixing black --- baseclasses/utils/tecplotIO.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index f2edcb4..e74525a 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -3,8 +3,7 @@ from abc import ABC, abstractmethod from enum import Enum from pathlib import Path -from typing import (Any, Dict, Generic, List, Literal, TextIO, Tuple, TypeVar, - Union) +from typing import Any, Dict, Generic, List, Literal, TextIO, Tuple, TypeVar, Union import numpy as np import numpy.typing as npt From 3718d068494ba7da970e329ef56c9b65240ad45e Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sat, 29 Jun 2024 12:35:40 -0400 Subject: [PATCH 03/28] Adding docs for tecplot module --- baseclasses/utils/__init__.py | 4 +- baseclasses/utils/tecplotIO.py | 131 +++++++++++++++++++++++++++------ 2 files changed, 111 insertions(+), 24 deletions(-) diff --git a/baseclasses/utils/__init__.py b/baseclasses/utils/__init__.py index c35beb1..df9b3ab 100644 --- a/baseclasses/utils/__init__.py +++ b/baseclasses/utils/__init__.py @@ -2,7 +2,7 @@ from .error import Error from .fileIO import readJSON, readPickle, redirectingIO, redirectIO, writeJSON, writePickle from .solverHistory import SolverHistory -from .tecplotIO import TecplotFEZone, TecplotOrderedZone, readTecplot, writeTecplot +from .tecplotIO import TecplotZone, TecplotFEZone, TecplotOrderedZone, readTecplot, writeTecplot from .utils import ParseStringFormat, getPy3SafeString, pp __all__ = [ @@ -18,8 +18,10 @@ "redirectIO", "redirectingIO", "SolverHistory", + "TecplotZone", "TecplotFEZone", "TecplotOrderedZone", "writeTecplot", + "readTecplot", "ParseStringFormat", ] diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index e74525a..c84001d 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -70,8 +70,10 @@ class StrandID(Enum): # DATA STRUCTURES # ============================================================================== class TecplotZone: + """Base class for Tecplot zones.""" + def __init__(self, name: str, data: Dict[str, npt.NDArray], solutionTime: float = 0.0, strandID: int = -1): - """Base class for Tecplot zones. + """Create a tecplot zone object. Parameters ---------- @@ -115,6 +117,10 @@ def _validateShape(self) -> None: class TecplotOrderedZone(TecplotZone): + """Tecplot ordered zone. These zones do not contain connectivity information + because the data is ordered in an (i, j, k) grid. + """ + def __init__( self, name: str, @@ -122,8 +128,23 @@ def __init__( solutionTime: float = 0.0, strandID: int = -1, ): - """Tecplot ordered zone. These zones do not contain connectivity information - because the data is ordered in a structured grid. + """To create a tecplot ordered zone object: + + .. code-block:: python + + # --- Example usage --- + # Create the data + nx, ny = 10, 10 + X = np.random.rand(nx, ny) + Y = np.random.rand(nx, ny) + + # Create the zone + zone = TecplotOrderedZone( + "OrderedZone", + {"X": X, "Y": Y}, + solutionTime=0.0, + strandID=-1, + ) Parameters ---------- @@ -152,6 +173,20 @@ def kMax(self) -> int: class TecplotFEZone(TecplotZone): + """Tecplot finite element zone. These zones contain connectivity information + to describe the elements in the zone. The type of element is determined by the + shape of the connectivity array and the ``tetrahedral`` flag. The connectivity + array is 0-based. + + The following shapes correspond to the following element types, where n is the number of elements: + + - ``(n, 2)``: FELINESEG + - ``(n, 3)``: FETRIANGLE + - ``(n, 4)``, ``tetrahedral=False``: FEQUADRILATERAL + - ``(n, 4)``, ``tetrahedral=True``: FETETRAHEDRON + - ``(n, 8)``: FEBRICK + """ + def __init__( self, zoneName: str, @@ -161,8 +196,27 @@ def __init__( solutionTime: float = 0.0, strandID: int = -1, ): - """Tecplot finite element zone. These zones contain connectivity information - to describe the elements in the zone. + """To create a tecplot finite element zone object: + + .. code-block:: python + + # --- Example usage --- + # Create node coordinates + x = np.linspace(0, 1, nx + 1) + nodes = np.column_stack((x, x**2)) + + # Create element connectivity + connectivity = np.column_stack((np.arange(nx), np.arange(1, nx + 1))) + + # Create the zone + zone = TecplotFEZone( + "FEZone", + {"x": nodes[:, 0], "y": nodes[:, 1]}, + connectivity, + tetrahedral=False, + solutionTime=0.0, + strandID=-1, + ) Parameters ---------- @@ -204,9 +258,9 @@ def __init__( ---------- zone : T The Tecplot zone to write. - datapacking : Literal["BLOCK", "POINT"] + datapacking : Literal["BLOCK", "POINT"] The data packing format. BLOCK is row-major, POINT is column-major. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ self.zone = zone @@ -239,9 +293,9 @@ def __init__( ---------- zone : TecplotOrderedZone The ordered zone to write. - datapacking : Literal["BLOCK", "POINT"] + datapacking : Literal["BLOCK", "POINT"] The data packing format. BLOCK is row-major, POINT is column-major. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ super().__init__(zone, datapacking, precision) @@ -316,9 +370,9 @@ def __init__( ---------- zone : TecplotFEZone The finite element zone to write. - datapacking : Literal["BLOCK", "POINT"] + datapacking : Literal["BLOCK", "POINT"] The data packing format. BLOCK is row-major, POINT is column-major. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ super().__init__(zone, datapacking, precision) @@ -420,9 +474,9 @@ def __init__( The title of the Tecplot file. zones : List[TecplotZone] A list of Tecplot zones to write. - datapacking : Literal["BLOCK", "POINT"] + datapacking : Literal["BLOCK", "POINT"] The data packing format. BLOCK is row-major, POINT is column-major. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ self.title = title @@ -563,7 +617,7 @@ def __init__( The title of the Tecplot file. zone : T The Tecplot zone to write. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ self.title = title @@ -687,7 +741,7 @@ def __init__( The title of the Tecplot file. zone : TecplotOrderedZone The ordered zone to write. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ super().__init__(title, zone, precision) @@ -734,7 +788,7 @@ def __init__( The title of the Tecplot file. zone : TecplotFEZone The finite element zone to write. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ super().__init__(title, zone, precision) @@ -783,7 +837,7 @@ def __init__( The title of the Tecplot file. zones : List[TecplotZone] A list of Tecplot zones to write. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ self.title = title @@ -1468,10 +1522,10 @@ def writeTecplot( binary format. If the extension is .dat, the file will be written in ASCII format. - Note - ---- - - ASCII files can be written with either BLOCK or POINT data packing. - - Binary files are always written with BLOCK data packing. + .. note:: + + - ASCII files can be written with either BLOCK or POINT data packing. + - Binary files are always written with BLOCK data packing. Parameters ---------- @@ -1481,15 +1535,34 @@ def writeTecplot( The title of the Tecplot file. zones : List[TecplotZone] A list of Tecplot zones to write - datapacking : Literal["BLOCK", "POINT"], optional + datapacking : Literal["BLOCK", "POINT"], optional The data packing format. BLOCK is row-major, POINT is column-major, by default "POINT" - precision : Literal["SINGLE", "DOUBLE"], optional + precision : Literal["SINGLE", "DOUBLE"], optional The floating point precision to write the data, by default "SINGLE" Raises ------ ValueError If the file extension is invalid. + + Examples + -------- + .. code-block:: python + + from baseclasses.utils import TecplotOrderedZone, writeTecplot + import numpy as np + + nx, ny, nz = 10, 10, 10 + X, Y, Z = np.meshgrid(np.random.rand(nx), np.random.rand(ny), np.random.rand(nz), indexing="ij") + data = {"X": X, "Y": Y, "Z": Z} + zone = TecplotOrderedZone("OrderedZone", data) + + # Write the Tecplot file in ASCII format + writeTecplot("ordered.dat", "Ordered Zone", [zone], datapacking="BLOCK", precision="SINGLE") + + # Write the Tecplot file in binary format + writeTecplot("ordered.plt", "Ordered Zone", [zone], precision="SINGLE") + """ filepath = Path(filename) if filepath.suffix == ".plt": @@ -1522,6 +1595,18 @@ def readTecplot(filename: Union[str, Path]) -> Tuple[str, List[Union[TecplotOrde ------ ValueError If the file extension is invalid. + + Examples + -------- + .. code-block:: python + + from baseclasses.utils import readTecplot + + # Read a Tecplot file in ASCII format + title, zones = readTecplot("ordered.dat") + + # Read a Tecplot file in binary format + title, zones = readTecplot("ordered.plt") """ filepath = Path(filename) if filepath.suffix == ".plt": From b277258f483a077cc706f802a3acc7d216392d66 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sat, 29 Jun 2024 12:44:06 -0400 Subject: [PATCH 04/28] Bumped the minor version --- baseclasses/__init__.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/baseclasses/__init__.py b/baseclasses/__init__.py index 9ecf32e..2ac75e6 100644 --- a/baseclasses/__init__.py +++ b/baseclasses/__init__.py @@ -1,24 +1,21 @@ -__version__ = "1.8.0" +__version__ = "1.9.0" from .problems import ( AeroProblem, - TransiProblem, - StructProblem, AeroStructProblem, + EngineProblem, + FieldPerformanceProblem, + FluidProperties, + FuelCase, + ICAOAtmosphere, + LGProblem, MissionProblem, MissionProfile, MissionSegment, + StructProblem, + TransiProblem, WeightProblem, - FuelCase, - FluidProperties, - ICAOAtmosphere, - EngineProblem, - FieldPerformanceProblem, - LGProblem, ) - -from .solvers import BaseSolver, AeroSolver - -from .utils import getPy3SafeString - +from .solvers import AeroSolver, BaseSolver from .testing import BaseRegTest, getTol +from .utils import getPy3SafeString From 0f35428d45bc72f1feb0b2970e5fa5b187a64725 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sat, 29 Jun 2024 12:58:49 -0400 Subject: [PATCH 05/28] Fixed exception in external io test to catch all errors --- tests/test_tecplotIO.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index 6bce371..caef168 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -669,11 +669,11 @@ def test_BinaryReadWriteFEBrickZones(self): def test_ASCIIReadWriteExternal(self): try: title, zones = readTecplot(self.externalFileAscii) - except FileNotFoundError: - self.fail(f"Reading external ASCII file {self.externalFileAscii} failed.") + except Exception as e: + self.fail(f"Reading external ASCII file {self.externalFileAscii} failed with error: {e}") def test_BinaryReadWriteExternal(self): try: title, zones = readTecplot(self.externalFileBinary) - except FileNotFoundError: - self.fail(f"Reading external binary file {self.externalFileBinary} failed.") + except Exception as e: + self.fail(f"Reading external binary file {self.externalFileBinary} failed with error: {e}") From b03bcacd909b5ece4909e8c7f722a529926bc50b Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 19 Jul 2024 23:56:36 -0400 Subject: [PATCH 06/28] Adding zone input validation, better use of enums, refactoring, and better testing --- baseclasses/utils/__init__.py | 2 +- baseclasses/utils/tecplotIO.py | 337 +++++++-------- tests/test_tecplotIO.py | 729 +++++++++++++-------------------- 3 files changed, 464 insertions(+), 604 deletions(-) diff --git a/baseclasses/utils/__init__.py b/baseclasses/utils/__init__.py index df9b3ab..117558f 100644 --- a/baseclasses/utils/__init__.py +++ b/baseclasses/utils/__init__.py @@ -2,7 +2,7 @@ from .error import Error from .fileIO import readJSON, readPickle, redirectingIO, redirectIO, writeJSON, writePickle from .solverHistory import SolverHistory -from .tecplotIO import TecplotZone, TecplotFEZone, TecplotOrderedZone, readTecplot, writeTecplot +from .tecplotIO import TecplotFEZone, TecplotOrderedZone, TecplotZone, ZoneType, readTecplot, writeTecplot from .utils import ParseStringFormat, getPy3SafeString, pp __all__ = [ diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index c84001d..1fa3535 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -15,6 +15,7 @@ # ENUMS # ============================================================================== class ZoneType(Enum): + UNSET = -1 ORDERED = 0 FELINESEG = 1 FETRIANGLE = 2 @@ -35,6 +36,11 @@ class VariableLocation(Enum): class DataPrecision(Enum): + SINGLE = 6 + DOUBLE = 12 + + +class BinaryDataPrecisionCodes(Enum): SINGLE = 1 DOUBLE = 2 @@ -72,7 +78,13 @@ class StrandID(Enum): class TecplotZone: """Base class for Tecplot zones.""" - def __init__(self, name: str, data: Dict[str, npt.NDArray], solutionTime: float = 0.0, strandID: int = -1): + def __init__( + self, + name: str, + data: Dict[str, npt.NDArray], + solutionTime: float = 0.0, + strandID: int = -1, + ): """Create a tecplot zone object. Parameters @@ -90,7 +102,11 @@ def __init__(self, name: str, data: Dict[str, npt.NDArray], solutionTime: float self.data = data self.solutionTime = solutionTime self.strandID = strandID - self._validateShape() + self.zoneType: Union[str, ZoneType] = ZoneType.UNSET + self._validateName() + self._validateData() + self._validateSolutionTime() + self._validateStrandID() @property def variables(self) -> List[str]: @@ -104,17 +120,65 @@ def shape(self) -> Tuple[int, ...]: def nNodes(self) -> int: return np.multiply.reduce(self.shape) - def _validateShape(self) -> None: - """Check that all variables have the same shape. + def _validateName(self) -> None: + """Check that the zone name is a valid string. + + Raises + ------ + TypeError + If the zone name is not a valid string. + """ + if not isinstance(self.name, str): + raise TypeError("Zone name must be a string.") + + def _validateData(self) -> None: + """Check that the data is a valid dictionary and the values + are numpy arrays that have the same shape. Raises ------ + TypeError + If the data is not a dictionary or the values are not numpy arrays. ValueError - If all variable data arrays do not have the same shape. + If the variables do not have the same shape. """ + if not isinstance(self.data, dict): + raise TypeError("Data must be a dictionary.") + + for val in self.data.values(): + if not isinstance(val, np.ndarray): + raise TypeError("Data values must be numpy arrays.") + if not all(self.data[var].shape == self.shape for var in self.variables): raise ValueError("All variables must have the same shape.") + def _validateSolutionTime(self) -> None: + """Check that the solution time is a valid float. + + Raises + ------ + TypeError + If the solution time is not a float. + ValueError + If the solution time is less than zero. + """ + if not isinstance(self.solutionTime, float): + raise TypeError("Solution time must be a float.") + + if self.solutionTime < 0.0: + raise ValueError("Solution time must be greater than or equal to zero.") + + def _validateStrandID(self) -> None: + """Check that the strand ID is a valid integer. + + Raises + ------ + TypeError + If the strand ID is not an integer. + """ + if not isinstance(self.strandID, int): + raise TypeError("Strand ID must be an integer.") + class TecplotOrderedZone(TecplotZone): """Tecplot ordered zone. These zones do not contain connectivity information @@ -152,12 +216,15 @@ def __init__( The name of the zone. data : Dict[str, npt.NDArray] A dictionary of variable names and their corresponding data. + zoneType : Union[str, ZoneType], optional + The type of the zone, by default ZoneType.ORDERED solutionTime : float, optional The solution time of the zone, by default 0.0 strandID : int, optional The strand id of the zone, by default -1 """ super().__init__(name, data, solutionTime=solutionTime, strandID=strandID) + self.zoneType = ZoneType.ORDERED @property def iMax(self) -> int: @@ -182,8 +249,8 @@ class TecplotFEZone(TecplotZone): - ``(n, 2)``: FELINESEG - ``(n, 3)``: FETRIANGLE - - ``(n, 4)``, ``tetrahedral=False``: FEQUADRILATERAL - - ``(n, 4)``, ``tetrahedral=True``: FETETRAHEDRON + - ``(n, 4)``, FEQUADRILATERAL + - ``(n, 4)``, FETETRAHEDRON - ``(n, 8)``: FEBRICK """ @@ -192,7 +259,7 @@ def __init__( zoneName: str, data: Dict[str, npt.NDArray], connectivity: npt.NDArray, - tetrahedral: bool = False, + zoneType: Union[str, ZoneType], solutionTime: float = 0.0, strandID: int = -1, ): @@ -213,7 +280,7 @@ def __init__( "FEZone", {"x": nodes[:, 0], "y": nodes[:, 1]}, connectivity, - tetrahedral=False, + zoneType="FELINESEG", solutionTime=0.0, strandID=-1, ) @@ -226,8 +293,9 @@ def __init__( A dictionary of variable names and their corresponding data. connectivity : npt.NDArray The connectivity array that describes the elements in the zone. - tetrahedral : bool, optional - Flag to distinguish quadrilateral from tetrahedral zones, by default False + zoneType : Union[str, ZoneType] + The type of the zone. Can be a string that matches an entry + in the ZoneType enum or the ZoneType enum itself. solutionTime : float, optional The solution time of the zone, by default 0.0 strandID : int, optional @@ -235,12 +303,42 @@ def __init__( """ super().__init__(zoneName, data, solutionTime=solutionTime, strandID=strandID) self.connectivity = connectivity - self.tetrahedral = tetrahedral + self.zoneType = zoneType + self._validateZoneType() + self._validateConnectivity() @property def nElements(self) -> int: return self.connectivity.shape[0] + def _validateZoneType(self) -> None: + supportedZones = [zone.name for zone in ZoneType if zone.name != "ORDERED"] + if isinstance(self.zoneType, str): + if self.zoneType.upper() not in supportedZones: + raise ValueError("Invalid zone type.") + self.zoneType = ZoneType[self.zoneType.upper()] + elif isinstance(self.zoneType, ZoneType): + if self.zoneType.name not in supportedZones: + raise ValueError("Invalid zone type.") + else: + raise ValueError("Invalid zone type.") + + def _validateConnectivity(self) -> None: + if self.zoneType == ZoneType.FELINESEG: + assert self.connectivity.shape[1] == 2, "Connectivity shape does not match zone type." + elif self.zoneType == ZoneType.FETRIANGLE: + assert self.connectivity.shape[1] == 3, "Connectivity shape does not match zone type." + elif self.zoneType == ZoneType.FEQUADRILATERAL: + assert self.connectivity.shape[1] == 4, "Connectivity shape does not match zone type." + elif self.zoneType == ZoneType.FETETRAHEDRON: + assert self.connectivity.shape[1] == 4, "Connectivity shape does not match zone type." + elif self.zoneType == ZoneType.FEBRICK: + assert self.connectivity.shape[1] == 8, "Connectivity shape does not match zone type." + else: + # Prior validation step should ensure we don't reach this point + # but raise an error just in case. + raise TypeError("Invalid zone type.") + # ============================================================================== # ASCII WRITERS @@ -260,25 +358,31 @@ def __init__( The Tecplot zone to write. datapacking : Literal["BLOCK", "POINT"] The data packing format. BLOCK is row-major, POINT is column-major. - precision : Literal["SINGLE", "DOUBLE"] + precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ self.zone = zone - self.datapacking = datapacking - self.nDigits = 6 if precision == "SINGLE" else 12 + self.datapacking = DataPacking[datapacking].name + self.fmtPrecision = DataPrecision[precision].value @abstractmethod def writeHeader(self, handle: TextIO): pass - @abstractmethod - def writeData(self, handle: TextIO): - pass - @abstractmethod def writeFooter(self, handle: TextIO): pass + def writeData(self, handle: TextIO): + data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) + + if self.datapacking == "POINT": + data = data.reshape(-1, len(self.zone.variables)) + else: + data = data.reshape(-1, len(self.zone.variables)).T + + np.savetxt(handle, data, fmt=f"%.{self.fmtPrecision}E") + class TecplotOrderedZoneWriterASCII(TecplotZoneWriterASCII[TecplotOrderedZone]): def __init__( @@ -318,34 +422,17 @@ def writeHeader(self, handle: TextIO): if self.zone.kMax > 1: zoneString += f", K={self.zone.kMax}" - if self.zone.strandID is not None: + # Write the strand ID and solution time + if self.zone.strandID != StrandID.STATIC and self.zone.strandID != StrandID.PENDING: + # ASCII format does not support the -1 or -2 strand IDs + # So we only write the strand ID if it is not -1 zoneString += f", STRANDID={self.zone.strandID}" - if self.zone.solutionTime is not None: - zoneString += f", SOLUTIONTIME={self.zone.solutionTime}" - + zoneString += f", SOLUTIONTIME={self.zone.solutionTime}" zoneString += f", DATAPACKING={self.datapacking}\n" handle.write(zoneString) - def writeData(self, handle: TextIO): - """Write the zone data to the file. - - Parameters - ---------- - handle : TextIO - The file handle. - """ - # Get the data into a single array - data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) - - if self.datapacking == "POINT": - # If the datapacking is POINT, then each variable is a column - np.savetxt(handle, data.reshape(-1, len(self.zone.variables)), fmt=f"%.{self.nDigits}E") - else: - # If the datapacking is BLOCK, then each variable is a row - np.savetxt(handle, data.reshape(-1, len(self.zone.variables)).T, fmt=f"%.{self.nDigits}E") - def writeFooter(self, handle: TextIO): """Write the zone footer to the file. @@ -376,33 +463,6 @@ def __init__( The floating point precision to write the data. """ super().__init__(zone, datapacking, precision) - self.zoneType = self._getZoneType() - - def _getZoneType(self) -> str: - """Get the Tecplot zone type based on the connectivity shape. - - Returns - ------- - str - The Tecplot zone type. - - Raises - ------ - ValueError - If the connectivity shape is invalid. - """ - if self.zone.connectivity.shape[1] == 2: - return ZoneType.FELINESEG.name - elif self.zone.connectivity.shape[1] == 3: - return ZoneType.FETRIANGLE.name - elif self.zone.connectivity.shape[1] == 4 and not self.zone.tetrahedral: - return ZoneType.FEQUADRILATERAL.name - elif self.zone.connectivity.shape[1] == 4 and self.zone.tetrahedral: - return ZoneType.FETETRAHEDRON.name - elif self.zone.connectivity.shape[1] == 8: - return ZoneType.FEBRICK.name - else: - raise ValueError("Invalid connectivity shape.") def writeHeader(self, handle: TextIO): """Write the zone header to the file. @@ -419,27 +479,17 @@ def writeHeader(self, handle: TextIO): # Write the node and element information zoneString += f", NODES={np.multiply.reduce(self.zone.shape):d}" zoneString += f", ELEMENTS={self.zone.nElements:d}" - zoneString += f", ZONETYPE={self.zoneType}\n" - - handle.write(zoneString) + zoneString += f", ZONETYPE={self.zone.zoneType.name}" - def writeData(self, handle: TextIO): - """Write the zone data to the file. + # Write the strand ID and solution time + if self.zone.strandID != StrandID.STATIC and self.zone.strandID != StrandID.PENDING: + # ASCII format does not support the -1 or -2 strand IDs + # So we only write the strand ID if it is not -1 + zoneString += f", STRANDID={self.zone.strandID}" - Parameters - ---------- - handle : TextIO - The file handle. - """ - # Get the data into a single array - data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) + zoneString += f", SOLUTIONTIME={self.zone.solutionTime}\n" - if self.datapacking == "POINT": - # If the datapacking is POINT, then each variable is a column - np.savetxt(handle, data.reshape(-1, len(self.zone.variables)), fmt=f"%.{self.nDigits}E") - else: - # If the datapacking is BLOCK, then each variable is a row - np.savetxt(handle, data.reshape(-1, len(self.zone.variables)).T, fmt=f"%.{self.nDigits}E") + handle.write(zoneString) def writeFooter(self, handle: TextIO): """Write the zone footer to the file. This includes the connectivity information. @@ -481,11 +531,11 @@ def __init__( """ self.title = title self.zones = zones - self.datapacking = datapacking - self.precision = precision - self._checkVariables() + self.datapacking = DataPacking[datapacking].name + self.precision = DataPrecision[precision].name + self._validateVariables() - def _checkVariables(self) -> None: + def _validateVariables(self) -> None: """Check that all zones have the same variables.""" if not all(set(self.zones[0].variables) == set(zone.variables) for zone in self.zones): raise ValueError("All zones must have the same variables.") @@ -623,41 +673,7 @@ def __init__( self.title = title self.zone = zone self.datapacking = "BLOCK" - self.precision = precision - self.zoneType = self._getZoneType() - - def _getZoneType(self) -> int: - """Get the Tecplot zone type based on the zone object. - - Returns - ------- - int - The Tecplot zone type. - - Raises - ------ - ValueError - If the zone type is invalid. - ValueError - If the connectivity shape is invalid. - """ - if isinstance(self.zone, TecplotOrderedZone): - return ZoneType.ORDERED.value - elif isinstance(self.zone, TecplotFEZone): - if self.zone.connectivity.shape[1] == 2: - return ZoneType.FELINESEG.value - elif self.zone.connectivity.shape[1] == 3: - return ZoneType.FETRIANGLE.value - elif self.zone.connectivity.shape[1] == 4 and not self.zone.tetrahedral: - return ZoneType.FEQUADRILATERAL.value - elif self.zone.connectivity.shape[1] == 4 and self.zone.tetrahedral: - return ZoneType.FETETRAHEDRON.value - elif self.zone.connectivity.shape[1] == 8: - return ZoneType.FEBRICK.value - else: - raise ValueError("Invalid connectivity shape.") - else: - raise ValueError("Invalid zone type.") + self.precision = DataPrecision[precision].name def _writeCommonHeader(self, handle: TextIO) -> None: """Write the common header information for all zones. @@ -671,16 +687,10 @@ def _writeCommonHeader(self, handle: TextIO) -> None: _writeFloat32(handle, SectionMarkers.ZONE.value) # Write the zone marker _writeString(handle, self.zone.name) # Write the zone name _writeInteger(handle, BinaryFlags.NONE.value) # Write the parent zone - if self.zone.strandID is not None: - _writeInteger(handle, self.zone.strandID) # Write the strand ID - else: - _writeInteger(handle, StrandID.STATIC.value) - if self.zone.solutionTime is not None: - _writeFloat64(handle, self.zone.solutionTime) # Write the solution time - else: - _writeFloat64(handle, 0.0) + _writeInteger(handle, self.zone.strandID) # Write the strand ID + _writeFloat64(handle, self.zone.solutionTime) # Write the solution time _writeInteger(handle, BinaryFlags.NONE.value) # Write the default color - _writeInteger(handle, self.zoneType) # Write the zone type + _writeInteger(handle, self.zone.zoneType.value) # Write the zone type _writeInteger(handle, DataPacking.BLOCK.value) # Data Packing (Always block for binary) _writeInteger(handle, VariableLocation.NODE.value) # Specify the variable location _writeInteger(handle, BinaryFlags.FALSE.value) # Are raw 1-1 face neighbors supplied @@ -707,7 +717,7 @@ def writeData(self, handle: TextIO): # Write the variable data format for each variable for _ in range(len(self.zone.variables)): - _writeInteger(handle, DataPrecision[self.precision].value) + _writeInteger(handle, BinaryDataPrecisionCodes[self.precision].value) _writeInteger(handle, BinaryFlags.FALSE.value) # No passive variables _writeInteger(handle, BinaryFlags.FALSE.value) # No variable sharing @@ -831,6 +841,12 @@ def __init__( ) -> None: """Writer for Tecplot files in binary format. + This writer only supports files formatted using the format + designated by magic number ``#!TDV112``. + + See the Tecplot 360 User's Manual for more information on the + binary file format and the magic number. + Parameters ---------- title : str @@ -840,6 +856,7 @@ def __init__( precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ + self._magicNumber = b"#!TDV112" self.title = title self.zones = zones self.precision = precision @@ -884,7 +901,7 @@ def write(self, filename: Union[str, Path]) -> None: The filename as a string or pathlib.Path object. """ with open(filename, "wb") as handle: - handle.write(b"#!TDV112") # Magic number + handle.write(self._magicNumber) # Magic number _writeInteger(handle, 1) # Byte order _writeInteger(handle, FileType.FULL.value) # Full filetype _writeString(handle, self.title) # Write the title @@ -1082,21 +1099,21 @@ def _readFEZoneData( zoneHeaderDict["zoneName"], data, connectivity - 1, + zoneType=zoneHeaderDict["zoneType"], solutionTime=zoneHeaderDict["solutionTime"], strandID=zoneHeaderDict["strandID"], - tetrahedral=zoneHeaderDict["zoneType"] == "FETETRAHEDRON", ) return zone, nodeOffset + nElements - def _readZoneData(self, lines: List[str], iCurrent: int, variables: List[str]) -> Tuple[TecplotZone, int]: + def _readZoneData(self, lines: List[str], iLine: int, variables: List[str]) -> Tuple[TecplotZone, int]: """Read the data for a Tecplot zone. Parameters ---------- lines : List[str] The list of lines in the Tecplot file. - iCurrent : int + iLine : int The current line number in the file. variables : List[str] The list of variable names. @@ -1106,14 +1123,14 @@ def _readZoneData(self, lines: List[str], iCurrent: int, variables: List[str]) - Tuple[TecplotZone, int] The Tecplot zone object and the number of lines read. """ - zoneHeaderDict, iCurrent = self._readZoneHeader(lines, iCurrent) + zoneHeaderDict, iLine = self._readZoneHeader(lines, iLine) if zoneHeaderDict["zoneType"] == "ORDERED": - zone, iOffset = self._readOrderedZoneData(iCurrent, variables, zoneHeaderDict) + zone, iOffset = self._readOrderedZoneData(iLine, variables, zoneHeaderDict) else: - zone, iOffset = self._readFEZoneData(iCurrent, variables, zoneHeaderDict) + zone, iOffset = self._readFEZoneData(iLine, variables, zoneHeaderDict) - return zone, iCurrent + iOffset + return zone, iLine + iOffset def read(self) -> Tuple[str, List[TecplotZone]]: """Read the Tecplot file and return the title and zones. @@ -1144,14 +1161,14 @@ def read(self) -> Tuple[str, List[TecplotZone]]: # Get the variable names variables = re.findall(r'"([^"]*)"', lines[1]) - iCurrent = 2 - while iCurrent < len(lines): - zone, iCurrent = self._readZoneData(lines, iCurrent, variables) + iLine = 2 + while iLine < len(lines): + zone, iLine = self._readZoneData(lines, iLine, variables) zones.append(zone) # Skip any empty lines - while iCurrent < len(lines) and not lines[iCurrent].strip(): - iCurrent += 1 + while iLine < len(lines) and not lines[iLine].strip(): + iLine += 1 return title, zones @@ -1212,9 +1229,9 @@ def _readInteger(self, handle: TextIO, offset: int = 0) -> int: int The integer read from the file. """ - return np.fromfile(handle, dtype=np.int32, count=1, offset=offset)[0] + return int(np.fromfile(handle, dtype=np.int32, count=1, offset=offset)[0]) - def _readIntegerArray(self, handle: TextIO, nValues: int, offset: int = 0) -> np.ndarray: + def _readIntegerArray(self, handle: TextIO, nValues: int, offset: int = 0) -> npt.NDArray[np.int32]: """Read an array of integers from a binary file as int32. Parameters @@ -1228,7 +1245,7 @@ def _readIntegerArray(self, handle: TextIO, nValues: int, offset: int = 0) -> np Returns ------- - np.ndarray + npt.NDArray[np.int32] The integer array read from the file. """ return np.fromfile(handle, dtype=np.int32, count=nValues, offset=offset) @@ -1248,9 +1265,9 @@ def _readFloat32(self, handle: TextIO, offset: int = 0) -> float: float The float read from the file. """ - return np.fromfile(handle, dtype=np.float32, count=1, offset=offset)[0] + return float(np.fromfile(handle, dtype=np.float32, count=1, offset=offset)[0]) - def _readFloat32Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np.ndarray: + def _readFloat32Array(self, handle: TextIO, nValues: int, offset: int = 0) -> npt.NDArray[np.float32]: """Read an array of floats from a binary file as float32. Parameters @@ -1264,7 +1281,7 @@ def _readFloat32Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np Returns ------- - np.ndarray + npt.NDArray[np.float32] The float array read from the file. """ return np.fromfile(handle, dtype=np.float32, count=nValues, offset=offset) @@ -1284,9 +1301,9 @@ def _readFloat64(self, handle: TextIO, offset: int = 0) -> float: float The float read from the file. """ - return np.fromfile(handle, dtype=np.float64, count=1, offset=offset)[0] + return float(np.fromfile(handle, dtype=np.float64, count=1, offset=offset)[0]) - def _readFloat64Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np.ndarray: + def _readFloat64Array(self, handle: TextIO, nValues: int, offset: int = 0) -> npt.NDArray[np.float64]: """Read an array of floats from a binary file as float64. Parameters @@ -1300,7 +1317,7 @@ def _readFloat64Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np Returns ------- - np.ndarray + npt.NDArray[np.float64] The float array read from the file. """ return np.fromfile(handle, dtype=np.float64, count=nValues, offset=offset) @@ -1386,7 +1403,7 @@ def _readFEZone( zoneName, {var: np.zeros(nNodes) for _, var in enumerate(self._variables)}, connectivity, - tetrahedral=zoneType == ZoneType.FETETRAHEDRON.value, + zoneType=ZoneType(zoneType), solutionTime=solutionTime, strandID=strandID, ) @@ -1477,7 +1494,7 @@ def read(self) -> Tuple[str, List[TecplotZone]]: kMax = zones[izone].kMax for i in range(self._nVariables): - if dataFormats[i] == DataPrecision.SINGLE.value: + if dataFormats[i] == BinaryDataPrecisionCodes.SINGLE.value: readData = self._readFloat32Array(file, iMax * jMax * kMax).reshape(iMax, jMax, kMax).squeeze() else: readData = self._readFloat64Array(file, iMax * jMax * kMax).reshape(iMax, jMax, kMax).squeeze() @@ -1489,7 +1506,7 @@ def read(self) -> Tuple[str, List[TecplotZone]]: nElements = zones[izone].nElements for i in range(self._nVariables): - if dataFormats[i] == DataPrecision.SINGLE.value: + if dataFormats[i] == BinaryDataPrecisionCodes.SINGLE.value: readData = self._readFloat32Array(file, nNodes) else: readData = self._readFloat64Array(file, nNodes) diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index caef168..8cb8384 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -1,3 +1,4 @@ +import itertools import tempfile import unittest from itertools import product @@ -6,31 +7,70 @@ import numpy as np import numpy.typing as npt - -from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, readTecplot, writeTecplot - - -def createBrickGrid(nx: int, ny: int, nz: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: +from parameterized import parameterized + +from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, ZoneType, readTecplot, writeTecplot + +# --- Define test matrices for Ordered and FE Zones --- +TEST_CASES_ORDERED = [ + tc + for tc in itertools.product( + [(10,), (10, 10), (10, 10, 10)], ["SINGLE", "DOUBLE"], ["POINT", "BLOCK"], [".dat", ".plt"] + ) +] +# filter out cases that use POINT datapacking with binary '.plt' extensions +TEST_CASES_ORDERED = [tc for tc in TEST_CASES_ORDERED if not (tc[2] == "POINT" and tc[3] == ".plt")] + +TEST_CASES_FE = [ + tc + for tc in itertools.product( + [ZoneType.FELINESEG, ZoneType.FETRIANGLE, ZoneType.FEQUADRILATERAL], + ["SINGLE", "DOUBLE"], + ["POINT", "BLOCK"], + [".dat", ".plt"], + ) +] +# filter out cases that use POINT datapacking with binary '.plt' extensions +TEST_CASES_FE = [tc for tc in TEST_CASES_FE if not (tc[2] == "POINT" and tc[3] == ".plt")] + + +def createBrickGrid(ni: int, nj: int, nk: int) -> Tuple[npt.NDArray, npt.NDArray]: + """Create a 3D grid of hexahedral 'brick' elements. + + Parameters + ---------- + ni : int + The number of elements in the i-direction. + nj : int + The number of elements in the j-direction. + nk : int + The number of elements in the k-direction. + + Returns + ------- + Tuple[npt.NDArray, npt.NDArray] + A tuple containing the node coordinates and element connectivity. + """ # Create node coordinates - x = np.linspace(0, 1, nx + 1) - y = np.linspace(0, 1, ny + 1) - z = np.linspace(0, 1, nz + 1) + x = np.linspace(0, 1, ni + 1) + y = np.linspace(0, 1, nj + 1) + z = np.linspace(0, 1, nk + 1) xx, yy, zz = np.meshgrid(x, y, z) nodes = np.column_stack((xx.flatten(), yy.flatten(), zz.flatten())) # Create element connectivity connectivity = [] - for k in range(nz): - for j in range(ny): - for i in range(nx): + for k in range(nk): + for j in range(nj): + for i in range(ni): # Get the eight corners of the hexahedron - n0 = i + j * (nx + 1) + k * (nx + 1) * (ny + 1) + n0 = i + j * (ni + 1) + k * (ni + 1) * (nj + 1) n1 = n0 + 1 - n2 = n1 + (nx + 1) + n2 = n1 + (ni + 1) n3 = n2 - 1 - n4 = n0 + (nx + 1) * (ny + 1) + n4 = n0 + (ni + 1) * (nj + 1) n5 = n4 + 1 - n6 = n5 + (nx + 1) + n6 = n5 + (ni + 1) n7 = n6 - 1 connectivity.append([n0, n1, n2, n3, n4, n5, n6, n7]) @@ -40,27 +80,43 @@ def createBrickGrid(nx: int, ny: int, nz: int) -> Tuple[npt.ArrayLike, npt.Array return nodes, connectivity -def createTetGrid(nx: int, ny: int, nz: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: +def createTetGrid(ni: int, nj: int, nk: int) -> Tuple[npt.NDArray, npt.NDArray]: + """Create a 3D grid of tetrahedral elements. + + Parameters + ---------- + ni : int + The number of elements in the i-direction. + nj : int + The number of elements in the j-direction. + nk : int + The number of elements in the k-direction. + + Returns + ------- + Tuple[npt.NDArray, npt.NDArray] + A tuple containing the node coordinates and element connectivity. + """ # Create node coordinates - x = np.linspace(0, 1, nx + 1) - y = np.linspace(0, 1, ny + 1) - z = np.linspace(0, 1, nz + 1) + x = np.linspace(0, 1, ni + 1) + y = np.linspace(0, 1, nj + 1) + z = np.linspace(0, 1, nk + 1) xx, yy, zz = np.meshgrid(x, y, z) nodes = np.column_stack((xx.flatten(), yy.flatten(), zz.flatten())) # Create element connectivity connectivity = [] - for k in range(nz): - for j in range(ny): - for i in range(nx): + for k in range(nk): + for j in range(nj): + for i in range(ni): # Get the eight corners of the hexahedron - n0 = i + j * (nx + 1) + k * (nx + 1) * (ny + 1) + n0 = i + j * (ni + 1) + k * (ni + 1) * (nj + 1) n1 = n0 + 1 - n2 = n1 + (nx + 1) + n2 = n1 + (ni + 1) n3 = n2 - 1 - n4 = n0 + (nx + 1) * (ny + 1) + n4 = n0 + (ni + 1) * (nj + 1) n5 = n4 + 1 - n6 = n5 + (nx + 1) + n6 = n5 + (ni + 1) n7 = n6 - 1 # Basic subdivision (always add these) @@ -78,15 +134,15 @@ def createTetGrid(nx: int, ny: int, nz: int) -> Tuple[npt.ArrayLike, npt.ArrayLi # Additional tetrahedra for boundary faces if i == 0: connectivity.append([n0, n3, n4, n7]) - if i == nx - 1: + if i == ni - 1: connectivity.append([n1, n2, n5, n6]) if j == 0: connectivity.append([n0, n1, n4, n5]) - if j == ny - 1: + if j == nj - 1: connectivity.append([n2, n3, n6, n7]) if k == 0: connectivity.append([n0, n1, n2, n3]) - if k == nz - 1: + if k == nk - 1: connectivity.append([n4, n5, n6, n7]) connectivity = np.array(connectivity) @@ -95,20 +151,34 @@ def createTetGrid(nx: int, ny: int, nz: int) -> Tuple[npt.ArrayLike, npt.ArrayLi # Create a finite element mesh with connectivity -def createQuadGrid(nx: int, ny: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: +def createQuadGrid(ni: int, nj: int) -> Tuple[npt.NDArray, npt.NDArray]: + """Create a 2D grid of quadrilateral elements. + + Parameters + ---------- + ni : int + The number of elements in the i-direction. + nj : int + The number of elements in the j-direction. + + Returns + ------- + Tuple[npt.NDArray, npt.NDArray] + A tuple containing the node coordinates and element connectivity. + """ # Create node coordinates - x = np.linspace(0, 1, nx + 1) - y = np.linspace(0, 1, ny + 1) + x = np.linspace(0, 1, ni + 1) + y = np.linspace(0, 1, nj + 1) xx, yy = np.meshgrid(x, y) - nodes = np.column_stack((xx.flatten(), yy.flatten())) + nodes = np.column_stack((xx.flatten(), yy.flatten(), np.zeros_like(xx.flatten()))) # Create element connectivity connectivity = [] - for j in range(ny): - for i in range(nx): - n1 = i + j * (nx + 1) + for j in range(nj): + for i in range(ni): + n1 = i + j * (ni + 1) n2 = n1 + 1 - n3 = n2 + nx + 1 + n3 = n2 + ni + 1 n4 = n3 - 1 connectivity.append([n1, n2, n3, n4]) @@ -117,24 +187,38 @@ def createQuadGrid(nx: int, ny: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: return nodes, connectivity -def createTriGrid(nx: int, ny: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: +def createTriGrid(ni: int, nj: int) -> Tuple[npt.NDArray, npt.NDArray]: + """Create a 2D grid of triangular elements. + + Parameters + ---------- + ni : int + The number of elements in the i-direction. + nj : int + The number of elements in the j-direction. + + Returns + ------- + Tuple[npt.NDArray, npt.NDArray] + A tuple containing the node coordinates and element connectivity. + """ # Create node coordinates - x = np.linspace(0, 1, nx + 1) - y = np.linspace(0, 1, ny + 1) + x = np.linspace(0, 1, ni + 1) + y = np.linspace(0, 1, nj + 1) xx, yy = np.meshgrid(x, y) - nodes = np.column_stack((xx.flatten(), yy.flatten())) + nodes = np.column_stack((xx.flatten(), yy.flatten(), np.zeros_like(xx.flatten()))) # Create element connectivity connectivity = [] - for j in range(ny): - for i in range(nx): - n1 = i + j * (nx + 1) + for j in range(nj): + for i in range(ni): + n1 = i + j * (ni + 1) n2 = n1 + 1 - n3 = n2 + nx + 1 + n3 = n2 + ni + 1 connectivity.append([n1, n2, n3]) - n1 = i + j * (nx + 1) - n2 = n1 + nx + 1 + n1 = i + j * (ni + 1) + n2 = n1 + ni + 1 n3 = n2 + 1 connectivity.append([n1, n2, n3]) @@ -143,13 +227,25 @@ def createTriGrid(nx: int, ny: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: return nodes, connectivity -def createLineSegGrid(nx: int) -> Tuple[npt.ArrayLike, npt.ArrayLike]: +def createLineSegGrid(ni: int) -> Tuple[npt.NDArray, npt.NDArray]: + """Create a 1D grid of line segments. + + Parameters + ---------- + ni : int + The number of elements in the i-direction. + + Returns + ------- + Tuple[npt.NDArray, npt.NDArray] + A tuple containing the node coordinates and element connectivity. + """ # Create node coordinates - x = np.linspace(0, 1, nx + 1) - nodes = np.column_stack((x, x**2)) + x = np.linspace(0, 1, ni + 1) + nodes = np.column_stack((x, x**2, np.zeros_like(x))) # Create element connectivity - connectivity = np.column_stack((np.arange(nx), np.arange(1, nx + 1))) + connectivity = np.column_stack((np.arange(ni), np.arange(1, ni + 1))) return nodes, connectivity @@ -158,8 +254,6 @@ class TestTecplotIO(unittest.TestCase): N_PROCS = 1 def setUp(self): - np.random.seed(123) - thisDir = Path(__file__).parent self.externalFileAscii = thisDir / "input" / "airfoil_000_slices.dat" self.externalFileBinary = thisDir / "input" / "airfoil_000_surf.plt" @@ -186,7 +280,6 @@ def assertFEZonesEqual(self, zones1: List[TecplotFEZone], zones2: List[TecplotFE self.assertEqual(zone1.nNodes, zone2.nNodes) self.assertEqual(zone1.solutionTime, zone2.solutionTime) self.assertEqual(zone1.strandID, zone2.strandID) - self.assertEqual(zone1.tetrahedral, zone2.tetrahedral) def test_orderedZone(self): # Create a 3D grid of shape (nx, ny, nz, 3) @@ -208,6 +301,36 @@ def test_orderedZone(self): self.assertEqual(zone.solutionTime, 0.0) self.assertEqual(zone.strandID, -1) + msg = "Zone name must be a string" + with self.assertRaises(TypeError, msg=msg): + TecplotOrderedZone(123, {"X": X, "Y": Y, "Z": Z}, solutionTime=0.0, strandID=-1) + + msg = "Solution time must be a float" + with self.assertRaises(TypeError): + TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}, solutionTime="1.0", strandID=-1) + + msg = "Solution time must be greater than or equal to zero" + with self.assertRaises(ValueError): + TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}, solutionTime=-1.0, strandID=1) + + msg = "Strand ID must be an integer" + with self.assertRaises(TypeError): + TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}, solutionTime=0.0, strandID="1") + + msg = "Data values must be numpy arrays." + with self.assertRaises(TypeError): + TecplotOrderedZone( + "Grid", {"X": X.tolist(), "Y": Y.tolist(), "Z": Z.tolist()}, solutionTime=0.0, strandID=-1 + ) + + msg = "Data must be a dictionary." + with self.assertRaises(TypeError): + TecplotOrderedZone("Grid", X, solutionTime=0.0, strandID=-1) + + msg = "All variables must have the same shape." + with self.assertRaises(ValueError): + TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z[:-1]}, solutionTime=0.0, strandID=-1) + def test_FEZone(self): # Create a quad grid nx, ny = 10, 10 @@ -217,7 +340,12 @@ def test_FEZone(self): strandID = 4 # Create a Tecplot zone with Quad elements zone = TecplotFEZone( - "QuadGrid", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity, solutionTime=solutionTime, strandID=strandID + "QuadGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1]}, + connectivity, + zoneType=ZoneType.FEQUADRILATERAL, + solutionTime=solutionTime, + strandID=strandID, ) self.assertEqual(zone.name, "QuadGrid") @@ -228,6 +356,7 @@ def test_FEZone(self): self.assertEqual(zone.nNodes, (nx + 1) * (ny + 1)) self.assertEqual(zone.solutionTime, solutionTime) self.assertEqual(zone.strandID, strandID) + self.assertEqual(zone.zoneType, ZoneType.FEQUADRILATERAL) nx, ny, nz = 10, 10, 10 nodes, connectivity = createTetGrid(nx, ny, nz) @@ -237,9 +366,9 @@ def test_FEZone(self): "TetGrid", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity, + zoneType=ZoneType.FETETRAHEDRON, solutionTime=solutionTime, strandID=strandID, - tetrahedral=True, ) self.assertEqual(zone.name, "TetGrid") @@ -250,417 +379,131 @@ def test_FEZone(self): self.assertEqual(zone.nNodes, (nx + 1) * (ny + 1) * (nz + 1)) self.assertEqual(zone.solutionTime, solutionTime) self.assertEqual(zone.strandID, strandID) + self.assertEqual(zone.zoneType, ZoneType.FETETRAHEDRON) + + msg = "Zone name must be a string" + with self.assertRaises(TypeError, msg=msg): + TecplotFEZone( + 123, + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + zoneType=ZoneType.FETETRAHEDRON, + ) - def test_ASCIIReadWriteOrderedZones(self): - zones: List[TecplotOrderedZone] = [] - - X = np.random.rand(10) - Y = np.random.rand(10) - Z = np.random.rand(10) - zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) - zones.append(zone) - - X = np.random.rand(10, 10) - Y = np.random.rand(10, 10) - Z = np.random.rand(10, 10) - zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) - zones.append(zone) - - X = np.random.rand(10, 10) - Y = np.random.rand(10, 10) - Z = np.random.rand(10, 10) - zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) - zones.append(zone) - - X = np.random.rand(10, 10, 10) - Y = np.random.rand(10, 10, 10) - Z = np.random.rand(10, 10, 10) - zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) - zones.append(zone) - - title = "ASCII ORDERED ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertOrderedZonesEqual(zones, zonesRead) + msg = "Solution time must be a float" + with self.assertRaises(TypeError): + TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + zoneType=ZoneType.FETETRAHEDRON, + solutionTime="1.0", + ) - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLEs") - titleRead, zonesRead = readTecplot(tmpfile.name) + msg = "Solution time must be greater than or equal to zero" + with self.assertRaises(ValueError): + TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + zoneType=ZoneType.FETETRAHEDRON, + solutionTime=-1.0, + ) - self.assertEqual(titleRead, title) - self.assertOrderedZonesEqual(zones, zonesRead) + msg = "Strand ID must be an integer" + with self.assertRaises(TypeError): + TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + zoneType=ZoneType.FETETRAHEDRON, + strandID="1", + ) - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) + msg = "Data values must be numpy arrays." + with self.assertRaises(TypeError): + TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0].tolist(), "Y": nodes[:, 1].tolist(), "Z": nodes[:, 2].tolist()}, + connectivity, + zoneType=ZoneType.FETETRAHEDRON, + ) - self.assertEqual(titleRead, title) - self.assertOrderedZonesEqual(zones, zonesRead) + msg = "Data must be a dictionary." + with self.assertRaises(TypeError): + TecplotFEZone("TetGrid", nodes, connectivity, zoneType=ZoneType.FETETRAHEDRON) + + msg = "All variables must have the same shape." + with self.assertRaises(ValueError): + TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:-1, 2]}, + connectivity, + zoneType=ZoneType.FETETRAHEDRON, + ) - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) + msg = "Connectivity shape does not match zone type." + with self.assertRaises(AssertionError): + TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + zoneType=ZoneType.FETRIANGLE, + ) - def test_BinaryReadWriteOrderedZones(self): + @parameterized.expand( + TEST_CASES_ORDERED, + name_func=lambda f, n, p: parameterized.to_safe_name(f"{f.__name__}_{p[0]}"), + ) + def test_ReadWriteOrderedZones(self, shape: Tuple[int, ...], precision: str, datapacking: str, ext: str): zones: List[TecplotOrderedZone] = [] - X = np.random.rand(10) - Y = np.random.rand(10) - Z = np.random.rand(10) - zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) - zones.append(zone) - - X = np.random.rand(10, 10) - Y = np.random.rand(10, 10) - Z = np.random.rand(10, 10) - zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) - zones.append(zone) - - X = np.random.rand(10, 10) - Y = np.random.rand(10, 10) - Z = np.random.rand(10, 10) - zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) - zones.append(zone) + rng = np.random.default_rng(123) + X = rng.random(shape) + Y = rng.random(shape) + Z = rng.random(shape) - X = np.random.rand(10, 10, 10) - Y = np.random.rand(10, 10, 10) - Z = np.random.rand(10, 10, 10) zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) zones.append(zone) - title = "BINARY ORDERED ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertOrderedZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + title = "ASCII ORDERED ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=ext, delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) titleRead, zonesRead = readTecplot(tmpfile.name) self.assertEqual(titleRead, title) self.assertOrderedZonesEqual(zones, zonesRead) - def test_ASCIIReadWriteFELineSegZones(self): - zones: List[TecplotFEZone] = [] - - for nx in range(2, 10): - nodes, connectivity = createLineSegGrid(nx) - zone = TecplotFEZone(f"LineSegGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) - zones.append(zone) - - title = "ASCII FELINESEG ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_BinaryReadWriteFELineSegZones(self): - zones: List[TecplotFEZone] = [] - - for nx in range(2, 10): - nodes, connectivity = createLineSegGrid(nx) - zone = TecplotFEZone(f"LineSegGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) - zones.append(zone) - - title = "BINARY FELINESEG ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_ASCIIReadWriteFETriZones(self): + @parameterized.expand( + TEST_CASES_FE, + name_func=lambda f, n, p: parameterized.to_safe_name(f"{f.__name__}_{p[0]}"), + ) + def test_ReadWriteFEZones(self, zoneType: ZoneType, precision: str, datapacking: str, ext: str): zones: List[TecplotFEZone] = [] - dims = product(range(2, 10), range(2, 10)) - - for nx, ny in dims: - nodes, connectivity = createTriGrid(nx, ny) - zone = TecplotFEZone(f"TriGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) - zones.append(zone) - - title = "ASCII FETRIANGLE ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - def test_BinaryReadWriteFETriZones(self): - zones: List[TecplotFEZone] = [] - dims = product(range(2, 10), range(2, 10)) - - for nx, ny in dims: - nodes, connectivity = createTriGrid(nx, ny) - zone = TecplotFEZone(f"TriGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) - zones.append(zone) - - title = "BINARY FETRIANGLE ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_ASCIIReadWriteFEQuadZones(self): - zones: List[TecplotFEZone] = [] - dims = product(range(2, 10), range(2, 10)) - - for nx, ny in dims: - nodes, connectivity = createQuadGrid(nx, ny) - zone = TecplotFEZone(f"QuadGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) - zones.append(zone) - - title = "ASCII FEQUADRILATERAL ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_BinaryReadWriteFEQuadZones(self): - zones: List[TecplotFEZone] = [] - dims = product(range(2, 10), range(2, 10)) - - for nx, ny in dims: - nodes, connectivity = createQuadGrid(nx, ny) - zone = TecplotFEZone(f"QuadGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity) - zones.append(zone) - - title = "BINARY FEQUADRILATERAL ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_ASCIIReadWriteFETetZones(self): - zones: List[TecplotFEZone] = [] - npts = 5 - dims = product(range(2, npts), range(2, npts), range(2, npts)) - - for nx, ny, nz in dims: - nodes, connectivity = createTetGrid(nx, ny, nz) + if zoneType == ZoneType.FELINESEG: + rawDataList = [createLineSegGrid(nx) for nx in range(2, 10)] + elif zoneType == ZoneType.FETRIANGLE: + rawDataList = [createTriGrid(nx, ny) for nx, ny in product(range(2, 10), range(2, 10))] + elif zoneType == ZoneType.FEQUADRILATERAL: + rawDataList = [createQuadGrid(nx, ny) for nx, ny in product(range(2, 10), range(2, 10))] + elif zoneType == ZoneType.FETETRAHEDRON: + rawDataList = [createTetGrid(nx, ny, nz) for nx, ny, nz in product(range(2, 5), range(2, 5), range(2, 5))] + elif zoneType == ZoneType.FEBRICK: + rawDataList = [createBrickGrid(nx, ny, nz) for nx, ny, nz in product(range(2, 5), range(2, 5), range(2, 5))] + + for i, (nodes, connectivity) in enumerate(rawDataList): zone = TecplotFEZone( - f"TetGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity, tetrahedral=True + f"Grid_{i}", + {"X": nodes[..., 0], "Y": nodes[..., 1], "Z": nodes[..., 2]}, + connectivity, + zoneType=zoneType, ) zones.append(zone) - title = "ASCII FETETRAHEDRAL ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_BinaryReadWriteFETetZones(self): - zones: List[TecplotFEZone] = [] - npts = 5 - dims = product(range(2, npts), range(2, npts), range(2, npts)) - - for nx, ny, nz in dims: - nodes, connectivity = createTetGrid(nx, ny, nz) - zone = TecplotFEZone( - f"TetGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity, tetrahedral=True - ) - zones.append(zone) - - title = "BINARY FETETRAHEDRAL ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_ASCIIReadWriteFEBrickZones(self): - zones: List[TecplotFEZone] = [] - npts = 5 - dims = product(range(2, npts), range(2, npts), range(2, npts)) - - for nx, ny, nz in dims: - nodes, connectivity = createBrickGrid(nx, ny, nz) - zone = TecplotFEZone( - f"BrickGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity - ) - zones.append(zone) - - title = "ASCII FEBRICK ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="POINT", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".dat", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking="BLOCK", precision="DOUBLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - def test_BinaryReadWriteFEBrickZones(self): - zones: List[TecplotFEZone] = [] - npts = 5 - dims = product(range(2, npts), range(2, npts), range(2, npts)) - - for nx, ny, nz in dims: - nodes, connectivity = createBrickGrid(nx, ny, nz) - zone = TecplotFEZone( - f"BrickGrid_{nx}", {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, connectivity - ) - zones.append(zone) - - title = "BINARY FEBRICK ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="SINGLE") - titleRead, zonesRead = readTecplot(tmpfile.name) - - self.assertEqual(titleRead, title) - self.assertFEZonesEqual(zones, zonesRead) - - with tempfile.NamedTemporaryFile(suffix=".plt", delete=True) as tmpfile: - writeTecplot(tmpfile.name, title, zones, precision="DOUBLE") + title = f"ASCII {zoneType.name} ZONES TEST" + with tempfile.NamedTemporaryFile(suffix=ext, delete=True) as tmpfile: + writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) titleRead, zonesRead = readTecplot(tmpfile.name) self.assertEqual(titleRead, title) From 9ebce66522ae633132c46611bfe4251f90409f09 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sun, 4 Aug 2024 18:44:13 -0400 Subject: [PATCH 07/28] Fixing regex for zone name matching --- baseclasses/utils/tecplotIO.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 1fa3535..2422636 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -965,7 +965,7 @@ def _readZoneHeader(self, lines: List[str], iCurrent: int) -> Tuple[Dict[str, An headerString = ", ".join(header) # Use regex to parse the header information - zoneNameMatch = re.search(r'zone t\s*=\s*"(.+)"', headerString, re.IGNORECASE) + zoneNameMatch = re.search(r'zone t\s*=\s*"*(.+)"*', headerString, re.IGNORECASE) zoneName = zoneNameMatch.group(1) if zoneNameMatch else None zoneTypeMatch = re.search(r"zonetype\s*=\s*(\w+)", headerString, re.IGNORECASE) From 0288bee648d4165ef3a78a6cce79233c0817f28a Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sun, 4 Aug 2024 18:57:21 -0400 Subject: [PATCH 08/28] Added terminating comma to regex pattern for zone header --- baseclasses/utils/tecplotIO.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 2422636..9813ae3 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -965,7 +965,7 @@ def _readZoneHeader(self, lines: List[str], iCurrent: int) -> Tuple[Dict[str, An headerString = ", ".join(header) # Use regex to parse the header information - zoneNameMatch = re.search(r'zone t\s*=\s*"*(.+)"*', headerString, re.IGNORECASE) + zoneNameMatch = re.search(r'zone t\s*=\s*"*(.+)"*,', headerString, re.IGNORECASE) zoneName = zoneNameMatch.group(1) if zoneNameMatch else None zoneTypeMatch = re.search(r"zonetype\s*=\s*(\w+)", headerString, re.IGNORECASE) From d8fdc99d7fab8695de0da51427f9090e55af83b1 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 13 Sep 2024 13:26:47 -0400 Subject: [PATCH 09/28] Fixed regex for zone name matching --- baseclasses/utils/tecplotIO.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 9813ae3..9597425 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -965,7 +965,7 @@ def _readZoneHeader(self, lines: List[str], iCurrent: int) -> Tuple[Dict[str, An headerString = ", ".join(header) # Use regex to parse the header information - zoneNameMatch = re.search(r'zone t\s*=\s*"*(.+)"*,', headerString, re.IGNORECASE) + zoneNameMatch = re.search(r'zone t\s*=\s*"([^"]*)"', headerString, re.IGNORECASE) zoneName = zoneNameMatch.group(1) if zoneNameMatch else None zoneTypeMatch = re.search(r"zonetype\s*=\s*(\w+)", headerString, re.IGNORECASE) From 660609b7af73d3b6ce0293238f62ac02768d9232 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 13 Sep 2024 13:58:31 -0400 Subject: [PATCH 10/28] Updating tecplot writers in the weight problem --- baseclasses/problems/pyWeight_problem.py | 102 ++++++++++++----------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/baseclasses/problems/pyWeight_problem.py b/baseclasses/problems/pyWeight_problem.py index 4c84392..6592c35 100644 --- a/baseclasses/problems/pyWeight_problem.py +++ b/baseclasses/problems/pyWeight_problem.py @@ -4,8 +4,12 @@ Holds the weightProblem class for weightandbalance solvers. """ -import numpy as np import copy +from pathlib import Path + +import numpy as np + +from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, ZoneType, writeTecplot try: from pygeo import geo_utils @@ -194,24 +198,28 @@ def writeSurfaceTecplot(self, fileName): File name for tecplot file. Should have a .dat extension. """ - f = open(fileName, "w") - f.write('TITLE = "weight_problem Surface Mesh"\n') - f.write('VARIABLES = "CoordinateX" "CoordinateY" "CoordinateZ"\n') - f.write("Zone T=%s\n" % ("surf")) - f.write("Nodes = %d, Elements = %d ZONETYPE=FETRIANGLE\n" % (len(self.p0) * 3, len(self.p0))) - f.write("DATAPACKING=POINT\n") - for i in range(len(self.p0)): - points = [] - points.append(self.p0[i]) - points.append(self.p0[i] + self.v1[i]) - points.append(self.p0[i] + self.v2[i]) - for i in range(len(points)): - f.write(f"{points[i][0]:f} {points[i][1]:f} {points[i][2]:f}\n") + # Build the FETriangle data array + dataArrays = np.zeros((len(self.p0), 3), dtype=float) + dataArrays[:, 0] = self.p0 + dataArrays[:, 1] = self.p0 + self.v1 + dataArrays[:, 2] = self.p0 + self.v2 + data = {"CoordinateX": dataArrays[:, 0], "CoordinateY": dataArrays[:, 1], "CoordinateZ": dataArrays[:, 2]} + # Create the connectivity + conn = np.zeros((len(self.p0), 3), dtype=int) for i in range(len(self.p0)): - f.write("%d %d %d\n" % (3 * i + 1, 3 * i + 2, 3 * i + 3)) + conn[i, :] = [3 * i + 1, 3 * i + 2, 3 * i + 3] - f.close() + # Create the single zone + zones = [TecplotFEZone("surf", data, conn, zoneType=ZoneType.FETRIANGLE)] + + writeTecplot( + fileName, + title="weight_problem Surface Mesh", + zones=zones, + datapacking="POINT", + precision="SINGLE", + ) def writeTecplot(self, fileName): """ @@ -490,48 +498,42 @@ def writeMassesTecplot(self, filename): filename: str filename for writing the masses. This string will have the - .dat suffix appended to it. + .dat suffix appended to it if it does not already have it. """ - fileHandle = filename + ".dat" - f = open(fileHandle, "w") nMasses = len(self.nameList) - f.write('TITLE = "%s: Mass Data"\n' % self.name) - f.write('VARIABLES = "X", "Y", "Z", "Mass"\n') locList = ["current", "fwd", "aft"] + zones = [] for loc in locList: - f.write('ZONE T="%s", I=%d, J=1, K=1, DATAPACKING=POINT\n' % (loc, nMasses)) - - for key in self.components.keys(): + dataArray = np.zeros((nMasses, 4), dtype=float) + for i, key in enumerate(self.components.keys()): CG = self.components[key].getCG(loc) mass = self.components[key].getMass() - x = np.real(CG[0]) - y = np.real(CG[1]) - z = np.real(CG[2]) - m = np.real(mass) - - f.write(f"{x:f} {y:f} {z:f} {m:f}\n") - - # end - f.write("\n") - # end - - # textOffset = 0.5 - # for loc in locList: - # for name in self.nameList: - # x= np.real(self.componentDict[name].CG[loc][0]) - # y= np.real(self.componentDict[name].CG[loc][1]) - # z= np.real(self.componentDict[name].CG[loc][2])+textOffset - # m= np.real(self.componentDict[name].W) - - # f.write('TEXT CS=GRID3D, HU=POINT, X=%f, Y=%f, Z=%f, H=12, T="%s"\n'%(x,y,z,name+' '+loc)) - # # end - - # # end - - f.close() - return + dataArray[i, 0] = CG[0] + dataArray[i, 1] = CG[1] + dataArray[i, 2] = CG[2] + dataArray[i, 3] = mass + + data = { + "CoordinateX": dataArray[:, 0], + "CoordinateY": dataArray[:, 1], + "CoordinateZ": dataArray[:, 2], + "Mass": dataArray[:, 3], + } + + zones.append(TecplotOrderedZone(loc, data)) + + # Create the path with the .dat extension + filePath = Path(filename).with_suffix(".dat") + + writeTecplot( + filePath, + title=f"{self.name}: Mass Data", + zones=zones, + datapacking="POINT", + precision="SINGLE", + ) def writeProblemData(self, fileName): """ From f88c9039f0b952d8f883210af0d08a0c2305c60c Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 13 Sep 2024 14:36:28 -0400 Subject: [PATCH 11/28] Fixing if checks for types --- baseclasses/problems/pyWeight_problem.py | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/baseclasses/problems/pyWeight_problem.py b/baseclasses/problems/pyWeight_problem.py index 6592c35..2006642 100644 --- a/baseclasses/problems/pyWeight_problem.py +++ b/baseclasses/problems/pyWeight_problem.py @@ -72,9 +72,9 @@ def addComponents(self, components): # *components? """ # Check if components is of type Component or list, otherwise raise Error - if type(components) == list: + if isinstance(components, list): pass - elif type(components) == object: + elif isinstance(components, object): components = [components] else: raise Error("addComponents() takes in either a list of or a single component") @@ -136,7 +136,7 @@ def setSurface(self, surf): """ - if type(surf) == list: + if isinstance(surf, list): self.p0 = np.array(surf[0]) self.v1 = np.array(surf[1]) self.v2 = np.array(surf[2]) @@ -199,10 +199,10 @@ def writeSurfaceTecplot(self, fileName): """ # Build the FETriangle data array - dataArrays = np.zeros((len(self.p0), 3), dtype=float) - dataArrays[:, 0] = self.p0 - dataArrays[:, 1] = self.p0 + self.v1 - dataArrays[:, 2] = self.p0 + self.v2 + dataArrays = np.zeros((len(self.p0) * 3, 3), dtype=float) + dataArrays[::3] = self.p0 + dataArrays[1::3] = self.p0 + self.v1 + dataArrays[2::3] = self.p0 + self.v2 data = {"CoordinateX": dataArrays[:, 0], "CoordinateY": dataArrays[:, 1], "CoordinateZ": dataArrays[:, 2]} # Create the connectivity @@ -370,9 +370,9 @@ def addFuelCases(self, cases): """ # Check if case is a single entry or a list, otherwise raise Error - if type(cases) == list: + if isinstance(cases, list): pass - elif type(cases) == object: + elif isinstance(cases, object): cases = [cases] else: raise Error("addFuelCases() takes in either a list of or a single fuelcase") @@ -455,7 +455,7 @@ def _getComponentKeys(self, include=None, exclude=None, includeType=None, exclud if includeType is not None: # Specified a list of component types to include - if type(includeType) == str: + if isinstance(includeType, str): includeType = [includeType] weightKeysTmp = set() for key in weightKeys: @@ -465,21 +465,21 @@ def _getComponentKeys(self, include=None, exclude=None, includeType=None, exclud if include is not None: # Specified a list of compoents to include - if type(include) == str: + if isinstance(include, str): include = [include] include = set(include) weightKeys.intersection_update(include) if exclude is not None: # Specified a list of components to exclude - if type(exclude) == str: + if isinstance(exclude, str): exclude = [exclude] exclude = set(exclude) weightKeys.difference_update(exclude) if excludeType is not None: # Specified a list of compoent types to exclude - if type(excludeType) == str: + if isinstance(excludeType, str): excludeType = [excludeType] weightKeysTmp = copy.copy(weightKeys) for key in weightKeys: @@ -498,7 +498,7 @@ def writeMassesTecplot(self, filename): filename: str filename for writing the masses. This string will have the - .dat suffix appended to it if it does not already have it. + # .dat suffix appended to it if it does not already have it. """ nMasses = len(self.nameList) From 47903db801a9dfe2806693912ff88bba02cf33c9 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 13 Sep 2024 14:37:03 -0400 Subject: [PATCH 12/28] Updating tecplot writers in the aero solver --- baseclasses/solvers/pyAero_solver.py | 35 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/baseclasses/solvers/pyAero_solver.py b/baseclasses/solvers/pyAero_solver.py index 27c3ae6..a112a11 100644 --- a/baseclasses/solvers/pyAero_solver.py +++ b/baseclasses/solvers/pyAero_solver.py @@ -9,11 +9,12 @@ # ============================================================================= import numpy as np +from ..utils import CaseInsensitiveDict, Error, TecplotFEZone, ZoneType, writeTecplot + # ============================================================================= # Extension modules # ============================================================================= from .BaseSolver import BaseSolver -from ..utils import CaseInsensitiveDict, Error # ============================================================================= # AeroSolver Class @@ -21,7 +22,6 @@ class AeroSolver(BaseSolver): - """ Abstract Class for Aerodynamic Solver Object """ @@ -207,24 +207,25 @@ def writeTriangulatedSurfaceTecplot(self, fileName, groupName=None, **kwargs): """ [p0, v1, v2] = self.getTriangulatedMeshSurface(groupName, **kwargs) if self.comm.rank == 0: - f = open(fileName, "w") - f.write('TITLE = "%s Surface Mesh"\n' % self.name) - f.write('VARIABLES = "CoordinateX" "CoordinateY" "CoordinateZ"\n') - f.write("Zone T=%s\n" % ("surf")) - f.write("Nodes = %d, Elements = %d ZONETYPE=FETRIANGLE\n" % (len(p0) * 3, len(p0))) - f.write("DATAPACKING=POINT\n") - for i in range(len(p0)): - points = [] - points.append(p0[i]) - points.append(p0[i] + v1[i]) - points.append(p0[i] + v2[i]) - for i in range(len(points)): - f.write(f"{points[i][0]:f} {points[i][1]:f} {points[i][2]:f}\n") + dataArray = np.zeros((len(p0) * 3, 3), dtype=float) + dataArray[::3] = p0 + dataArray[1::3] = p0 + v1 + dataArray[2::3] = p0 + v2 + data = {"CoordinateX": dataArray[:, 0], "CoordinateY": dataArray[:, 1], "CoordinateZ": dataArray[:, 2]} + conn = np.zeros((len(p0), 3), dtype=int) for i in range(len(p0)): - f.write("%d %d %d\n" % (3 * i + 1, 3 * i + 2, 3 * i + 3)) + conn[i, :] = [3 * i + 1, 3 * i + 2, 3 * i + 3] - f.close() + zones = [TecplotFEZone("surf", data, conn, zoneType=ZoneType.FETRIANGLE)] + + writeTecplot( + fileName, + title=f"{self.name} Surface Mesh", + zones=zones, + datapacking="POINT", + precision="SINGLE", + ) def checkSolutionFailure(self, aeroProblem, funcs): """Take in a an aeroProblem and check for failure. Then append the From 75f59eaa4a3e6180aeea429419a8b1600d71bf4a Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 20 Sep 2024 18:47:30 -0400 Subject: [PATCH 13/28] Fixing strand ID and solution time bugs in ASCII writer --- baseclasses/utils/tecplotIO.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 9597425..23e1da5 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -72,6 +72,10 @@ class StrandID(Enum): STATIC = -1 +class SolutionTime(Enum): + UNSET = -1 + + # ============================================================================== # DATA STRUCTURES # ============================================================================== @@ -423,12 +427,15 @@ def writeHeader(self, handle: TextIO): zoneString += f", K={self.zone.kMax}" # Write the strand ID and solution time - if self.zone.strandID != StrandID.STATIC and self.zone.strandID != StrandID.PENDING: + if self.zone.strandID != StrandID.STATIC.value and self.zone.strandID != StrandID.PENDING.value: # ASCII format does not support the -1 or -2 strand IDs # So we only write the strand ID if it is not -1 zoneString += f", STRANDID={self.zone.strandID}" - zoneString += f", SOLUTIONTIME={self.zone.solutionTime}" + # Only write the solution time if it is set + if self.zone.solutionTime != SolutionTime.UNSET.value: + zoneString += f", SOLUTIONTIME={self.zone.solutionTime}" + zoneString += f", DATAPACKING={self.datapacking}\n" handle.write(zoneString) @@ -482,12 +489,14 @@ def writeHeader(self, handle: TextIO): zoneString += f", ZONETYPE={self.zone.zoneType.name}" # Write the strand ID and solution time - if self.zone.strandID != StrandID.STATIC and self.zone.strandID != StrandID.PENDING: + if self.zone.strandID != StrandID.STATIC.value and self.zone.strandID != StrandID.PENDING.value: # ASCII format does not support the -1 or -2 strand IDs # So we only write the strand ID if it is not -1 zoneString += f", STRANDID={self.zone.strandID}" - zoneString += f", SOLUTIONTIME={self.zone.solutionTime}\n" + # Only write the solution time if it is set + if self.zone.solutionTime != SolutionTime.UNSET.value: + zoneString += f", SOLUTIONTIME={self.zone.solutionTime}\n" handle.write(zoneString) @@ -1094,6 +1103,9 @@ def _readFEZoneData( if nodalData.ndim == 1: nodalData = nodalData.reshape(-1, len(variables)) + if connectivity.ndim == 1: + connectivity = connectivity.reshape(nElements, -1) + data = {var: nodalData[..., i] for i, var in enumerate(variables)} zone = TecplotFEZone( zoneHeaderDict["zoneName"], From 80196c6f649afdad653129a07576abc897815c69 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 20 Sep 2024 18:48:14 -0400 Subject: [PATCH 14/28] Fixed random data in ordered zones and added stress testing --- tests/test_tecplotIO.py | 91 +++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index 8cb8384..95c925a 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -11,11 +11,20 @@ from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, ZoneType, readTecplot, writeTecplot +# --- Save tempfile locally or in a temp directory --- +SAVE_TEMPFILES = True +SAVE_DIR = Path(__file__).parent / "tmp_tecplot" if SAVE_TEMPFILES else None +if SAVE_DIR is not None: + SAVE_DIR.mkdir(exist_ok=True) + # --- Define test matrices for Ordered and FE Zones --- TEST_CASES_ORDERED = [ tc for tc in itertools.product( - [(10,), (10, 10), (10, 10, 10)], ["SINGLE", "DOUBLE"], ["POINT", "BLOCK"], [".dat", ".plt"] + [(10,), (100,), (10, 10), (100, 100), (10, 10, 10), (100, 100, 100)], + ["SINGLE", "DOUBLE"], + ["POINT", "BLOCK"], + [".dat", ".plt"], ) ] # filter out cases that use POINT datapacking with binary '.plt' extensions @@ -24,7 +33,7 @@ TEST_CASES_FE = [ tc for tc in itertools.product( - [ZoneType.FELINESEG, ZoneType.FETRIANGLE, ZoneType.FEQUADRILATERAL], + [ZoneType.FELINESEG, ZoneType.FETRIANGLE, ZoneType.FEQUADRILATERAL, ZoneType.FETETRAHEDRON, ZoneType.FEBRICK], ["SINGLE", "DOUBLE"], ["POINT", "BLOCK"], [".dat", ".plt"], @@ -51,33 +60,31 @@ def createBrickGrid(ni: int, nj: int, nk: int) -> Tuple[npt.NDArray, npt.NDArray Tuple[npt.NDArray, npt.NDArray] A tuple containing the node coordinates and element connectivity. """ - # Create node coordinates - x = np.linspace(0, 1, ni + 1) - y = np.linspace(0, 1, nj + 1) - z = np.linspace(0, 1, nk + 1) - xx, yy, zz = np.meshgrid(x, y, z) - nodes = np.column_stack((xx.flatten(), yy.flatten(), zz.flatten())) + x = np.linspace(0, 1, ni) + y = np.linspace(0, 1, nj) + z = np.linspace(0, 1, nk) + x, y, z = np.meshgrid(x, y, z) + nodal_data = np.column_stack((x.flatten(), y.flatten(), z.flatten())) - # Create element connectivity connectivity = [] - for k in range(nk): - for j in range(nj): - for i in range(ni): - # Get the eight corners of the hexahedron - n0 = i + j * (ni + 1) + k * (ni + 1) * (nj + 1) - n1 = n0 + 1 - n2 = n1 + (ni + 1) - n3 = n2 - 1 - n4 = n0 + (ni + 1) * (nj + 1) - n5 = n4 + 1 - n6 = n5 + (ni + 1) - n7 = n6 - 1 - - connectivity.append([n0, n1, n2, n3, n4, n5, n6, n7]) - - connectivity = np.array(connectivity) + for i in range(nk - 1): + for j in range(nj - 1): + for k in range(ni - 1): + index = i * (ni * nj) + j * ni + k + connectivity.append( + [ + index, + index + 1, + index + ni + 1, + index + ni, + index + (ni * nj), + index + (ni * nj) + 1, + index + (ni * nj) + ni + 1, + index + (ni * nj) + ni, + ] + ) - return nodes, connectivity + return nodal_data, np.array(connectivity) def createTetGrid(ni: int, nj: int, nk: int) -> Tuple[npt.NDArray, npt.NDArray]: @@ -458,16 +465,29 @@ def test_FEZone(self): def test_ReadWriteOrderedZones(self, shape: Tuple[int, ...], precision: str, datapacking: str, ext: str): zones: List[TecplotOrderedZone] = [] - rng = np.random.default_rng(123) - X = rng.random(shape) - Y = rng.random(shape) - Z = rng.random(shape) + if len(shape) == 1: + X = np.linspace(0, 1, shape[0]) + Y = np.zeros_like(X) + Z = np.zeros_like(X) + elif len(shape) == 2: + x = np.linspace(0, 1, shape[0]) + y = np.linspace(0, 1, shape[1]) + + X, Y = np.meshgrid(x, y) + Z = np.zeros_like(X) + elif len(shape) == 3: + x = np.linspace(0, 1, shape[0]) + y = np.linspace(0, 1, shape[1]) + z = np.linspace(0, 1, shape[2]) + + X, Y, Z = np.meshgrid(x, y, z, indexing="ij") zone = TecplotOrderedZone("Grid", {"X": X, "Y": Y, "Z": Z}) zones.append(zone) title = "ASCII ORDERED ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=ext, delete=True) as tmpfile: + prefix = f"ORDERED_{shape}_{precision}_{datapacking}_" + with tempfile.NamedTemporaryFile(suffix=ext, delete=not SAVE_TEMPFILES, dir=SAVE_DIR, prefix=prefix) as tmpfile: writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) titleRead, zonesRead = readTecplot(tmpfile.name) @@ -488,13 +508,13 @@ def test_ReadWriteFEZones(self, zoneType: ZoneType, precision: str, datapacking: elif zoneType == ZoneType.FEQUADRILATERAL: rawDataList = [createQuadGrid(nx, ny) for nx, ny in product(range(2, 10), range(2, 10))] elif zoneType == ZoneType.FETETRAHEDRON: - rawDataList = [createTetGrid(nx, ny, nz) for nx, ny, nz in product(range(2, 5), range(2, 5), range(2, 5))] + rawDataList = [createTetGrid(nx, ny, nz) for nx, ny, nz in zip(range(2, 10), range(2, 10), range(2, 10))] elif zoneType == ZoneType.FEBRICK: - rawDataList = [createBrickGrid(nx, ny, nz) for nx, ny, nz in product(range(2, 5), range(2, 5), range(2, 5))] + rawDataList = [createBrickGrid(nx, ny, nz) for nx, ny, nz in zip(range(2, 10), range(2, 10), range(2, 10))] for i, (nodes, connectivity) in enumerate(rawDataList): zone = TecplotFEZone( - f"Grid_{i}", + f"Grid_{i}_{nodes.shape}", {"X": nodes[..., 0], "Y": nodes[..., 1], "Z": nodes[..., 2]}, connectivity, zoneType=zoneType, @@ -502,7 +522,8 @@ def test_ReadWriteFEZones(self, zoneType: ZoneType, precision: str, datapacking: zones.append(zone) title = f"ASCII {zoneType.name} ZONES TEST" - with tempfile.NamedTemporaryFile(suffix=ext, delete=True) as tmpfile: + prefix = f"{zoneType.name}_{precision}_{datapacking}_" + with tempfile.NamedTemporaryFile(suffix=ext, delete=not SAVE_TEMPFILES, dir=SAVE_DIR, prefix=prefix) as tmpfile: writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) titleRead, zonesRead = readTecplot(tmpfile.name) From 63169eec71f6b4fa7979879f85f3e6b85b13ad81 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Tue, 24 Sep 2024 15:06:06 -0400 Subject: [PATCH 15/28] Added block format writer and reader to adhere to line width constraints --- baseclasses/utils/tecplotIO.py | 144 +++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 8 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 23e1da5..b22f7e9 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -1,9 +1,11 @@ import re import struct from abc import ABC, abstractmethod +from contextlib import contextmanager from enum import Enum from pathlib import Path -from typing import Any, Dict, Generic, List, Literal, TextIO, Tuple, TypeVar, Union +from typing import (Any, Dict, Generator, Generic, List, Literal, TextIO, + Tuple, TypeVar, Union) import numpy as np import numpy.typing as npt @@ -347,6 +349,93 @@ def _validateConnectivity(self) -> None: # ============================================================================== # ASCII WRITERS # ============================================================================== +@contextmanager +def numpyPrintOptions(**kwargs) -> Generator[Any, Any, Any]: + """Context manager to temporarily set numpy print options. + + Parameters + ---------- + kwargs + The numpy print options to set. + + Yields + ------ + None + """ + originalOptions = np.get_printoptions() + + try: + np.set_printoptions(**kwargs) + yield + except ValueError: + raise ValueError("Invalid numpy print options.") + finally: + np.set_printoptions(**originalOptions) + + +def writeArrayToFile( + arr: npt.NDArray[np.float64], + handle: TextIO, + maxLineWidth: int = 4000, + precision: int = 6, +) -> None: + """ + Write a 2D numpy array to a file using numpy.array_str with custom formatting. + + Parameters + ---------- + arr : npt.NDArray[np.float64] + 2D numpy array to write. + file : TextIO + A file-like object (e.g., opened with 'open()') to write to. + maxLineWidth : int, optional + Maximum width of each line in characters (default is 4000). + precision : int, optional + Number of decimal places for floating-point numbers (default is 6). + + Raises + ------ + ValueError + If the input array is not 2-dimensional. + + Examples + -------- + >>> arr = np.array([[1.123456, 2.123456], [3.123456, 4.123456]]) + >>> with open('output.txt', 'w') as f: + ... writeArrayToFile(arr, f, maxLineWidth=20, precision=3) + >>> with open('output.txt', 'r') as f: + ... print(f.read()) + 1.123 2.123 + 3.123 4.123 + """ + if arr.ndim != 2: + raise ValueError("Input must be a 2D numpy array") + + def customFormatter(x: float) -> str: + """ + Custom formatter for numpy array elements. + + Parameters + ---------- + x : float + The value to format. + + Returns + ------- + str + The formatted string representation of the value. + """ + if isinstance(x, (float, np.float32, np.float64)): + return f"{x:.{precision}E}" + return str(x) + + with numpyPrintOptions(formatter={"float_kind": customFormatter}, threshold=np.inf): + for row in arr: + rowStr = np.array_str(row, max_line_width=maxLineWidth) + cleanedRowStr = rowStr.strip("[]").strip().replace("\n ", "\n") # Remove brackets and extra spaces + handle.write(cleanedRowStr + "\n") + + class TecplotZoneWriterASCII(Generic[T], ABC): def __init__( self, @@ -385,7 +474,7 @@ def writeData(self, handle: TextIO): else: data = data.reshape(-1, len(self.zone.variables)).T - np.savetxt(handle, data, fmt=f"%.{self.fmtPrecision}E") + writeArrayToFile(data, handle, maxLineWidth=4000, precision=self.fmtPrecision) class TecplotOrderedZoneWriterASCII(TecplotZoneWriterASCII[TecplotOrderedZone]): @@ -937,6 +1026,47 @@ def write(self, filename: Union[str, Path]) -> None: # ============================================================================== # ASCII READERS # ============================================================================== +def readBlockData(filename: str, iCurrent: int, nVals: int, nVars: int) -> Tuple[npt.NDArray, int]: + """Read block data from a Tecplot ASCII file. + + Parameters + ---------- + filename : str + The filename of the Tecplot file. + iCurrent : int + The current line number in the file. + nVals : int + The number of values to read. + nVars : int + The number of variables in the data. + + Returns + ------- + Tuple[npt.NDArray, int] + The data and the number of lines read. + """ + MAX_LINE_LENGTH = 4000 + + with open(filename, "r") as handle: + lines = handle.readlines() + + firstVal = lines[iCurrent].split()[0] # This accounts for the precision + nPerLine = MAX_LINE_LENGTH // (len(firstVal) + 1) + + if len(lines[iCurrent].split()) < nPerLine: + nLines = nVars + else: + nLines = int(np.ceil(nVals / nPerLine) * nVars) + + data = [] + for line in lines[iCurrent : iCurrent + nLines]: + data.append([float(val) for val in line.split()]) + + data = np.concatenate(data) + + return data, nLines + + class TecplotASCIIReader: def __init__(self, filename: Union[str, Path]) -> None: """Reader for Tecplot files in ASCII format. @@ -1051,9 +1181,8 @@ def _readOrderedZoneData( nodeOffset = nNodes else: # Block data is row-major - nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=len(variables), dtype=float) - nodalData = nodalData.T.reshape(shape).squeeze() - nodeOffset = len(variables) + nodalData, nodeOffset = readBlockData(self.filename, iCurrent, nNodes, len(variables)) + nodalData = nodalData.reshape(shape).squeeze() data = {var: nodalData[..., i] for i, var in enumerate(variables)} zone = TecplotOrderedZone( @@ -1093,9 +1222,8 @@ def _readFEZoneData( nodeOffset = nNodes else: # Block data is row-major - nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=len(variables), dtype=float) - nodalData = np.atleast_2d(nodalData).T - nodeOffset = len(variables) + nodalData, nodeOffset = readBlockData(self.filename, iCurrent, nNodes, len(variables)) + nodalData = nodalData.reshape(nNodes, len(variables)) connectivity = np.loadtxt(self.filename, skiprows=iCurrent + nodeOffset, max_rows=nElements, dtype=int) From a503e8c6a78b48d444d7a48b638c0981a703fb9f Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Tue, 24 Sep 2024 15:07:15 -0400 Subject: [PATCH 16/28] Added line length test for ASCII files --- tests/test_tecplotIO.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index 95c925a..c645e73 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -12,7 +12,7 @@ from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, ZoneType, readTecplot, writeTecplot # --- Save tempfile locally or in a temp directory --- -SAVE_TEMPFILES = True +SAVE_TEMPFILES = False SAVE_DIR = Path(__file__).parent / "tmp_tecplot" if SAVE_TEMPFILES else None if SAVE_DIR is not None: SAVE_DIR.mkdir(exist_ok=True) @@ -491,6 +491,13 @@ def test_ReadWriteOrderedZones(self, shape: Tuple[int, ...], precision: str, dat writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) titleRead, zonesRead = readTecplot(tmpfile.name) + if ext == ".dat": + with open(tmpfile.name, "r") as f: + lines = f.readlines() + + for line in lines: + self.assertTrue(len(line) <= 4000) + self.assertEqual(titleRead, title) self.assertOrderedZonesEqual(zones, zonesRead) @@ -527,6 +534,13 @@ def test_ReadWriteFEZones(self, zoneType: ZoneType, precision: str, datapacking: writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) titleRead, zonesRead = readTecplot(tmpfile.name) + if ext == ".dat": + with open(tmpfile.name, "r") as f: + lines = f.readlines() + + for line in lines: + self.assertTrue(len(line) <= 4000) + self.assertEqual(titleRead, title) self.assertFEZonesEqual(zones, zonesRead) From fc5c1c27e25f6057cce6f25cd0205657e9ac2ce9 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Tue, 24 Sep 2024 17:34:18 -0400 Subject: [PATCH 17/28] Improved the ascii data reader to use multiple separators and ignore newlines --- baseclasses/utils/tecplotIO.py | 45 +++++++++++++++------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index b22f7e9..7c0a9c4 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -4,8 +4,7 @@ from contextlib import contextmanager from enum import Enum from pathlib import Path -from typing import (Any, Dict, Generator, Generic, List, Literal, TextIO, - Tuple, TypeVar, Union) +from typing import Any, Dict, Generator, Generic, List, Literal, TextIO, Tuple, TypeVar, Union import numpy as np import numpy.typing as npt @@ -1026,8 +1025,8 @@ def write(self, filename: Union[str, Path]) -> None: # ============================================================================== # ASCII READERS # ============================================================================== -def readBlockData(filename: str, iCurrent: int, nVals: int, nVars: int) -> Tuple[npt.NDArray, int]: - """Read block data from a Tecplot ASCII file. +def readArrayData(filename: str, iCurrent: int, nVals: int, nVars: int) -> Tuple[npt.NDArray, int]: + """Read array data from a Tecplot ASCII file. Parameters ---------- @@ -1045,24 +1044,21 @@ def readBlockData(filename: str, iCurrent: int, nVals: int, nVars: int) -> Tuple Tuple[npt.NDArray, int] The data and the number of lines read. """ - MAX_LINE_LENGTH = 4000 - with open(filename, "r") as handle: lines = handle.readlines() - firstVal = lines[iCurrent].split()[0] # This accounts for the precision - nPerLine = MAX_LINE_LENGTH // (len(firstVal) + 1) - - if len(lines[iCurrent].split()) < nPerLine: - nLines = nVars - else: - nLines = int(np.ceil(nVals / nPerLine) * nVars) + pattern = r"[\s,\t\n\r]+" # Separator pattern data = [] - for line in lines[iCurrent : iCurrent + nLines]: - data.append([float(val) for val in line.split()]) + nLines = 0 + while len(data) < nVals * nVars: + line = lines[iCurrent].strip() # Remove leading/trailing whitespace + vals = [float(val) for val in re.split(pattern, line) if val] # Split the line into values + data.extend(vals) # Add the values to the data list + iCurrent += 1 # Increment the line number + nLines += 1 - data = np.concatenate(data) + data = np.array(data) return data, nLines @@ -1176,13 +1172,12 @@ def _readOrderedZoneData( if zoneHeaderDict["datapacking"] == "POINT": # Point data is column-major - nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=nNodes, dtype=float) - nodalData = nodalData.reshape(shape).squeeze() - nodeOffset = nNodes + nodalData, nodeOffset = readArrayData(self.filename, iCurrent, nNodes, len(variables)) + nodalData = nodalData.reshape(shape, order="C").squeeze() else: # Block data is row-major - nodalData, nodeOffset = readBlockData(self.filename, iCurrent, nNodes, len(variables)) - nodalData = nodalData.reshape(shape).squeeze() + nodalData, nodeOffset = readArrayData(self.filename, iCurrent, nNodes, len(variables)) + nodalData = nodalData.reshape(shape, order="F").squeeze() data = {var: nodalData[..., i] for i, var in enumerate(variables)} zone = TecplotOrderedZone( @@ -1218,12 +1213,12 @@ def _readFEZoneData( if zoneHeaderDict["datapacking"] == "POINT": # Point data is column-major - nodalData = np.loadtxt(self.filename, skiprows=iCurrent, max_rows=nNodes, dtype=float) - nodeOffset = nNodes + nodalData, nodeOffset = readArrayData(self.filename, iCurrent, nNodes, len(variables)) + nodalData = nodalData.reshape(nNodes, len(variables), order="C") else: # Block data is row-major - nodalData, nodeOffset = readBlockData(self.filename, iCurrent, nNodes, len(variables)) - nodalData = nodalData.reshape(nNodes, len(variables)) + nodalData, nodeOffset = readArrayData(self.filename, iCurrent, nNodes, len(variables)) + nodalData = nodalData.reshape(nNodes, len(variables), order="F") connectivity = np.loadtxt(self.filename, skiprows=iCurrent + nodeOffset, max_rows=nElements, dtype=int) From 066a6bdec8f4912bfaedab9d2c5176a2b5b841d4 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Tue, 24 Sep 2024 18:43:32 -0400 Subject: [PATCH 18/28] Added full reader and writer separator support with tests --- baseclasses/utils/__init__.py | 4 +- baseclasses/utils/tecplotIO.py | 108 +++++++++++++-------------------- tests/test_tecplotIO.py | 22 +++++-- 3 files changed, 62 insertions(+), 72 deletions(-) diff --git a/baseclasses/utils/__init__.py b/baseclasses/utils/__init__.py index 117558f..56aea31 100644 --- a/baseclasses/utils/__init__.py +++ b/baseclasses/utils/__init__.py @@ -2,7 +2,7 @@ from .error import Error from .fileIO import readJSON, readPickle, redirectingIO, redirectIO, writeJSON, writePickle from .solverHistory import SolverHistory -from .tecplotIO import TecplotFEZone, TecplotOrderedZone, TecplotZone, ZoneType, readTecplot, writeTecplot +from .tecplotIO import Separator, TecplotFEZone, TecplotOrderedZone, TecplotZone, ZoneType, readTecplot, writeTecplot from .utils import ParseStringFormat, getPy3SafeString, pp __all__ = [ @@ -23,5 +23,7 @@ "TecplotOrderedZone", "writeTecplot", "readTecplot", + "ZoneType", + "Separator", "ParseStringFormat", ] diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 7c0a9c4..eef6775 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -1,10 +1,9 @@ import re import struct from abc import ABC, abstractmethod -from contextlib import contextmanager from enum import Enum from pathlib import Path -from typing import Any, Dict, Generator, Generic, List, Literal, TextIO, Tuple, TypeVar, Union +from typing import Any, Dict, Generic, List, Literal, TextIO, Tuple, TypeVar, Union import numpy as np import numpy.typing as npt @@ -77,6 +76,14 @@ class SolutionTime(Enum): UNSET = -1 +class Separator(Enum): + SPACE = " " + COMMA = "," + TAB = "\t" + NEWLINE = "\n" + CARRIAGE_RETURN = "\r" + + # ============================================================================== # DATA STRUCTURES # ============================================================================== @@ -348,35 +355,12 @@ def _validateConnectivity(self) -> None: # ============================================================================== # ASCII WRITERS # ============================================================================== -@contextmanager -def numpyPrintOptions(**kwargs) -> Generator[Any, Any, Any]: - """Context manager to temporarily set numpy print options. - - Parameters - ---------- - kwargs - The numpy print options to set. - - Yields - ------ - None - """ - originalOptions = np.get_printoptions() - - try: - np.set_printoptions(**kwargs) - yield - except ValueError: - raise ValueError("Invalid numpy print options.") - finally: - np.set_printoptions(**originalOptions) - - def writeArrayToFile( arr: npt.NDArray[np.float64], handle: TextIO, maxLineWidth: int = 4000, precision: int = 6, + separator: Separator = Separator.SPACE, ) -> None: """ Write a 2D numpy array to a file using numpy.array_str with custom formatting. @@ -391,48 +375,27 @@ def writeArrayToFile( Maximum width of each line in characters (default is 4000). precision : int, optional Number of decimal places for floating-point numbers (default is 6). + separator : Literal[" ", ",", "\\t", "\\n", "\\r"], optional + Separator to use between elements (default is " "). Raises ------ ValueError If the input array is not 2-dimensional. - - Examples - -------- - >>> arr = np.array([[1.123456, 2.123456], [3.123456, 4.123456]]) - >>> with open('output.txt', 'w') as f: - ... writeArrayToFile(arr, f, maxLineWidth=20, precision=3) - >>> with open('output.txt', 'r') as f: - ... print(f.read()) - 1.123 2.123 - 3.123 4.123 """ if arr.ndim != 2: raise ValueError("Input must be a 2D numpy array") - def customFormatter(x: float) -> str: - """ - Custom formatter for numpy array elements. - - Parameters - ---------- - x : float - The value to format. - - Returns - ------- - str - The formatted string representation of the value. - """ - if isinstance(x, (float, np.float32, np.float64)): - return f"{x:.{precision}E}" - return str(x) - - with numpyPrintOptions(formatter={"float_kind": customFormatter}, threshold=np.inf): - for row in arr: - rowStr = np.array_str(row, max_line_width=maxLineWidth) - cleanedRowStr = rowStr.strip("[]").strip().replace("\n ", "\n") # Remove brackets and extra spaces - handle.write(cleanedRowStr + "\n") + for row in arr: + rowStr = np.array2string( + row, + max_line_width=maxLineWidth, + separator=separator.value, + formatter={"float_kind": lambda x: f"{x:.{precision}E}"}, + threshold=np.inf, + ) + cleanedRowStr = rowStr.strip("[]").strip().replace("\n ", "\n") # Remove brackets and extra spaces + handle.write(cleanedRowStr + "\n") class TecplotZoneWriterASCII(Generic[T], ABC): @@ -441,6 +404,7 @@ def __init__( zone: T, datapacking: Literal["BLOCK", "POINT"], precision: Literal["SINGLE", "DOUBLE"], + separator: Separator = Separator.SPACE, ) -> None: """Abstract base class for writing Tecplot zones to ASCII files. @@ -456,6 +420,7 @@ def __init__( self.zone = zone self.datapacking = DataPacking[datapacking].name self.fmtPrecision = DataPrecision[precision].value + self.separator = separator @abstractmethod def writeHeader(self, handle: TextIO): @@ -473,7 +438,7 @@ def writeData(self, handle: TextIO): else: data = data.reshape(-1, len(self.zone.variables)).T - writeArrayToFile(data, handle, maxLineWidth=4000, precision=self.fmtPrecision) + writeArrayToFile(data, handle, maxLineWidth=4000, precision=self.fmtPrecision, separator=self.separator) class TecplotOrderedZoneWriterASCII(TecplotZoneWriterASCII[TecplotOrderedZone]): @@ -482,6 +447,7 @@ def __init__( zone: TecplotOrderedZone, datapacking: Literal["BLOCK", "POINT"], precision: Literal["SINGLE", "DOUBLE"], + separator: Separator = Separator.SPACE, ) -> None: """Writer for Tecplot ordered zones in ASCII format. @@ -493,8 +459,10 @@ def __init__( The data packing format. BLOCK is row-major, POINT is column-major. precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. + separator : Separator, optional + Separator to use between elements, by default Separator.SPACE """ - super().__init__(zone, datapacking, precision) + super().__init__(zone, datapacking, precision, separator) def writeHeader(self, handle: TextIO): """Write the zone header to the file. @@ -545,6 +513,7 @@ def __init__( zone: TecplotFEZone, datapacking: Literal["BLOCK", "POINT"], precision: Literal["SINGLE", "DOUBLE"], + separator: Separator = Separator.SPACE, ) -> None: """Writer for Tecplot finite element zones in ASCII format. @@ -556,8 +525,10 @@ def __init__( The data packing format. BLOCK is row-major, POINT is column-major. precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. + separator : Separator, optional + Separator to use between elements, by default Separator.SPACE """ - super().__init__(zone, datapacking, precision) + super().__init__(zone, datapacking, precision, separator) def writeHeader(self, handle: TextIO): """Write the zone header to the file. @@ -612,6 +583,7 @@ def __init__( zones: List[TecplotZone], datapacking: Literal["BLOCK", "POINT"], precision: Literal["SINGLE", "DOUBLE"], + separator: Separator = Separator.SPACE, ) -> None: """Writer for Tecplot files in ASCII format. @@ -625,11 +597,14 @@ def __init__( The data packing format. BLOCK is row-major, POINT is column-major. precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. + separator : Separator, optional + Separator to use between elements, by default Separator.SPACE """ self.title = title self.zones = zones self.datapacking = DataPacking[datapacking].name self.precision = DataPrecision[precision].name + self.separator = separator self._validateVariables() def _validateVariables(self) -> None: @@ -665,9 +640,9 @@ def _writeZone(self, handle: TextIO, zone: TecplotZone) -> None: If the zone type is invalid. """ if isinstance(zone, TecplotOrderedZone): - writer = TecplotOrderedZoneWriterASCII(zone, self.datapacking, self.precision) + writer = TecplotOrderedZoneWriterASCII(zone, self.datapacking, self.precision, self.separator) elif isinstance(zone, TecplotFEZone): - writer = TecplotFEZoneWriterASCII(zone, self.datapacking, self.precision) + writer = TecplotFEZoneWriterASCII(zone, self.datapacking, self.precision, self.separator) else: raise ValueError("Invalid zone type.") @@ -1668,6 +1643,7 @@ def writeTecplot( zones: List[TecplotZone], datapacking: Literal["BLOCK", "POINT"] = "POINT", precision: Literal["SINGLE", "DOUBLE"] = "SINGLE", + separator: Separator = Separator.SPACE, ) -> None: """Write a Tecplot file to disk. The file format is determined by the file extension. If the extension is .plt, the file will be written in @@ -1691,6 +1667,8 @@ def writeTecplot( The data packing format. BLOCK is row-major, POINT is column-major, by default "POINT" precision : Literal["SINGLE", "DOUBLE"], optional The floating point precision to write the data, by default "SINGLE" + separator : Separator, optional + The separator to use when writing ASCII files, by default Separator.SPACE Raises ------ @@ -1721,7 +1699,7 @@ def writeTecplot( writer = TecplotWriterBinary(title, zones, precision) writer.write(filepath) elif filepath.suffix == ".dat": - writer = TecplotWriterASCII(title, zones, datapacking, precision) + writer = TecplotWriterASCII(title, zones, datapacking, precision, separator) writer.write(filename) else: raise ValueError("Invalid file extension. Must be .plt (binary) or .dat (ASCII).") diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index c645e73..55fbdb8 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -9,7 +9,7 @@ import numpy.typing as npt from parameterized import parameterized -from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, ZoneType, readTecplot, writeTecplot +from baseclasses.utils import Separator, TecplotFEZone, TecplotOrderedZone, ZoneType, readTecplot, writeTecplot # --- Save tempfile locally or in a temp directory --- SAVE_TEMPFILES = False @@ -21,12 +21,17 @@ TEST_CASES_ORDERED = [ tc for tc in itertools.product( - [(10,), (100,), (10, 10), (100, 100), (10, 10, 10), (100, 100, 100)], + [(10,), (100,), (10, 10), (100, 100), (10, 10, 10)], ["SINGLE", "DOUBLE"], ["POINT", "BLOCK"], [".dat", ".plt"], + [Separator.SPACE, Separator.COMMA, Separator.TAB, Separator.NEWLINE, Separator.CARRIAGE_RETURN], ) ] +# Add a single stress test case for each datapacking +TEST_CASES_ORDERED.append(((100, 100, 100), "DOUBLE", "BLOCK", ".dat", Separator.SPACE)) +TEST_CASES_ORDERED.append(((100, 100, 100), "DOUBLE", "POINT", ".dat", Separator.COMMA)) + # filter out cases that use POINT datapacking with binary '.plt' extensions TEST_CASES_ORDERED = [tc for tc in TEST_CASES_ORDERED if not (tc[2] == "POINT" and tc[3] == ".plt")] @@ -37,6 +42,7 @@ ["SINGLE", "DOUBLE"], ["POINT", "BLOCK"], [".dat", ".plt"], + [Separator.SPACE, Separator.COMMA, Separator.TAB, Separator.NEWLINE, Separator.CARRIAGE_RETURN], ) ] # filter out cases that use POINT datapacking with binary '.plt' extensions @@ -462,7 +468,9 @@ def test_FEZone(self): TEST_CASES_ORDERED, name_func=lambda f, n, p: parameterized.to_safe_name(f"{f.__name__}_{p[0]}"), ) - def test_ReadWriteOrderedZones(self, shape: Tuple[int, ...], precision: str, datapacking: str, ext: str): + def test_ReadWriteOrderedZones( + self, shape: Tuple[int, ...], precision: str, datapacking: str, ext: str, separator: Separator + ): zones: List[TecplotOrderedZone] = [] if len(shape) == 1: @@ -488,7 +496,7 @@ def test_ReadWriteOrderedZones(self, shape: Tuple[int, ...], precision: str, dat title = "ASCII ORDERED ZONES TEST" prefix = f"ORDERED_{shape}_{precision}_{datapacking}_" with tempfile.NamedTemporaryFile(suffix=ext, delete=not SAVE_TEMPFILES, dir=SAVE_DIR, prefix=prefix) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) + writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision, separator=separator) titleRead, zonesRead = readTecplot(tmpfile.name) if ext == ".dat": @@ -505,7 +513,9 @@ def test_ReadWriteOrderedZones(self, shape: Tuple[int, ...], precision: str, dat TEST_CASES_FE, name_func=lambda f, n, p: parameterized.to_safe_name(f"{f.__name__}_{p[0]}"), ) - def test_ReadWriteFEZones(self, zoneType: ZoneType, precision: str, datapacking: str, ext: str): + def test_ReadWriteFEZones( + self, zoneType: ZoneType, precision: str, datapacking: str, ext: str, separator: Separator + ): zones: List[TecplotFEZone] = [] if zoneType == ZoneType.FELINESEG: @@ -531,7 +541,7 @@ def test_ReadWriteFEZones(self, zoneType: ZoneType, precision: str, datapacking: title = f"ASCII {zoneType.name} ZONES TEST" prefix = f"{zoneType.name}_{precision}_{datapacking}_" with tempfile.NamedTemporaryFile(suffix=ext, delete=not SAVE_TEMPFILES, dir=SAVE_DIR, prefix=prefix) as tmpfile: - writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision) + writeTecplot(tmpfile.name, title, zones, datapacking=datapacking, precision=precision, separator=separator) titleRead, zonesRead = readTecplot(tmpfile.name) if ext == ".dat": From 449c9b9ec3bc30107e77a87cd17afcb0ec4a7748 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Tue, 24 Sep 2024 23:40:53 -0400 Subject: [PATCH 19/28] Cleaning up documentation, imports, and api --- baseclasses/__init__.py | 2 +- baseclasses/problems/pyWeight_problem.py | 3 +- baseclasses/solvers/pyAero_solver.py | 4 +- baseclasses/utils/__init__.py | 4 +- baseclasses/utils/tecplotIO.py | 139 ++++++++++++++++------- tests/test_tecplotIO.py | 3 +- 6 files changed, 104 insertions(+), 51 deletions(-) diff --git a/baseclasses/__init__.py b/baseclasses/__init__.py index 2ac75e6..9dbaee9 100644 --- a/baseclasses/__init__.py +++ b/baseclasses/__init__.py @@ -18,4 +18,4 @@ ) from .solvers import AeroSolver, BaseSolver from .testing import BaseRegTest, getTol -from .utils import getPy3SafeString +from .utils import getPy3SafeString, tecplotIO diff --git a/baseclasses/problems/pyWeight_problem.py b/baseclasses/problems/pyWeight_problem.py index 2006642..5b931c7 100644 --- a/baseclasses/problems/pyWeight_problem.py +++ b/baseclasses/problems/pyWeight_problem.py @@ -9,7 +9,8 @@ import numpy as np -from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, ZoneType, writeTecplot +from ..utils import TecplotFEZone, TecplotOrderedZone, writeTecplot +from ..utils.tecplotIO import ZoneType try: from pygeo import geo_utils diff --git a/baseclasses/solvers/pyAero_solver.py b/baseclasses/solvers/pyAero_solver.py index a112a11..32cdfee 100644 --- a/baseclasses/solvers/pyAero_solver.py +++ b/baseclasses/solvers/pyAero_solver.py @@ -9,12 +9,12 @@ # ============================================================================= import numpy as np -from ..utils import CaseInsensitiveDict, Error, TecplotFEZone, ZoneType, writeTecplot - # ============================================================================= # Extension modules # ============================================================================= from .BaseSolver import BaseSolver +from ..utils import CaseInsensitiveDict, Error, TecplotFEZone, writeTecplot +from ..utils.tecplotIO import ZoneType # ============================================================================= # AeroSolver Class diff --git a/baseclasses/utils/__init__.py b/baseclasses/utils/__init__.py index 56aea31..5e0737f 100644 --- a/baseclasses/utils/__init__.py +++ b/baseclasses/utils/__init__.py @@ -2,7 +2,7 @@ from .error import Error from .fileIO import readJSON, readPickle, redirectingIO, redirectIO, writeJSON, writePickle from .solverHistory import SolverHistory -from .tecplotIO import Separator, TecplotFEZone, TecplotOrderedZone, TecplotZone, ZoneType, readTecplot, writeTecplot +from .tecplotIO import TecplotFEZone, TecplotOrderedZone, TecplotZone, readTecplot, writeTecplot from .utils import ParseStringFormat, getPy3SafeString, pp __all__ = [ @@ -23,7 +23,5 @@ "TecplotOrderedZone", "writeTecplot", "readTecplot", - "ZoneType", - "Separator", "ParseStringFormat", ] diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index eef6775..c5da3c5 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -15,6 +15,8 @@ # ENUMS # ============================================================================== class ZoneType(Enum): + """Tecplot finite element zone types""" + UNSET = -1 ORDERED = 0 FELINESEG = 1 @@ -25,58 +27,80 @@ class ZoneType(Enum): class DataPacking(Enum): + """Tecplot data packing formats""" + BLOCK = 0 POINT = 1 class VariableLocation(Enum): + """Grid location of the variable data""" + NODE = 0 CELL_CENTER = 1 NODE_AND_CELL_CENTER = 2 class DataPrecision(Enum): + """Tecplot data precision""" + SINGLE = 6 DOUBLE = 12 class BinaryDataPrecisionCodes(Enum): + """Binary data precision codes""" + SINGLE = 1 DOUBLE = 2 class DTypePrecision(Enum): + """Numpy data types for single and double precision""" + SINGLE = np.float32 DOUBLE = np.float64 class FileType(Enum): + """Tecplot file types""" + FULL = 0 GRID = 1 SOLUTION = 2 class SectionMarkers(Enum): + """Tecplot section markers""" + ZONE = 299.0 # V11.2 marker DATA = 357.0 class BinaryFlags(Enum): + """Binary boolean flags""" + NONE = -1 FALSE = 0 TRUE = 1 class StrandID(Enum): + """Strand ID default codes""" + PENDING = -2 STATIC = -1 class SolutionTime(Enum): + """Solution time default codes""" + UNSET = -1 class Separator(Enum): + """Separator characters""" + SPACE = " " COMMA = "," TAB = "\t" @@ -150,7 +174,8 @@ def _validateData(self) -> None: Raises ------ TypeError - If the data is not a dictionary or the values are not numpy arrays. + If the data is not a dictionary or the values are not numpy + arrays. ValueError If the variables do not have the same shape. """ @@ -193,8 +218,8 @@ def _validateStrandID(self) -> None: class TecplotOrderedZone(TecplotZone): - """Tecplot ordered zone. These zones do not contain connectivity information - because the data is ordered in an (i, j, k) grid. + """Tecplot ordered zone. These zones do not contain connectivity + information because the data is ordered in an (i, j, k) grid. """ def __init__( @@ -252,12 +277,13 @@ def kMax(self) -> int: class TecplotFEZone(TecplotZone): - """Tecplot finite element zone. These zones contain connectivity information - to describe the elements in the zone. The type of element is determined by the - shape of the connectivity array and the ``tetrahedral`` flag. The connectivity - array is 0-based. + """Tecplot finite element zone. These zones contain connectivity + information to describe the elements in the zone. The type of + element is determined by the shape of the connectivity array and + the ``tetrahedral`` flag. The connectivity array is 0-based. - The following shapes correspond to the following element types, where n is the number of elements: + The following shapes correspond to the following element types, + where n is the number of elements: - ``(n, 2)``: FELINESEG - ``(n, 3)``: FETRIANGLE @@ -304,7 +330,8 @@ def __init__( data : Dict[str, npt.NDArray] A dictionary of variable names and their corresponding data. connectivity : npt.NDArray - The connectivity array that describes the elements in the zone. + The connectivity array that describes the elements in the + zone. zoneType : Union[str, ZoneType] The type of the zone. Can be a string that matches an entry in the ZoneType enum or the ZoneType enum itself. @@ -363,7 +390,8 @@ def writeArrayToFile( separator: Separator = Separator.SPACE, ) -> None: """ - Write a 2D numpy array to a file using numpy.array_str with custom formatting. + Write a 2D numpy array to a file using numpy.array_str with custom + formatting. Parameters ---------- @@ -372,11 +400,12 @@ def writeArrayToFile( file : TextIO A file-like object (e.g., opened with 'open()') to write to. maxLineWidth : int, optional - Maximum width of each line in characters (default is 4000). + Maximum width of each line in characters, by default 4000. precision : int, optional - Number of decimal places for floating-point numbers (default is 6). + Number of decimal places for floating-point numbers, by default + 6. separator : Literal[" ", ",", "\\t", "\\n", "\\r"], optional - Separator to use between elements (default is " "). + Separator to use between elements, by default Separator.SPACE Raises ------ @@ -413,7 +442,8 @@ def __init__( zone : T The Tecplot zone to write. datapacking : Literal["BLOCK", "POINT"] - The data packing format. BLOCK is row-major, POINT is column-major. + The data packing format. BLOCK is row-major, POINT is + column-major. precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. """ @@ -456,11 +486,14 @@ def __init__( zone : TecplotOrderedZone The ordered zone to write. datapacking : Literal["BLOCK", "POINT"] - The data packing format. BLOCK is row-major, POINT is column-major. + The data packing format. BLOCK is row-major, POINT is + column-major. precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. separator : Separator, optional - Separator to use between elements, by default Separator.SPACE + Separator to use between elements. The Separator + is an enum defined in :meth:`Separator `, + by default Separator.SPACE """ super().__init__(zone, datapacking, precision, separator) @@ -522,11 +555,14 @@ def __init__( zone : TecplotFEZone The finite element zone to write. datapacking : Literal["BLOCK", "POINT"] - The data packing format. BLOCK is row-major, POINT is column-major. + The data packing format. BLOCK is row-major, POINT is + column-major. precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. separator : Separator, optional - Separator to use between elements, by default Separator.SPACE + Separator to use between elements. The Separator is an enum + defined in :meth:`Separator `, + by default Separator.SPACE """ super().__init__(zone, datapacking, precision, separator) @@ -560,7 +596,8 @@ def writeHeader(self, handle: TextIO): handle.write(zoneString) def writeFooter(self, handle: TextIO): - """Write the zone footer to the file. This includes the connectivity information. + """Write the zone footer to the file. This includes the + connectivity information. Parameters ---------- @@ -594,11 +631,14 @@ def __init__( zones : List[TecplotZone] A list of Tecplot zones to write. datapacking : Literal["BLOCK", "POINT"] - The data packing format. BLOCK is row-major, POINT is column-major. + The data packing format. BLOCK is row-major, POINT is + column-major. precision : Literal["SINGLE", "DOUBLE"] The floating point precision to write the data. separator : Separator, optional - Separator to use between elements, by default Separator.SPACE + Separator to use between elements. The Separator + is an enum defined in :meth:`Separator `, + by default Separator.SPACE """ self.title = title self.zones = zones @@ -731,7 +771,8 @@ def __init__( zone: T, precision: Literal["SINGLE", "DOUBLE"], ) -> None: - """Abstract base class for writing Tecplot zones to binary files. + """Abstract base class for writing Tecplot zones to binary + files. Parameters ---------- @@ -845,7 +886,8 @@ def writeHeader(self, handle: TextIO): _writeInteger(handle, BinaryFlags.FALSE.value) # No aux data def writeFooter(self, handle: TextIO): - """Write the zone footer to the file. This is not used for ordered zones. + """Write the zone footer to the file. This is not used for + ordered zones. Parameters ---------- @@ -894,7 +936,8 @@ def writeHeader(self, handle: TextIO): _writeInteger(handle, BinaryFlags.FALSE.value) # No aux data def writeFooter(self, handle: TextIO): - """Write the zone footer to the file. This includes the connectivity information. + """Write the zone footer to the file. This includes the + connectivity information. Parameters ---------- @@ -1332,7 +1375,8 @@ def _readInteger(self, handle: TextIO, offset: int = 0) -> int: handle : TextIO The file handle to read from. offset : int, optional - The offset (in bytes) from the file's current position, by default 0 + The offset (in bytes) from the file's current position, + by default 0 Returns ------- @@ -1351,7 +1395,8 @@ def _readIntegerArray(self, handle: TextIO, nValues: int, offset: int = 0) -> np nValues : int The number of values to read. offset : int, optional - The offset (in bytes) from the file's current position, by default 0 + The offset (in bytes) from the file's current position, + by default 0 Returns ------- @@ -1368,7 +1413,8 @@ def _readFloat32(self, handle: TextIO, offset: int = 0) -> float: handle : TextIO The file handle to read from. offset : int, optional - The offset (in bytes) from the file's current position, by default 0 + The offset (in bytes) from the file's current position, + by default 0 Returns ------- @@ -1387,7 +1433,8 @@ def _readFloat32Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np nValues : int The number of values to read. offset : int, optional - The offset (in bytes) from the file's current position, by default 0 + The offset (in bytes) from the file's current position, + by default 0 Returns ------- @@ -1404,7 +1451,8 @@ def _readFloat64(self, handle: TextIO, offset: int = 0) -> float: handle : TextIO The file handle to read from. offset : int, optional - The offset (in bytes) from the file's current position, by default 0 + The offset (in bytes) from the file's current position, + by default 0 Returns ------- @@ -1423,7 +1471,8 @@ def _readFloat64Array(self, handle: TextIO, nValues: int, offset: int = 0) -> np nValues : int The number of values to read. offset : int, optional - The offset (in bytes) from the file's current position, by default 0 + The offset (in bytes) from the file's current position, + by default 0 Returns ------- @@ -1664,11 +1713,15 @@ def writeTecplot( zones : List[TecplotZone] A list of Tecplot zones to write datapacking : Literal["BLOCK", "POINT"], optional - The data packing format. BLOCK is row-major, POINT is column-major, by default "POINT" + The data packing format. BLOCK is row-major, POINT is + column-major, by default "POINT" precision : Literal["SINGLE", "DOUBLE"], optional - The floating point precision to write the data, by default "SINGLE" + The floating point precision to write the data, by default + "SINGLE" separator : Separator, optional - The separator to use when writing ASCII files, by default Separator.SPACE + The separator to use when writing ASCII files. The Separator + is an enum defined in :meth:`Separator `, + by default Separator.SPACE Raises ------ @@ -1679,19 +1732,19 @@ def writeTecplot( -------- .. code-block:: python - from baseclasses.utils import TecplotOrderedZone, writeTecplot + from baseclasses import tecplotIO as tpio import numpy as np nx, ny, nz = 10, 10, 10 X, Y, Z = np.meshgrid(np.random.rand(nx), np.random.rand(ny), np.random.rand(nz), indexing="ij") data = {"X": X, "Y": Y, "Z": Z} - zone = TecplotOrderedZone("OrderedZone", data) + zone = tpio.TecplotOrderedZone("OrderedZone", data) # Write the Tecplot file in ASCII format - writeTecplot("ordered.dat", "Ordered Zone", [zone], datapacking="BLOCK", precision="SINGLE") + tpio.writeTecplot("ordered.dat", "Ordered Zone", [zone], datapacking="BLOCK", precision="SINGLE", separator=tpio.Separator.SPACE) # Write the Tecplot file in binary format - writeTecplot("ordered.plt", "Ordered Zone", [zone], precision="SINGLE") + tpio.writeTecplot("ordered.plt", "Ordered Zone", [zone], precision="SINGLE") """ filepath = Path(filename) @@ -1706,9 +1759,9 @@ def writeTecplot( def readTecplot(filename: Union[str, Path]) -> Tuple[str, List[Union[TecplotOrderedZone, TecplotFEZone]]]: - """Read a Tecplot file from disk. The file format is determined by the - file extension. If the extension is .plt, the file will be read in - binary format. If the extension is .dat, the file will be read in + """Read a Tecplot file from disk. The file format is determined by + the file extension. If the extension is .plt, the file will be read + in binary format. If the extension is .dat, the file will be read in ASCII format. Parameters @@ -1730,13 +1783,13 @@ def readTecplot(filename: Union[str, Path]) -> Tuple[str, List[Union[TecplotOrde -------- .. code-block:: python - from baseclasses.utils import readTecplot + from baseclasses import tecplotIO as tpio # Read a Tecplot file in ASCII format - title, zones = readTecplot("ordered.dat") + title, zones = tpio.readTecplot("ordered.dat") # Read a Tecplot file in binary format - title, zones = readTecplot("ordered.plt") + title, zones = tpio.readTecplot("ordered.plt") """ filepath = Path(filename) if filepath.suffix == ".plt": diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index 55fbdb8..48c55df 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -9,7 +9,8 @@ import numpy.typing as npt from parameterized import parameterized -from baseclasses.utils import Separator, TecplotFEZone, TecplotOrderedZone, ZoneType, readTecplot, writeTecplot +from baseclasses.utils import TecplotFEZone, TecplotOrderedZone, readTecplot, writeTecplot +from baseclasses.utils.tecplotIO import Separator, ZoneType # --- Save tempfile locally or in a temp directory --- SAVE_TEMPFILES = False From c2baf7c88a7f364e5f9893b20451b788f3560527 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 8 Nov 2024 17:23:55 -0500 Subject: [PATCH 20/28] Added tri connectivity property for FE zones with tests --- baseclasses/utils/tecplotIO.py | 9 ++++++ tests/test_tecplotIO.py | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index c5da3c5..38c3d30 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -350,6 +350,15 @@ def __init__( def nElements(self) -> int: return self.connectivity.shape[0] + @property + def triConnectivity(self) -> npt.NDArray: + if self.zoneType == ZoneType.FETRIANGLE: + return self.connectivity + elif self.zoneType == ZoneType.FEQUADRILATERAL: + return np.row_stack((self.connectivity[:, [0, 1, 2]], self.connectivity[:, [0, 2, 3]])) + else: + raise TypeError(f"'triConnectivity' not supported for {self.zoneType.name} zone type.") + def _validateZoneType(self) -> None: supportedZones = [zone.name for zone in ZoneType if zone.name != "ORDERED"] if isinstance(self.zoneType, str): diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index 48c55df..bd66e38 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -566,3 +566,62 @@ def test_BinaryReadWriteExternal(self): title, zones = readTecplot(self.externalFileBinary) except Exception as e: self.fail(f"Reading external binary file {self.externalFileBinary} failed with error: {e}") + + def test_TriToTriConn(self): + # Create a fe ordered tri zone + ni, nj = 10, 10 + nodes, connectivity = createTriGrid(ni, nj) + zone = TecplotFEZone( + "TriGrid", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity, zoneType=ZoneType.FETRIANGLE + ) + + triConn = zone.triConnectivity + + np.testing.assert_array_equal(triConn, zone.connectivity) + + def test_QuadToTriConn(self): + # Create a fe ordered quad zone + ni, nj = 10, 10 + nodes, connectivity = createQuadGrid(ni, nj) + zone = TecplotFEZone( + "QuadGrid", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity, zoneType=ZoneType.FEQUADRILATERAL + ) + + triConn = zone.triConnectivity + + self.assertEqual(triConn.shape, (zone.nElements * 2, 3)) + + def test_TriConnBadZoneType(self): + # Create an line seg zone + ni = 10 + nodes, connectivity = createLineSegGrid(ni) + zone = TecplotFEZone("LineSeg", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity, zoneType=ZoneType.FELINESEG) + + with self.assertRaises(TypeError): + triConn = zone.triConnectivity + + # Create a tet zone + ni, nj, nk = 10, 10, 10 + nodes, connectivity = createTetGrid(ni, nj, nk) + zone = TecplotFEZone( + "TetGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + zoneType=ZoneType.FETETRAHEDRON, + ) + + with self.assertRaises(TypeError): + triConn = zone.triConnectivity + + # Create a brick zone + ni, nj, nk = 10, 10, 10 + nodes, connectivity = createBrickGrid(ni, nj, nk) + zone = TecplotFEZone( + "BrickGrid", + {"X": nodes[:, 0], "Y": nodes[:, 1], "Z": nodes[:, 2]}, + connectivity, + zoneType=ZoneType.FEBRICK, + ) + + with self.assertRaises(TypeError): + triConn = zone.triConnectivity From fc5fbe8b0989b4a625878412c272751240f8baa4 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 8 Nov 2024 17:26:11 -0500 Subject: [PATCH 21/28] Flake 8 is always watching --- tests/test_tecplotIO.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index bd66e38..3c75c9d 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -598,7 +598,7 @@ def test_TriConnBadZoneType(self): zone = TecplotFEZone("LineSeg", {"X": nodes[:, 0], "Y": nodes[:, 1]}, connectivity, zoneType=ZoneType.FELINESEG) with self.assertRaises(TypeError): - triConn = zone.triConnectivity + zone.triConnectivity # Create a tet zone ni, nj, nk = 10, 10, 10 @@ -611,7 +611,7 @@ def test_TriConnBadZoneType(self): ) with self.assertRaises(TypeError): - triConn = zone.triConnectivity + zone.triConnectivity # Create a brick zone ni, nj, nk = 10, 10, 10 @@ -624,4 +624,4 @@ def test_TriConnBadZoneType(self): ) with self.assertRaises(TypeError): - triConn = zone.triConnectivity + zone.triConnectivity From 1e44f714973e0ffbded6c900cf151c9abe7beb0e Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 8 Nov 2024 18:33:56 -0500 Subject: [PATCH 22/28] Added unique indices and nodes to fe zones --- baseclasses/utils/tecplotIO.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 38c3d30..37523d5 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -359,6 +359,14 @@ def triConnectivity(self) -> npt.NDArray: else: raise TypeError(f"'triConnectivity' not supported for {self.zoneType.name} zone type.") + @property + def uniqueIndices(self) -> npt.NDArray: + return np.unique(self.connectivity.flatten()) + + @property + def uniqueNodalData(self) -> Dict[str, npt.NDArray]: + return {var: self.data[var][self.uniqueIndices] for var in self.variables} + def _validateZoneType(self) -> None: supportedZones = [zone.name for zone in ZoneType if zone.name != "ORDERED"] if isinstance(self.zoneType, str): From b9b83aab8a075cc25b0d66a18a131d48f419900c Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Sun, 10 Nov 2024 17:45:14 -0500 Subject: [PATCH 23/28] New connectivity handling for FE zones with unique data and connectivity mapping --- baseclasses/utils/tecplotIO.py | 30 +++++++++++++++++++++++++----- tests/test_tecplotIO.py | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 37523d5..032472b 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -341,10 +341,23 @@ def __init__( The strand id of the zone, by default -1 """ super().__init__(zoneName, data, solutionTime=solutionTime, strandID=strandID) - self.connectivity = connectivity + self._connectivity = connectivity self.zoneType = zoneType self._validateZoneType() self._validateConnectivity() + self._uniqueIndices = np.unique(self.connectivity.flatten()) + self._uniqueConnectivity = self._remapConnectivity() + + @property + def connectivity(self) -> npt.NDArray: + return self._connectivity + + @connectivity.setter + def connectivity(self, value: npt.NDArray) -> None: + self._connectivity = value + self._validateConnectivity() + self._uniqueIndices = np.unique(self.connectivity.flatten()) + self._uniqueConnectivity = self._remapConnectivity() @property def nElements(self) -> int: @@ -360,12 +373,12 @@ def triConnectivity(self) -> npt.NDArray: raise TypeError(f"'triConnectivity' not supported for {self.zoneType.name} zone type.") @property - def uniqueIndices(self) -> npt.NDArray: - return np.unique(self.connectivity.flatten()) + def uniqueData(self) -> Dict[str, npt.NDArray]: + return {var: self.data[var][self._uniqueIndices] for var in self.variables} @property - def uniqueNodalData(self) -> Dict[str, npt.NDArray]: - return {var: self.data[var][self.uniqueIndices] for var in self.variables} + def uniqueConnectivity(self) -> npt.NDArray: + return self._uniqueConnectivity def _validateZoneType(self) -> None: supportedZones = [zone.name for zone in ZoneType if zone.name != "ORDERED"] @@ -395,6 +408,13 @@ def _validateConnectivity(self) -> None: # but raise an error just in case. raise TypeError("Invalid zone type.") + def _remapConnectivity(self) -> npt.NDArray: + uniqueIndices = self._uniqueIndices + remap = np.full(len(uniqueIndices), -1, dtype=int) + remap[uniqueIndices] = np.arange(len(uniqueIndices)) + remappedConnectivity = remap[self.connectivity] + return remappedConnectivity + # ============================================================================== # ASCII WRITERS diff --git a/tests/test_tecplotIO.py b/tests/test_tecplotIO.py index 3c75c9d..210a0db 100644 --- a/tests/test_tecplotIO.py +++ b/tests/test_tecplotIO.py @@ -558,12 +558,31 @@ def test_ReadWriteFEZones( def test_ASCIIReadWriteExternal(self): try: title, zones = readTecplot(self.externalFileAscii) + + for zone in zones: + conn = zone.connectivity + uniqueConn = zone.uniqueConnectivity + data = zone.data + uniqueData = zone.uniqueData + + for variable in zone.variables: + np.testing.assert_allclose(data[variable][conn], uniqueData[variable][uniqueConn]) except Exception as e: self.fail(f"Reading external ASCII file {self.externalFileAscii} failed with error: {e}") def test_BinaryReadWriteExternal(self): try: title, zones = readTecplot(self.externalFileBinary) + + for zone in zones: + conn = zone.connectivity + uniqueConn = zone.uniqueConnectivity + data = zone.data + uniqueData = zone.uniqueData + + for variable in zone.variables: + np.testing.assert_allclose(data[variable][conn], uniqueData[variable][uniqueConn]) + except Exception as e: self.fail(f"Reading external binary file {self.externalFileBinary} failed with error: {e}") From b30741e1cdb92edf3a8f654f6042b5aee6f003df Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Mon, 11 Nov 2024 20:38:04 -0500 Subject: [PATCH 24/28] Fixed remap connectivity bug --- baseclasses/utils/tecplotIO.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 032472b..7d3007d 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -345,7 +345,7 @@ def __init__( self.zoneType = zoneType self._validateZoneType() self._validateConnectivity() - self._uniqueIndices = np.unique(self.connectivity.flatten()) + self._uniqueIndices = np.unique(self._connectivity.flatten()) self._uniqueConnectivity = self._remapConnectivity() @property @@ -356,7 +356,7 @@ def connectivity(self) -> npt.NDArray: def connectivity(self, value: npt.NDArray) -> None: self._connectivity = value self._validateConnectivity() - self._uniqueIndices = np.unique(self.connectivity.flatten()) + self._uniqueIndices = np.unique(self._connectivity.flatten()) self._uniqueConnectivity = self._remapConnectivity() @property @@ -410,9 +410,8 @@ def _validateConnectivity(self) -> None: def _remapConnectivity(self) -> npt.NDArray: uniqueIndices = self._uniqueIndices - remap = np.full(len(uniqueIndices), -1, dtype=int) - remap[uniqueIndices] = np.arange(len(uniqueIndices)) - remappedConnectivity = remap[self.connectivity] + indexMap = {idx: i for i, idx in enumerate(uniqueIndices)} + remappedConnectivity = np.array([[indexMap[idx] for idx in row] for row in self._connectivity]) return remappedConnectivity From 99e04c72601c5aaf8ddb42814f842935db15d9d8 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Mon, 11 Nov 2024 21:00:45 -0500 Subject: [PATCH 25/28] Switched remap connectivity to numpy for sppeeeed --- baseclasses/utils/tecplotIO.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 7d3007d..4d3117e 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -409,10 +409,10 @@ def _validateConnectivity(self) -> None: raise TypeError("Invalid zone type.") def _remapConnectivity(self) -> npt.NDArray: - uniqueIndices = self._uniqueIndices - indexMap = {idx: i for i, idx in enumerate(uniqueIndices)} - remappedConnectivity = np.array([[indexMap[idx] for idx in row] for row in self._connectivity]) - return remappedConnectivity + # Create a remapping array that's as large as the maximum index + remap = np.full(self._uniqueIndices.max() + 1, -1, dtype=np.int64) + remap[self._uniqueIndices] = np.arange(len(self._uniqueIndices)) + return remap[self._connectivity] # ============================================================================== From 7fc9a08e51da8bb91c273933c00a6cafd35620ac Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Tue, 19 Nov 2024 11:37:23 -0500 Subject: [PATCH 26/28] Updgraded the zone name matching to be more robust --- baseclasses/utils/tecplotIO.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 4d3117e..3f009d7 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -1154,8 +1154,8 @@ def _readZoneHeader(self, lines: List[str], iCurrent: int) -> Tuple[Dict[str, An headerString = ", ".join(header) # Use regex to parse the header information - zoneNameMatch = re.search(r'zone t\s*=\s*"([^"]*)"', headerString, re.IGNORECASE) - zoneName = zoneNameMatch.group(1) if zoneNameMatch else None + zoneNameMatch = re.search(r'(zone t)\s*=\s*[\'""]?([^\'""\n,]+)[\'""]?(?=[,\n]|$)', headerString, re.IGNORECASE) + zoneName = zoneNameMatch.group(2) if zoneNameMatch else None zoneTypeMatch = re.search(r"zonetype\s*=\s*(\w+)", headerString, re.IGNORECASE) zoneType = zoneTypeMatch.group(1) if zoneTypeMatch else "ORDERED" From 512543f446868e46738f040926719cacaa492833 Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 2 May 2025 00:16:42 -0400 Subject: [PATCH 27/28] Fixing ordered zones i,j,k ordering bug --- baseclasses/utils/tecplotIO.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index 3f009d7..f8662d8 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -500,9 +500,9 @@ def writeData(self, handle: TextIO): data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) if self.datapacking == "POINT": - data = data.reshape(-1, len(self.zone.variables)) + data = data.reshape(-1, len(self.zone.variables), order="F") else: - data = data.reshape(-1, len(self.zone.variables)).T + data = data.reshape(-1, len(self.zone.variables), order="F").T writeArrayToFile(data, handle, maxLineWidth=4000, precision=self.fmtPrecision, separator=self.separator) @@ -860,7 +860,7 @@ def writeData(self, handle: TextIO): data = np.stack([self.zone.data[var] for var in self.zone.variables], axis=-1) # Flatten the data such that each variable is a row - data = data.reshape(-1, len(self.zone.variables)).T + data = data.reshape(-1, len(self.zone.variables), order="F").T _writeFloat32(handle, SectionMarkers.ZONE.value) # Write the zone marker @@ -916,9 +916,9 @@ def writeHeader(self, handle: TextIO): self._writeCommonHeader(handle) # Write the common header information # --- Specific to Ordered Zones --- - _writeInteger(handle, self.zone.iMax) # Write the I dimension + _writeInteger(handle, self.zone.iMax) # Write the K dimension _writeInteger(handle, self.zone.jMax) # Write the J dimension - _writeInteger(handle, self.zone.kMax) # Write the K dimension + _writeInteger(handle, self.zone.kMax) # Write the I dimension _writeInteger(handle, BinaryFlags.FALSE.value) # No aux data def writeFooter(self, handle: TextIO): From 036c57dacf8532774328e639615623856970b7ef Mon Sep 17 00:00:00 2001 From: Andrew Lamkin Date: Fri, 22 Aug 2025 17:16:40 -0400 Subject: [PATCH 28/28] Fixing small bug with header regex --- baseclasses/utils/tecplotIO.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/baseclasses/utils/tecplotIO.py b/baseclasses/utils/tecplotIO.py index f8662d8..5556021 100644 --- a/baseclasses/utils/tecplotIO.py +++ b/baseclasses/utils/tecplotIO.py @@ -372,6 +372,15 @@ def triConnectivity(self) -> npt.NDArray: else: raise TypeError(f"'triConnectivity' not supported for {self.zoneType.name} zone type.") + @property + def uniqueTriConnectivity(self) -> npt.NDArray: + if self.zoneType == ZoneType.FETRIANGLE: + return self.uniqueConnectivity + elif self.zoneType == ZoneType.FEQUADRILATERAL: + return np.row_stack((self.uniqueConnectivity[:, [0, 1, 2]], self.uniqueConnectivity[:, [0, 2, 3]])) + else: + raise TypeError(f"'uniqueTriConnectivity' not supported for {self.zoneType.name} zone type.") + @property def uniqueData(self) -> Dict[str, npt.NDArray]: return {var: self.data[var][self._uniqueIndices] for var in self.variables} @@ -1154,8 +1163,8 @@ def _readZoneHeader(self, lines: List[str], iCurrent: int) -> Tuple[Dict[str, An headerString = ", ".join(header) # Use regex to parse the header information - zoneNameMatch = re.search(r'(zone t)\s*=\s*[\'""]?([^\'""\n,]+)[\'""]?(?=[,\n]|$)', headerString, re.IGNORECASE) - zoneName = zoneNameMatch.group(2) if zoneNameMatch else None + zoneNameMatch = re.search(r'zone t\s*=\s*["\']([^"\']*)["\']', headerString, re.IGNORECASE) + zoneName = zoneNameMatch.group(1) if zoneNameMatch else None zoneTypeMatch = re.search(r"zonetype\s*=\s*(\w+)", headerString, re.IGNORECASE) zoneType = zoneTypeMatch.group(1) if zoneTypeMatch else "ORDERED"