Source code for filewrite

#
##
##  SPDX-FileCopyrightText: © 2007-2021 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.0  (Mon Nov 22 14:32:59 CET 2021)
##  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/.
##

"""Write geometry to file in a whole number of formats.

This module defines both the basic routines to write geometrical data
to a file and the specialized exporters to write files in a number of
well known standardized formats.

Most of the exporters support transparent compression of the output files
by just specifying a file name that ends in '.gz' or '.bz2'.

The basic routines are very versatile as well as optimized and allow to
easily create new exporters for other formats.
"""

import sys
import datetime

import numpy as np

import pyformex as pf
from pyformex.path import Path
from pyformex.coords import Coords
from pyformex.formex import Formex
from pyformex.mesh import Mesh    # noqa: F401    (used in doctests)
from pyformex import arraytools as at
from pyformex import utils

from pyformex.plugins.neu_exp import writeNEU


__all__ = ['writePGF', 'writeOFF', 'writeOBJ', 'writePLY', 'writeGTS',
           'writeSTL', 'writeNEU', 'writeData', 'writeIData']


[docs]def writePGF(filename, objects, sep=' ', mode='w', shortlines=False, **kargs): """Save geometric objects to 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. Parameters ---------- filename: :term:`path_like` The name of the file to be written. Usually this has a suffix '.pgf'. If a '.gz' or '.bz2' is added, the file will be compressed with gzip or bzip2, respectively. objects: object | list | dict One or more objects to be saved on the file. If it is a dictionary, the keys are saved in the file as the names of the objects. Objects that can not be exported to a pyFormex Geometry File are silently ignored. mode: 'w' | 'a' The mode in which to open the file. The default 'w' will overwrite an existing file. Use 'a' to append to an existing file. sep: str The string used to separate data. If set to an empty string, the data will be written in binary format and the resulting file will be smaller, but less portable. **kargs: Extra keyword arguments are passed to :meth:`~geomfile.GeometryFile.write`. Returns ------- int: The number of objects that were written to the file. Examples -------- >>> f = Path('test_filewrite.pgf') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writePGF(f, M) 1 >>> print(f.read_text()) # pyFormex Geometry File (http://pyformex.org) version='2.1'; sep=' ' # objtype='Mesh'; ncoords=4; nelems=2; nplex=3; props=False; normals=False; color=None; sep=' '; name='test_filewrite-0'; eltype='tri3' 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0 0.0 0 1 2 2 3 0 <BLANKLINE> >>> f.remove() """ from pyformex import geomfile pf.debug(f"WriteGeomFile filename{filename}; sep='{sep}'; " f"shortlines={shortlines}; kargs={kargs}", pf.DEBUG.PGF) filename = Path(filename) pf.verbose(1, f"Write PGF file {filename.absolute()}") f = geomfile.GeometryFile(filename, mode, sep=sep, **kargs) # TODO: shis option could goto into GeometryFile if shortlines: f.fmt = {'i': '%i ', 'f': '%f '} nobj = f.write(objects) f.close() return nobj
[docs]def writeOFF(fn, mesh): """Write a mesh of polygons to a file in OFF format. Parameters ---------- fn: :term:`path_like` The output file name, commonly having a suffix '.off'. If the suffix ends on '.gz' or '.bz2', the file will transparently be compressed during writing. mesh: Mesh The Mesh to write to the file. Notes ----- See https://en.wikipedia.org/wiki/OFF_(file_format). Examples -------- >>> f = Path('test_filewrite.off') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeOFF(f, M) >>> print(f.read_text()) OFF 4 2 0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0 0.0 3 0 1 2 3 2 3 0 <BLANKLINE> >>> f.remove() """ if not mesh.elName() in ['line2', 'tri3', 'quad4']: raise ValueError( "Can only write Line2, Tri3 or Quad4 meshes to OFF format") fn = Path(fn) pf.verbose(1, f"Write OFF file {fn}") coords = mesh.coords elems = mesh.elems ncoords = coords.shape[0] nfaces, nplex = elems.shape nedges = 0 if mesh.nplex() == 2: # write as edges, faces nfaces, nedges = nedges, nfaces with utils.File(fn, 'wb') as fil: writeText(fil, "OFF\n") # don't write comments: we can't read it yet # fil.write("# OFF file written by %s\n" % pf.Version()) writeText(fil, f"{ncoords} {nfaces} {nedges}\n") writeData(fil, coords, fmt='%s', sep=' ') plex = np.full_like(elems[:, :1], nplex) elemdata = np.column_stack([plex, elems]) writeData(fil, elemdata, fmt='%i', sep=' ') pf.verbose(2, f"File size: {fn.size} bytes")
[docs]def writeOBJ(fn, mesh, name=None): """Write a mesh of polygons to a file in OBJ format. Parameters ---------- fn: :term:`path_like` The output file name, commonly having a suffix '.obj'. If the suffix ends on '.gz' or '.bz2', the file will transparently be compressed during writing. mesh: :class:`Mesh` The Mesh to write to the file. name: str, optional Name of the Mesh to be written into the file. If not provided None and the Mesh has an .attrib.name, that name will be used. Notes ----- See https://en.wikipedia.org/wiki/OBJ_(file_format). Examples -------- >>> f = Path('test_filewrite.obj') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeOBJ(f, M, name='test') >>> print(f.read_text()) # OBJ file written by pyFormex ... o test v 0.0 0.0 0.0 v 1.0 0.0 0.0 v 1.0 1.0 0.0 v 0.0 1.0 0.0 f 1 2 3 f 3 4 1 # End <BLANKLINE> >>> f.remove() """ fn = Path(fn) pf.verbose(1, f"Write OBJ file {fn.absolute()}") coords = mesh.coords elems = mesh.elems if name is None and hasattr(mesh, 'attrib'): name = mesh.attrib.name with utils.File(fn, 'w') as fil: fil.write("# OBJ file written by %s\n" % pf.Version()) if name is not None: fil.write("o %s\n" % str(name)) for v in coords: fil.write("v %s %s %s\n" % tuple(v)) # element code: p(oint), l(ine) or f(ace) nplex = mesh.nplex() code = {1: 'p', 2: 'l'}.get(nplex, 'f') s = code+(' %s'*nplex)+'\n' for e in elems+1: # .obj format starts at 1 fil.write(s % tuple(e)) fil.write('# End\n') pf.verbose(2, f"File size: {fn.size} bytes")
[docs]def writePLY(fn, coords, faces=None, *, edges=False, comment=None, vcolor=None, fcolor=None, ecolor=None, binary=False): """Write polygons to a file in PLY format. Parameters ---------- fn: :term:`path_like` The output file name, commonly having a suffix '.ply'. If the suffix ends on '.gz' or '.bz2', the file will transparently be compressed during writing. coords: :term:`coords_like` | :class:`Mesh` A float array (ncoords, 3) with the coordinates of all vertices. As a convenience, if a :class:`Mesh` is specified, the coords and faces arguments are taken from the :attr:`Mesh.coords` and :`Mesh.elems` attributes. faces: dict A dict where the keys are plexitudes (<= mplex) and the values are the Connectivity tables of all the polygons of the plexitude. This should not be provided if a Mesh was given as the coords argument. comment: str, optional An extra comment to add in the file header. binary: bool If True, a binary file is written. The default is ascii. Notes ----- For PLY format, see `<https://en.wikipedia.org/wiki/PLY_(file_format)>`_). Examples -------- >>> f = Path('test_filewrite.ply') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writePLY(f, M.coords, {3:M.elems}, comment='This is a test') >>> print(f.read_text()) ply format ascii 1.0 comment .ply file written by pyFormex ... comment This is a test element vertex 4 property float x property float y property float z element face 2 property list int int vertex_indices end_header 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0 1.0 0.0 3 0 1 2 3 2 3 0 <BLANKLINE> >>> f.remove() """ def int_color(color): return (255*vcolor).astype(Int).clip(0,255) if isinstance(coords, Mesh): coords, faces = coords.coords, {coords.nplex():coords.elems} else: coords = Coords(coords) if not isinstance(faces, dict): raise ValueError("writePLY: faces should be a dict") for nplex in faces: if faces[nplex].shape[1] != nplex: raise ValueError(f"faces[{nplex}]: plexitude does not match the key") fn = Path(fn) pf.verbose(1, f"Write PLY file {fn.absolute()}") now = datetime.datetime.now().isoformat() if binary: fmt = f"binary_{sys.byteorder}_endian 1.0" else: fmt = "ascii 1.0" header = [ "ply", f"format {fmt}", f"comment .ply file written by {pf.Version()} on {now}" ] if comment is not None: header.append(f"comment {comment}") cprop_type = 'uchar' color_type = [f"property {cprop_type} {color}" for color in ['red', 'green', 'blue']] # vertices ncoords = coords.shape[0] header.extend([f"element vertex {ncoords}", "property float x", "property float y", "property float z", ]) if vcolor: header.extend(color_type) vcolor = int_color(vcolor) # faces nelems = sum([faces[nplex].shape[0] for nplex in faces]) header.extend([f"element face {nelems}", "property list int int vertex_indices", ]) if fcolor: header.extend(color_type) fcolor = int_color(fcolor) # edges if edges: edges = [Mesh(coords, faces[nplex]).getEdges() for nplex in faces] edges = np.concatenate(edges) nedges = edges.shape[0] header.append("element edge {nedges}") header.extend(["property int vertex{%i}" for i in (1,2)]) if ecolor: header.append(color_type) ecolor = int_color(ecolor) # end header header.append("end_header\n") header = '\n'.join(header) #print(header) with utils.File(fn, 'wb') as fil: fil.write(header.encode('utf-8')) pf.verbose(2, f"Write {ncoords} vertices") if binary: print(coords.shape) print(coords.dtype) fil.write(coords.tobytes()) else: writeData(fil, coords, fmt='%s', sep=' ') for nplex in faces: elems = faces[nplex] nelems = np.full_like(elems[:, :1], nplex) pf.verbose(2, f"Write {elems.shape[0]} faces of plexitude {nplex}") elemdata = np.column_stack([nelems, elems]) if binary: print(elemdata.shape) print(elemdata.dtype) fil.write(elemdata.tobytes()) else: writeData(fil, elemdata, fmt='%i', sep=' ') pf.verbose(2, f"File size: {fn.size} bytes")
# Output of surface file formats
[docs]def writeGTS(fn, surf): """Write a TriSurface to a file in GTS format. Parameters ---------- fn: :term:`path_like` The output file name, commonly having a suffix '.gts'. If the suffix ends on '.gz' or '.bz2', the file will transparently be compressed during writing. surf: TriSurface The TriSurface to write to the file. Examples -------- >>> f = Path('test_filewrite.gts') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeGTS(f, M.toSurface()) >>> print(f.read_text()) 4 5 2 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1 2 3 1 4 1 2 3 3 4 1 4 2 5 3 2 #GTS file written by pyFormex ... <BLANKLINE> >>> f.remove() """ fn = Path(fn) from .trisurface import TriSurface if not isinstance(surf, TriSurface): raise ValueError("Expected TriSurface as second argument'") pf.verbose(1, f"Write GTS file {fn.absolute()}") coords = surf.coords edges = surf.getEdges() faces = surf.getElemEdges() with utils.File(fn, 'wb') as fil: writeText(fil, f"{coords.shape[0]} {edges.shape[0]} " f"{faces.shape[0]}\n") writeData(fil, coords, fmt='%f', sep=' ') writeData(fil, edges+1, fmt='%i', sep=' ') writeData(fil, faces+1, fmt='%i', sep=' ') writeText(fil, f"#GTS file written by {pf.Version()}\n") pf.verbose(2, f"File size: {fn.size} bytes")
[docs]def writeSTL(fn, x, n=None, binary=False, color=None): """Write a collection of triangles to an STL file. Parameters ---------- fn: :term:`path_like` The output file name, commonly having a suffix '.stl' or '.stla' (for ascii output) or '.stlb' (for binary output). If the suffix ends on '.gz' or '.bz2', the file will transparently be compressed during writing. x: Coords | Formex A Coords or Formex with shape (ntriangles,3,3) holding the coordinates of the vertices of the triangles to write to the file. n: Coords, optional A Coords with shape (ntriangles,3) holding the normal vectors to the triangles. If not specified, they will be calculated. binary: bool If True, the output file format will be a binary STL. The default is an ascii STL. color: (4,) int array An int array with 4 values in the range 0..255. These are the red, green, blue and alpha components of a single color for all the triangles. It will be stored in the header of a binary STL file. Note ---- The color can only be used with a binary STL format, and is not recognized by all STL processing software. Warning ------- The STL format stores a loose collection of triangles and does not include connectivity information between the triangles. Therefore the use of this format for intermediate storage is **strongly discouraged**, as many processing algorithms will need to build the connectivity information over and again, which may lead to different results depending on round-off errors. The STL format should only be used as a **final export** to e.g. visualisation methods or machining processes. Examples -------- >>> f = Path('test_filewrite.stl') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeSTL(f, M.toFormex()) >>> print(f.read_text()) solid Created by pyFormex ... facet normal 0.0 0.0 1.0 outer loop vertex 0.0 0.0 0.0 vertex 1.0 0.0 0.0 vertex 1.0 1.0 0.0 endloop endfacet facet normal 0.0 0.0 1.0 outer loop vertex 1.0 1.0 0.0 vertex 0.0 1.0 0.0 vertex 0.0 0.0 0.0 endloop endfacet endsolid <BLANKLINE> >>> f.remove() """ fn = Path(fn) if isinstance(x, Formex): x = x.coords if not x.shape[1:] == (3, 3): raise ValueError("Expected an (ntri,3,3) array, got %s" % x.shape) stltype = 'binary' if binary else 'ascii' pf.verbose(1, f"Write {stltype} STL file {fn.absolute()}") if n is None: from pyformex import geomtools a, n = geomtools.areaNormals(x) ndegen = geomtools.degenerate(a, n).shape[0] if ndegen > 0: pf.verbose(2, f"The model contains {ndegen} degenerate triangles") x = np.column_stack([n.reshape(-1, 1, 3), x]) x = at.checkArray(x, shape=(-1, 4, 3), kind='f') pf.verbose(2, f"Writing {x.shape[0]} triangles") mode = 'wb' if binary else 'w' with utils.File(fn, mode) as fil: if binary: write_stl_bin(fil, x, color) else: write_stl_asc(fil, x) pf.verbose(2, f"File size: {fn.size} bytes")
[docs]def write_stl_bin(fil, x, color=None): """Write a binary stl. Note ---- This is a low level routine for use in writeSTL. It is not intended to be used directly. Parameters ---------- fil: :term:`file_like` The file to write the data to. It can be any object supporting the write(bytes) method, like a file opened in binary write mode. x: (ntri,4,3) float array Array with 1 normal and 3 vertices and 1 normal per triangle. color: (4,) int array, optional Four color components in the range 0..255: red, green, blue and alpha. If specified, these will be stored in the header and **may** be recognized by some other software. Examples -------- >>> f = Path('test_filewrite.stl') >>> M = Mesh(eltype='quad4').convert('tri3-u') >>> writeSTL(f, M.toFormex().coords, binary=True, color=[255,0,0,128]) >>> from .fileread import readSTL >>> x, n, c = readSTL(f) >>> print(x) [[[0. 0. 0.] [1. 0. 0.] [1. 1. 0.]] <BLANKLINE> [[1. 1. 0.] [0. 1. 0.] [0. 0. 0.]]] >>> print(n) [[0. 0. 1.] [0. 0. 1.]] >>> print(c) (1.0, 0.0, 0.0) >>> f.remove() """ if color is not None: color = at.checkArray(color, shape=(4,), kind='u', allow='i').astype(np.uint8) ver = pf.fullVersion().encode('latin1') if len(ver) > 50: ver = ver[:50] if color is None: color = b'' else: color = b" COLOR=%c%c%c%c" % tuple(color) pf.verbose(2, "Adding %s to the header" % color) head = b"%-50s %-29s" % (ver, color) fil.write(head) ntri = x.shape[0] np.array(ntri).astype(np.int32).tofile(fil) x = x.astype(np.float32) for i in range(ntri): x[i].tofile(fil) fil.write(b'\x00\x00')
[docs]def write_stl_asc(fil, x): """Write a collection of triangles to an ascii .stl file. Note ---- This is a low level routine for use in writeSTL. It is not intended to be used directly. Parameters ---------- fil: :term:`file_like` The file to write the data to. It can be any object supporting the write(bytes) method, like a file opened in binary write mode. x: (ntri,4,3) float array Array with 1 normal and 3 vertices and 1 normal per triangle. """ fil.write("solid Created by %s\n" % pf.fullVersion()) for e in x: fil.write(" facet normal %s %s %s\n" % tuple(e[0])) fil.write(" outer loop\n") for p in e[1:]: fil.write(" vertex %s %s %s\n" % tuple(p)) fil.write(" endloop\n") fil.write(" endfacet\n") fil.write("endsolid\n")
[docs]def writeData(fil, data, fmt=None, sep='', end='\n'): """Write an array of numerical data to an open file. Parameters ---------- fil: :term:`file_like` The file to write the data to. It can be any object supporting the write(bytes) method, like a file opened in binary write mode. data: :term:`array_like` A numerical array of int or float type. For output, the array will be reshaped to a 2D array, keeping the length of the last axis. fmt: str, optional A format string compatible with the array data type. If not provided, the data are written as a single block using :func:`numpy.tofile`. If provided, the format string should contain a valid format converter for a single data item. It may also contain the necessary spacing or separator. Examples are '%5i ' for int data and '%f,' or '%10.3e' for float data. sep: str, optional A string to be used as separator between single items. Only used if fmt is provided and the file is written in ascii mode. end: str, optional A string to be written at the end of the data block (if no `fmt`) or at the end of each row (with `fmt`). The default value is a newline character. Only used if fmt is provided and the file is written in ascii mode. Examples -------- >>> i = np.eye(3) >>> f = Path('test_filewrite.out') >>> with f.open('wb') as fil: ... writeData(fil,i,sep=' ') >>> f.size 35 >>> print(f.read_text()) 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 >>> with f.open('wb') as fil: ... writeData(fil,i,fmt='%.4f',sep=' ') >>> f.size 63 >>> print(f.read_text()) 1.0000 0.0000 0.0000 0.0000 1.0000 0.0000 0.0000 0.0000 1.0000 >>> i = np.arange(24).reshape(2,3,4) >>> with f.open('wb') as fil: ... writeData(fil,i,fmt='%.4f',sep=' ') >>> print(f.read_text()) 0.0000 1.0000 2.0000 3.0000 4.0000 5.0000 6.0000 7.0000 8.0000 9.0000 10.0000 11.0000 12.0000 13.0000 14.0000 15.0000 16.0000 17.0000 18.0000 19.0000 20.0000 21.0000 22.0000 23.0000 >>> f.remove() """ if fmt is None: data.tofile(fil, sep) else: ncols = data.shape[-1] val = data.reshape(-1, ncols) template = sep.join([fmt] * ncols) + end for v in val: writeText(fil, template % tuple(v))
[docs]def writeIData(fil, data, fmt, ind=1, sep=' ', end='\n'): """Write an indexed array of numerical data to an open file. Parameters ---------- fil: :term:`file_like` The file to write the data to. It can be any object supporting the write(bytes) method, like a file opened in binary write mode. data: :term:`array_like` A numerical array of int or float type. For output, the array will be reshaped to a 2D array, keeping the length of the last axis. fmt: str A format string compatible with the array data type. The format string should contain a valid format converter for a a single data item. It may also contain the necessary spacing or separator. Examples are '%5i ' for int data and '%f,' or '%10.3e' for float data. ind: int or int :term:`array_like` The row indices to write with the data. If an array, its length should be equal to the numbe of rows in the (2D-reshaped) `data` array. If a single int, it specifies the index for the first row, and the value will be automatically incremented for the other rows. sep: str, optional A string to be used as separator between single items. end: str, optional A string to be written at the end of the data block (if no `fmt`) or at the end of each row (with `fmt`). The default value is a newline character. Examples -------- >>> i = np.eye(3) >>> f = Path('test_filewrite.out') >>> with f.open('wb') as fil: ... writeIData(fil,i,fmt='%.4f',sep=' ') >>> f.size 72 >>> print(f.read_text()) 1 1.0000 0.0000 0.0000 2 0.0000 1.0000 0.0000 3 0.0000 0.0000 1.0000 >>> f.remove() """ ncols = data.shape[-1] val = data.reshape(-1, ncols) nrows = val.shape[0] if at.isInt(ind): ind = ind + np.arange(nrows) else: ind = ind.reshape(-1) if ind.shape[0] != nrows: raise ValueError("Index should have same length as data") template = "%d " + sep.join([fmt] * ncols) + end for i, v in zip(ind, val): writeText(fil, template % (i, *v))
[docs]def writeText(fil, text): """Write text to a file opened in text or binary mode text can be either str or bytes. If not matching the open mode, text is encoded to/decoded from utf-8 """ if 'b' in fil.mode: if isinstance(text, bytes): out = text else: out = text.encode('utf-8') else: if isinstance(text, bytes): out = text.decode('utf-8') else: out = text fil.write(out)
# End