#
##
##  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 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