Source code for fileread

#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##

"""Read geometry from files in a number of formats.

This module defines basic routines to read geometrical data
from a file and the specialized importers to read files in a number of
well known standardized formats.
"""
import re
import numpy as np

import pyformex as pf
import pyformex.arraytools as at
from pyformex import utils
from pyformex.path import Path
from pyformex.varray import Varray
from pyformex.connectivity import Connectivity
from pyformex.coords import Coords
from pyformex.mesh import Mesh
from pyformex.pzffile import loadPZF as readPZF

__all__ = ['readPZF', 'readPGF', 'readOFF', 'readOBJ', 'readPLY', 'readGTS',
           'readSTL']


[docs]def readPGF(filename, count=-1): """Read a pyFormex Geometry File. A pyFormex Geometry File can store multiple geometrical objects in a native format that can be efficiently read back into pyformex. The format is portable over different pyFormex versions and even to other software, as the format is stable and documented. This reader can restore subclasses of :class:`Geometry`: :class:`Formex`, :class:`Mesh`, :class:`TriSurface`, :class:`PolyLine`, :class:`BezierSpline`, :class:`NurbsCurve`, with their stored attributes (name, color, field variables). Parameters ---------- filename: :term:`path_like` The name of an existing pyFormex Geometry File. If the filename ends on '.gz' or '.bz2', it is considered to be a gzipped, resp. bzipped file and it will transparently be uncompressed during reading. count: int, optional If specified, no more than this number of objects will be read. Returns ------- dict A dictionary with the geometric objects read from the file. If the file contains the object names, these are used as the keys. Else, default names are provided. If the file contains attributes (colors, fields) for an object, these will also be restored. """ from pyformex import geomfile filename = Path(filename) pf.verbose(1, f"Read PGF file {filename.absolute()}") f = geomfile.GeometryFile(filename, 'r') objects = f.read(count) pf.verbose(2, f"Got {len(objects)} objects") return objects
[docs]def readOFF(filename): """Read polygon meshes from an OFF file. Parameters ---------- filename: :term:`path_like` The name of a file in OFF format, commonly having a suffix '.off'. If the name ends with '.off.gz' or '.off.bz2', then the file will transparently be uncompressed during reading. mplex: int, optional The maximum plexitude of the output polygons. If provided, polygons with a plexitude higher that mplex will be split into smaller ones. For example, with mplex=4, polygons with 5 or more vertices will be split into quads and triangles. Likewise, mplex=3 will split all polygons into triangles. Returns ------- Polygons A :class:`Polygons` with the polygons read from the file. Examples -------- >>> from .filewrite import writeOFF >>> f = Path('test_filewrite.off') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeOFF(f, M) >>> poly = readOFF(f) >>> print(poly) Polygons: nnodes: 4, nelems: 2, nplex: min 3, max 3, eltype: polygon BBox: [0. 0. 0.], [1. 1. 0.] Size: [1. 1. 0.] >>> print(poly.coords) [[0. 0. 0.] [1. 0. 0.] [1. 1. 0.] [0. 1. 0.]] >>> print(poly.elems) Varray (nrows=2, width=3..3) [0 1 2] [2 3 0] """ from pyformex.polygons import Polygons filename = Path(filename) pf.verbose(1, f"Read OFF file {filename.absolute()}") class STAGE(): INIT, HEADER, COORDS, ELEMS, EDGES = range(5) coords = None faces = [] stage = STAGE.INIT name = None with utils.File(filename, 'r') as fil: for line in fil: comment = line.find('#') if comment >= 0: m = re.match(r'# name=(\w+)', line, flags=re.A) if m is not None: name = m.group(1) continue line = line[:comment] if not line: continue line = line.strip() if not line: continue if stage == STAGE.INIT: stage += 1 if "OFF" in line: if line != 'OFF': raise ValueError( f"I can not yet read {line} files, only OFF!") continue if stage == STAGE.HEADER: nnodes, nelems, nedges = [int(i) for i in line.split()] pf.verbose(2, f" {nnodes} vertices, {nelems} faces, {nedges} edges") stage += 1 # Prepare for reading nodes coords = np.empty((nnodes, 3), dtype=at.Float) i = 0 elif stage == STAGE.COORDS: s = line.split() coords[i] = [float(si) for si in s] i += 1 if i >= nnodes: stage += 1 i = 0 elif stage == STAGE.ELEMS: s = line.split() n = int(s[0]) faces.append([int(si) for si in s[1:n+1]]) i += 1 if i >= nelems: stage += 1 i = 0 elif stage == STAGE.EDGES: pf.verbose(2, "Reading of edges is not yet implemented") if coords is None or not faces: raise ValueError("Could not read any vertices and/or faces") poly = Polygons(Coords(coords), faces) if name: poly.attrib(name=name) pf.verbose(2, f"Got {poly}") return poly
[docs]def readOBJ(filename): """Read a mesh from an OBJ file. Reads the mesh data from a wavefront .obj file. Parameters ---------- filename: :term:`path_like` The name of a file in OBJ format, commonly having a suffix '.obj'. If the name ends with '.obj.gz' or '.obj.bz2', the file will transparently be uncompressed during reading. Returns ------- Polygons The polygons read from the .obj file. Notes ----- Currently only handles polygons. It does not handle relative indexing, subobjects, groups, lines, splines, beziers, materials. Normals and texture coordinates are read but not returned. Examples -------- >>> from .filewrite import writeOBJ >>> f = Path('test_filewrite.obj') >>> M = Mesh(eltype='quad4') >>> writeOBJ(f, M) >>> poly = readOBJ(f) >>> print(poly) Polygons: nnodes: 4, nelems: 1, nplex: min 4, max 4, eltype: polygon BBox: [0. 0. 0.], [1. 1. 0.] Size: [1. 1. 0.] >>> print(poly.coords) [[0. 0. 0.] [1. 0. 0.] [1. 1. 0.] [0. 1. 0.]] >>> print(poly.elems) Varray (nrows=1, width=4..4) [0 1 2 3] """ from pyformex.polygons import Polygons def vertex_data(s): t = s.split('/') vid = int(t[0]) tid = int(t[1]) if len(t) > 1 and t[1] else -1 nid = int(t[2]) if len(t) > 2 else -1 return vid, tid, nid filename = Path(filename) pf.verbose(1, f"Read OBJ file {filename.absolute()}") # storage for coords, normals, texture coords coords = {'': [], 'n': [], 't': []} # storage for faces and edges faces = [] # lines = [] name = None with utils.File(filename, 'r') as fil: for line in fil: s = line.split() if len(s) == 0: continue typ, *data = s if typ[0] == 'v': coords[typ[1:2]].append([float(d) for d in data]) elif typ == 'f': faces.append([vertex_data(d)[0] for d in data]) elif typ == 'o': name = s[1] faces = Varray(faces) faces.data -= 1 # OBJ format starts at 1 try: poly = Polygons(Coords(coords['']), faces) except Exception: raise ValueError("This file is too complex for our current .OBJ reader") if coords['n']: normals = Coords(coords['n']) if normals.shape == poly.coords.shape: poly._memory['avg_normals'] = normals if coords['t']: texture = Coords(coords['t']) if texture.shape == poly.coords.shape: poly._memory['tex_coords'] = texture if name: poly.attrib(name=name) pf.verbose(2, f"Got {poly}") return poly
[docs]def readPLY(filename, check=True): """Read polygons from a PLY file. Reads the polygon data from a stanford .ply file and possibly splits the high plexitude polygons into smaller ones. Parameters ---------- filename: :term:`path_like` The name of a file in PLY format, commonly having a suffix '.ply'. Ascii as well as binary formats are supported If the name ends with '.ply.gz' or '.ply.bz2', the file will transparently be uncompressed during reading. Returns ------- Polygons The polygons read from the PLY file. Notes ----- This uses plyfile from https://github.com/dranjan/python-plyfile to read the PLY file. Examples -------- >>> from .filewrite import writePLY >>> f = Path('test_filewrite.ply') >>> M = Mesh(eltype='quad4') >>> writePLY(f, M) >>> poly = readPLY(f) >>> print(poly) Polygons: nnodes: 4, nelems: 1, nplex: min 4, max 4, eltype: polygon BBox: [0. 0. 0.], [1. 1. 0.] Size: [1. 1. 0.] >>> print(poly.coords) [[0. 0. 0.] [1. 0. 0.] [1. 1. 0.] [0. 1. 0.]] >>> print(poly.elems) Varray (nrows=1, width=4..4) [0 1 2 3] >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writePLY(f, M, binary=True) >>> P = readPLY(f, check=False) >>> print(P) Polygons: nnodes: 4, nelems: 2, nplex: min 3, max 3, eltype: polygon BBox: [0. 0. 0.], [1. 1. 0.] Size: [1. 1. 0.] >>> print(P.coords) [[0. 0. 0.] [1. 0. 0.] [1. 1. 0.] [0. 1. 0.]] >>> print(P.elems) Varray (nrows=2, width=3..3) [0 1 2] [2 3 0] """ from pyformex.polygons import Polygons from pyformex.plugins.plyfile import PlyData filename = Path(filename) pf.verbose(1, f"Read PLY file {filename.absolute()}") with utils.File(filename, 'rb') as fil: ply_data = PlyData.read(fil) name = None for line in ply_data._get_comments(): m = re.match(r'name=(\w+)', line, flags=re.A) if m is not None: name = m.group(1) vertices = ply_data['vertex'] faces = ply_data['face'] # Point coordinates coords = Coords(np.stack([ vertices['x'], vertices['y'], vertices['z'], ], axis=1)) # Vertex indices vertex_indices = faces['vertex_indices'] # Polygons poly = Polygons(coords, vertex_indices, check=check) if name: poly.attrib(name=name) pf.verbose(2, f"Got {poly}") return poly
[docs]def readGTS(filename): """Read a surface mesh from a GTS file. Parameters ---------- filename: :term:`path_like` The name of a file in GTS format, commonly having a suffix '.gts'. If the name ends with '.gts.gz' or '.gts.bz2', then the file will transparently be uncompressed during reading. Returns ------- coords: float array (ncoords, 3) The coordinates of all vertices. edges: int array (nedges,2) The edges to nodes connectivity table. faces: int array (nfaces,2) The faces to edges connectivity table. Examples -------- >>> from .filewrite import writeGTS >>> f = Path('test_filewrite.gts') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeGTS(f, M.toSurface()) >>> coords, edges, faces = readGTS(f) >>> print(coords) [[0. 0. 0.] [1. 0. 0.] [1. 1. 0.] [0. 1. 0.]] >>> print(edges) [[0 1] [2 0] [3 0] [1 2] [2 3]] >>> print(faces) [[0 3 1] [4 2 1]] """ filename = Path(filename) pf.verbose(1, f"Read GTS file {filename.absolute()}") with utils.File(filename, 'r') as fil: header = fil.readline().split() ncoords, nedges, nfaces = [int(i) for i in header[:3]] if len(header) >= 7 and header[6].endswith('Binary'): sep = '' raise RuntimeError( "We can not read binary GTS format yet. " "See https://savannah.nongnu.org/bugs/index.php?38608. " "Maybe you should recompile the extra/gts commands." ) else: sep = ' ' coords = np.fromfile( fil, dtype=at.Float, count=3 * ncoords, sep=sep ).reshape(-1, 3) edges = np.fromfile( fil, dtype=np.int32, count=2 * nedges, sep=' ' ).reshape(-1, 2) - 1 faces = np.fromfile( fil, dtype=np.int32, count=3 * nfaces, sep=' ' ).reshape(-1, 3) - 1 pf.verbose(2, f"Got {ncoords} coords, {nedges} edges, {nfaces} faces") if coords.shape[0] != ncoords or \ edges.shape[0] != nedges or \ faces.shape[0] != nfaces: pf.verbose(1, "Error reading GTS file! Result may be incomplete.") return coords, edges, faces
[docs]def readSTL(filename): """Read a surface mesh from an STL file. Parameters ---------- filename: :term:`path_like` The name of a file in STL format, commonly having a suffix '.stl'. If the name ends with '.gz' or '.bz2', then the file will transparently be uncompressed during reading. Returns ------- coords: float array (ncoords, 3) The coordinates of all vertices. edges: int array (nedges,2) The edges to nodes connectivity table. faces: int array (nfaces,2) The faces to edges connectivity table. Notes ----- STL files come in ascii and binary formats. As there is no simple way to detect the format, a binary read is tried first, and if unsuccessful, the ascii read is tried next. Examples -------- >>> from .filewrite import writeSTL >>> f = Path('test_filewrite.stl') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeSTL(f, M.toFormex().coords, binary=True, color=[255,0,0,128]) >>> coords, normals, color = readSTL(f) >>> print(coords) [[[0. 0. 0.] [1. 0. 0.] [1. 1. 0.]] <BLANKLINE> [[1. 1. 0.] [0. 1. 0.] [0. 0. 0.]]] >>> print(normals) [[0. 0. 1.] [0. 0. 1.]] >>> print(color) (1.0, 0.0, 0.0) >>> writeSTL(f, M.toFormex().coords, binary=False) >>> coords, normals, color = readSTL(f) >>> print(coords) [[[0. 0. 0.] [1. 0. 0.] [1. 1. 0.]] <BLANKLINE> [[1. 1. 0.] [0. 1. 0.] [0. 0. 0.]]] >>> print(normals) [[0. 0. 1.] [0. 0. 1.]] """ filename = Path(filename) pf.verbose(1, f"Read STL file {filename.absolute()}") with utils.File(filename, 'rb') as fil: head = fil.read(5) asc = head[:5] == b'solid' fil.seek(0) pf.verbose(2, f"Reading {'ascii' if asc else 'binary'} STL file") if asc: coords, normals = read_stl_asc(fil) color = None else: coords, normals, color = read_stl_bin(fil) pf.verbose(2, f"Got {coords.shape[0]} triangles") return coords, normals, color
[docs]def read_stl_bin(fil): """Read a binary STL file. Note ---- This is a low level routine for use in readSTL. It is not intended to be used directly. Parameters ---------- fil: open file File opened in binary read mode, holding binary STL data. Returns ------- coords: Coords (ntri,3,3) A Coords with ntri triangles. Each triangle consists of 3 vertices. normals: Coords (ntri,3) A Coords with ntri vectors: the outer normals on the triangles. color: None | float array (3,) If the STL file header contained a color in Materialise (TM) format, the RGB color components are returned as OpenGL color components. The alpha value is currently not returned. """ def addTriangle(i): x[i] = np.fromfile(file=fil, dtype=at.Float, count=12).reshape(4, 3) fil.read(2) head = fil.read(80) i = head.find(b'COLOR=') if i >= 0 and i <= 70: color = np.frombuffer(head[i + 6:i + 10], dtype=np.uint8, count=4) else: color = None ntri = np.fromfile(fil, dtype=at.Int, count=1)[0] x = np.zeros((ntri, 4, 3), dtype=at.Float) # nbytes = 12*4 + 2 [addTriangle(it) for it in range(ntri)] normals = Coords(x[:, 0]) coords = Coords(x[:, 1:]) if color is not None: from pyformex import colors color = colors.GLcolor(color[:3]) return coords, normals, color
[docs]def read_stl_asc(fil): """Read an ascii STL file. Note ---- This is a low level routine for use in readSTL. It is not intended to be used directly. Parameters ---------- fil: open file File opened in binary read mode, holding ascii STL data. Returns ------- coords: Coords (ntri,3,3) A Coords with ntri triangles. Each triangle consists of 3 vertices. normals: Coords (ntri,3) A Coords with ntri vectors: the outer normals on the triangles. """ # different line modes and the corresponding re's solid, facet, outer, vertex, endloop, endfacet, endsolid = range(7) _re_solid = re.compile(b"\\s*solid\\s+.*") _re_facet = re.compile(b"\\s*facet\\s+normal\\s+(?P<data>.*)") _re_outer = re.compile(b"\\s*outer\\s+loop\\s*") _re_vertex = re.compile(b"\\s*vertex\\s+(?P<data>.*)") _re_endloop = re.compile(b"\\s*endloop\\s*") _re_endfacet = re.compile(b"\\s*endfacet\\s*") _re_endsolid = re.compile(b"\\s*endsolid\\s*") _re_expect = { solid: _re_solid, facet: _re_facet, outer: _re_outer, vertex: _re_vertex, endloop: _re_endloop, endfacet: _re_endfacet, } # place to store the results normals = [] coords = [] def getdata(s): """Get 3 floats from a string""" data = [float(i) for i in s.split()] if len(data) != 3: raise ValueError("Expected 3 float values") return data mode = solid facets = 0 nverts = 0 count = 0 for line in fil: count += 1 m = _re_expect[mode].match(line) if m is None: if mode == facet and _re_endsolid.match(line): # We reached the end break else: raise ValueError(f"Invalid input on line {count}:\n{line}") if mode == vertex: try: data = getdata(m.group(1)) except Exception: raise ValueError(f"Data error in line {count}: \n{line}") nverts += 1 coords.append(data) if nverts == 3: mode = endloop elif mode == facet: try: data = getdata(m.group(1)) except Exception: raise ValueError(f"Data error in line {count}: \n{line}") normals.append(data) mode = outer elif mode == outer: nverts = 0 mode = vertex elif mode == endloop: mode = endfacet elif mode == endfacet: facets += 1 mode = facet elif mode == solid: if count != 1: raise ValueError(f"'solid' found on line {count}") mode = facet else: raise ValueError(f"Invalid input on line {count}") coords = Coords(coords).reshape(-1, 3, 3) normals = Coords(normals) return coords, normals
# TODO: this should unzip compressed input files and zip compressed output # TODO: remove the default outname and warn if not specified
[docs]def stlConvert(stlname, outname=None, binary=False, options='-d'): """Convert an .stl file to .off or .gts or binary .stl format. Parameters ---------- stlname: :term:`path_like` Name of an existing .stl file (either ascii or binary). outname: str or Path Name or suffix of the output file. The suffix defines the format and should be one of '.off', '.gts', '.stl', '.stla', or .stlb'. If a suffix only is given (other than '.stl'), then the outname will be constructed by changing the suffix of the input ``stlname``. If not specified, the suffix of the configuration variable 'surface/stlread' is used. binary: bool Only used if the extension of ``outname`` is '.stl'. Defines whether the output format is a binary or ascii STL format. options: str Returns ------- outname: Path The name of the output file. status: int The exit status (0 if successful) of the conversion program. stdout: str The output of running the conversion program or a 'file is already up to date' message. Notes ----- If the outname file exists and its mtime is more recent than the stlname, the outname file is considered up to date and the conversion program will not be run. The conversion program will be choosen depending on the extension. This uses the external commands 'admesh' or 'stl2gts'. """ stlname = Path(stlname) if outname is None: outname = pf.cfg['surface/stlread'] outname = Path(outname) if outname.suffix == '' and outname.name.startswith('.'): # It is considered as a suffix only outname = stlname.with_suffix(outname) if outname.resolve() == stlname.resolve(): raise ValueError("stlname and outname resolve to the same file") if outname.exists() and stlname.mtime < outname.mtime: return outname, 0, "File '{outname}' seems to be up to date" ftype = outname.ftype if ftype == 'stl' and binary: ftype = 'stlb' if ftype in ['off', 'stl', 'stla', 'stlb']: utils.External.has('admesh') cmdopt = {'off': '--write-off', 'stlb': '-b'}.get(ftype, '-a') cmd = f"admesh {options} {cmdopt} '{outname}' '{stlname}'" elif ftype == 'gts': cmd = f"stl2gts < '{stlname}' > '{outname}'" else: return outname, 1, f"Can not convert file '{stlname}' to '{outname}'" P = utils.command(cmd, shell=True) return outname, P.returncode, P.stdout
# Global variables used by some functions filename = None mode = None nplex = None offset = None
[docs]def getParams(line): """Strip the parameters from a comment line""" s = line.split() d = {'mode': s.pop(0), 'filename': s.pop(0)} d.update(zip(s[::2], s[1::2])) return d
[docs]def readNodes(fil): """Read a set of nodes from an open mesh file""" a = np.fromfile(fil, sep=" ").reshape(-1, 3) x = Coords(a) return x
[docs]def readElems(fil, nplex): """Read a set of elems of plexitude nplex from an open mesh file""" print(f"Reading elements of plexitude {nplex}") e = np.fromfile(fil, sep=" ", dtype=at.Int).reshape(-1, nplex) e = Connectivity(e) return e
[docs]def readEsets(fil): """Read the eset data of type generate""" data = [] for line in fil: s = line.strip('\n').split() if len(s) == 4: data.append(s[:1] + [int(i) for i in s[1:]]) return data
[docs]def readMeshFile(filename): """Read a nodes/elems model from file. Returns a dict: - 'coords': a Coords with all nodes - 'elems': a list of Connectivities - 'esets': a list of element sets """ d = {} fil = open(filename, 'r') for line in fil: if line[0] == '#': line = line[1:] globals().update(getParams(line)) dfil = open(filename, 'r') if mode == 'nodes': d['coords'] = readNodes(dfil) elif mode == 'elems': elems = d.setdefault('elems', []) e = readElems(dfil, int(nplex)) - int(offset) elems.append(e) elif mode == 'esets': d['esets'] = readEsets(dfil) else: print(f"Skipping unrecognized line: {line}") dfil.close() fil.close() return d
[docs]def extractMeshes(d): """Extract the Meshes read from a .mesh file. """ x = d['coords'] e = d['elems'] M = [Mesh(x, ei) for ei in e] return M
[docs]def convertInp(filename): """Convert an Abaqus .inp to a .mesh set of files""" filename = Path(filename).resolve() converter = pf.cfg['bindir'] / 'read_abq_inp.awk' cmd = f"cd {filename.parent}; awk -f {converter} {filename.name}" print(cmd) P = utils.command(cmd, shell=True) print(P.stdout) print(P.stderr)
######## Abaqus/Calculix INP format (.inp) ##########
[docs]def readINP(filename, tempdir=None): """Read the geometry from an Abaqus/Calculix .inp file This uses :func:`plugins.ccxinp.readInpFile` to import the Finite Element meshes from an Abaqus or Calculix INP format file. Parameters ---------- filename: :term:`path_like` The path of the input file, usually with a .inp suffix. tempdir: :term:`path_like`, optional The path of a directory where intermediary results can be stored. If provided but not existent, it will be created. If not provided, a temporary directory is used and intermediary data are not saved. Returns ------- dict A dict where the keys are part names and the values are :class:`FeModel` instances. If the .inp file does not contain parts, default part names are generated. Warning ------- Element blocks with an unrecognized element type will raise an exception, unless :attr:`plugins.ccxinp.skip_unknown_eltype` is set to True. """ from pyformex.plugins import ccxinp from pyformex.plugins import fe model = ccxinp.readInpFile(filename, tempdir=tempdir) print(f"Number of parts: {len(model.parts)}") fem = {} for part in model.parts: try: coords = Coords(part['coords']) nodid = part['nodid'] nodpos = at.inverseUniqueIndex(nodid) print(f"nnodes = {coords.shape[0]}") print(f"nodid: {nodid}") print(f"nodpos: {nodpos}") elems = [] for t, e in part['elems']: try: print(f"Converting elements of type {t}") el = nodpos[e] els = Connectivity(nodpos[e], eltype=t) elems.append(els) except Exception: print(f"Conversion failed for elements of type {t}") print(f"Orig els {e}") print(f"Trl els {el}") print(f"ELEM TYPES: {[e.eltype for e in elems]}") fem[part['name']] = fe.Model(coords, elems) except Exception as e: print(f"Skipping part {part['name']}") print(e) return fem
######## Gambit neutral file (.neu) ########## # convert Gambit neutral node order to pyFormex order neu_pyf_order = { 4: (0, 1, 3, 2, 4, 5, 7, 6), }
[docs]def readNEU(filename, return_numbering=False): """Read a Mesh from a Gambit neutral file. Parameters ---------- filename: :term:`path_like` The name of a file in Gambit neutral format, commonly having a suffix '.neu'. If the name ends with '.gz' or '.bz2', the file will transparently be uncompressed during reading. return_numbering: bool If True, also returns the original node and element numbers as found in the file. The internal pyFormex numbers are always in order of appearance in the file. Returns ------- Mesh The Mesh read from the Gambit neutral file, if reading was successful. None if the file could not be read. If the .neu file contains groups, the Mesh will have prop values equal to the material type numbers in the file. nrs: int array The node numbers in the .neu file corrsponding to the nodes in Mesh.coords. Only returned if return_numbering is True. enrs: in array The element numbers in the .neu file corresponding to the elements in Mesh.elems. Only returned if return_numbering is True. Notes ----- Currently this function can only read Gambit files where all cells are of the same element type. Examples -------- >>> from pyformex.plugins.neu_exp import writeNEU >>> f = Path('test_filewrite.neu') >>> M = Mesh(eltype='hex8').subdivide(3) >>> writeNEU(f, M) >>> M = readNEU(f) Successfully read test_filewrite.neu ncoords=64; nelems=27; nplex=8; eltype=Hex8 >>> print(M) Mesh: nnodes: 64, nelems: 27, nplex: 8, level: 3, eltype: hex8 BBox: [0. 0. 0.], [1. 1. 1.] Size: [1. 1. 1.] Area: 6.0 Volume: 1.0 """ from pyformex.plugins.neu_exp import pyf_neu_eltype, neu_pyf_order neu_pyf_eltype = utils.inverseDict(pyf_neu_eltype) def find_line(fil, pat): for line in fil: if line.split()[:len(pat)] == pat: return True def read_coords(fil, ncoords): nrs = np.full((ncoords,), -1, dtype=at.Int) coords = np.empty((ncoords, 3), dtype=at.Float) for i in range(ncoords): line = next(fil) s = line.split() nrs[i] = int(s[0]) coords[i] = [float(si) for si in s[1:4]] return nrs, coords def read_elems(fil, nelems): enrs = np.full((nelems,), -1, dtype=at.Int) for i in range(nelems): line = next(fil) s = line.split() enrs[i] = int(s[0]) if i == 0: eltyp = int(s[1]) nplex = int(s[2]) elems = np.empty((nelems, nplex), dtype=at.Int) else: if eltyp != int(s[1]) or nplex != int(s[2]): raise ValueError( "Currently I can not handle .neu files with more " "than 1 element type") s = s[3:] while len(s) < nplex: s.extend(next(fil).split()) elems[i] = [int(si) for si in s] return eltyp, enrs, elems def read_group(fil): s = next(fil).split() group, nels, matnr, nflags = [int(i) for i in s[1::2]] next(fil) # groupname next(fil) # flags values = [] for i in range((nels+9) // 10): values.extend([int(si) for si in next(fil).split()]) return matnr, np.array(values, dtype=at.Int) filename = Path(filename) pf.verbose(1, f"Read NEU file {filename.absolute()}") with utils.File(filename, 'r') as fil: if find_line(fil, ['NUMNP']): # read the parameters line = next(fil) s = line.split() ncoords = int(s[0]) nelems = int(s[1]) if find_line(fil, ['NODAL', 'COORDINATES']): # read the nodal coordinates nrs, coords = read_coords(fil, ncoords) line = next(fil) if find_line(fil, ['ELEMENTS/CELLS']): # read the elements eltyp, enrs, elems = read_elems(fil, nelems) nplex = elems.shape[-1] elname = neu_pyf_eltype.get((eltyp, nplex), None) if elname is None: raise ValueError( f"I can not convert .neu files with elements" f" of type {eltyp} and plexitude {nplex}") # Translate node numbers to in-order coordinates inv = at.inverseUniqueIndex(nrs) elems = inv[elems] if elname in neu_pyf_order: # Reorder the nodes to our element node numbering scheme elems = elems[:, neu_pyf_order[elname]] M = Mesh(coords, elems, eltype=elname) # print Read groups while find_line(fil, ['ELEMENT', 'GROUP']): # read an element group p, els = read_group(fil) if M.prop is None: M.setProp(0) M.prop[els] = p print(f"Successfully read {filename}") print(f" ncoords={ncoords}; nelems={M.nelems()}; " f" nplex={M.nplex()}; eltype={M.elType()}") if return_numbering: return M, nrs, enrs else: return M print(f"Error while reading {filename}")
# End