"""Reader and writer for GOCAD TSurf triangulated surface files."""
import six
import numpy
import meshio
[docs]
class tsurf(object):
"""
Read, create, and write GOCAD TSurf triangulated surface files.
Supports construction from a ``.ts`` file path or from raw coordinate
and connectivity arrays. Provides access to the triangulated mesh via
the ``triangles`` property and can export back to the GOCAD TSurf format.
Parameters
----------
*args :
Either a single ``filename`` string (reads from file), or four
positional arguments ``x, y, z, cells`` (constructs from arrays).
solid_color : tuple of float, optional
RGBA colour tuple for visualisation. Defaults to cyan ``(0,1,1,1)``.
visible : str, optional
GOCAD visibility flag string. Defaults to ``"false"``.
name : str, optional
Surface name stored in the TSurf header. Defaults to
``"Undefined"``.
NAME : str, optional
GOCAD coordinate system ``NAME`` field. Defaults to
``"Default"``.
AXIS_NAME : str, optional
GOCAD coordinate system ``AXIS_NAME`` field. Defaults to
``'"X" "Y" "Z"'``.
AXIS_UNIT : str, optional
GOCAD coordinate system ``AXIS_UNIT`` field. Defaults to
``'"m" "m" "m"'``.
ZPOSITIVE : str, optional
GOCAD coordinate system ``ZPOSITIVE`` field. Defaults to
``"Elevation"``.
Attributes
----------
mesh : meshio.Mesh
Internal mesh representation holding ``points`` and ``cells``.
x, y, z :
Sequences of point coordinates.
header : dict
GOCAD header key-value pairs.
csInfo : dict
GOCAD coordinate system key-value pairs.
name : str
Surface name.
solid_color : tuple
RGBA visualisation colour.
visible : str
Visibility flag.
Raises
------
ValueError
If the number of positional arguments is not 1 or 4.
IOError
If a filename is supplied that does not start with ``GOCAD TSurf``.
"""
[docs]
default_name = 'Undefined'
[docs]
default_solid_color = (0, 1, 1, 1.0)
[docs]
default_visible = 'false'
[docs]
default_NAME = 'Default'
[docs]
default_AXIS_NAME = '"X" "Y" "Z"'
[docs]
default_AXIS_UNIT = '"m" "m" "m"'
[docs]
default_ZPOSITIVE = 'Elevation'
def __init__(self, *args, **kwargs):
"""
Initialise a tsurf object from a file or from coordinate arrays.
Accepts either a single filename or 4 arguments: x, y, z, cells.
keyword arguments are: ``solid_color``, ``visible``, ``name``,
``NAME``, ``AXIS_NAME``, ``AXIS_UNIT``, ``ZPOSITIVE``.
If a filename is given, the tsurf is read from the file.
Otherwise, ``x``, ``y``, ``z`` are sequences of the x, y, and z
coordinates of the points, and ``cells`` is a sequence of the
indices of the coord arrays making up each triangle in the mesh,
e.g. ``[[0, 1, 2], [2, 1, 3], ...]``.
Parameters
----------
*args :
One string (filename) or four sequences (x, y, z, cells).
**kwargs :
Optional overrides for header and coordinate system fields.
Raises
------
ValueError
If the number of positional arguments is not 1 or 4.
"""
if len(args) == 1:
self._read_tsurf(args[0])
elif len(args) == 4:
self._init_from_xyz(*args)
else:
raise ValueError('Invalid input arguments')
color = kwargs.get('solid_color', None)
visible = kwargs.get('visible', None)
name = kwargs.get('name', None)
if color is not None:
self.solid_color = color
if name is not None:
self.name = name
if visible is not None:
self.visible = visible
self.header['name'] = self.name
self.header['solid*color'] = self.solid_color
self.header['visible'] = self.visible
NAME = kwargs.get('NAME')
AXIS_NAME = kwargs.get('AXIS_NAME')
AXIS_UNIT = kwargs.get('AXIS_UNIT')
ZPOSITIVE = kwargs.get('ZPOSITIVE')
if NAME is not None:
self.NAME = NAME
if AXIS_NAME is not None:
self.AXIS_NAME = AXIS_NAME
if AXIS_UNIT is not None:
self.AXIS_UNIT = AXIS_UNIT
if ZPOSITIVE is not None:
self.ZPOSITIVE = ZPOSITIVE
self.csInfo['NAME'] = self.NAME
self.csInfo['AXIS_NAME'] = self.AXIS_NAME
self.csInfo['AXIS_UNIT'] = self.AXIS_UNIT
self.csInfo['ZPOSITIVE'] = self.ZPOSITIVE
def _read_tsurf(self, filename):
"""
Parse a GOCAD TSurf file and populate the instance attributes.
Reads the HEADER block, GOCAD_ORIGINAL_COORDINATE_SYSTEM block,
and the VRTX/PVRTX/TRGL data lines.
Parameters
----------
filename :
Path to the GOCAD TSurf ``.ts`` file.
Raises
------
IOError
If the file does not start with ``GOCAD TSurf``.
"""
with open(filename, 'r') as infile:
firstline = next(infile).strip()
if not firstline.startswith('GOCAD TSurf'):
raise IOError('This is not a valid TSurf file!')
# Parse Header
self.header = {}
self.csInfo = {}
line = next(infile).strip()
if line.startswith('HEADER'):
line = next(infile).strip()
while '}' not in line:
key, value = line.split(':')
self.header[key.lstrip('*')] = value
line = next(infile).strip()
self.name = self.header.get('name', filename)
try:
self.solid_color = [float(item) for item in self.header['solid*color'].split()]
self.solid_color = tuple(self.solid_color)
except KeyError:
self.solid_color = self.default_solid_color
try:
self.visible = self.header['visible']
except KeyError:
self.visible = self.default_visible
# Parse coordinate system info
line = next(infile).strip()
if line.startswith('GOCAD_ORIGINAL_COORDINATE_SYSTEM'):
line = next(infile).strip()
while 'END_ORIGINAL_COORDINATE_SYSTEM' not in line:
key, value = line.split(None, 1)
self.csInfo[key] = value
line = next(infile).strip()
try:
self.NAME = self.csInfo.get('NAME')
except KeyError:
self.NAME = self.default_NAME
try:
self.ZPOSITIVE = self.csInfo.get('ZPOSITIVE')
except KeyError:
self.ZPOSITIVE = self.default_ZPOSITIVE
try:
self.AXIS_NAME = self.csInfo.get('AXIS_NAME')
except KeyError:
self.AXIS_NAME = self.default_AXIS_NAME
try:
self.AXIS_UNIT = self.csInfo.get('AXIS_UNIT')
except KeyError:
self.AXIS_UNIT = self.default_AXIS_UNIT
# Read points and cells
# if not next(infile).startswith('TFACE'):
# raise IOError('Only "TFACE" format TSurf files are supported')
points, cellArray = [], []
for line in infile:
line = line.strip().split()
if line[0] in ['VRTX', 'PVRTX']:
points.append([float(item) for item in line[2:5]])
elif line[0] == 'TRGL':
cellArray.append([int(item)-1 for item in line[1:]])
self.x, self.y, self.z = zip(*points)
points = numpy.array(points, dtype=numpy.float64)
print(len(points))
cellArray = numpy.array(cellArray, dtype=numpy.int)
cells = [("triangle", cellArray)]
self.mesh = meshio.Mesh(points, cells)
@property
[docs]
def triangles(self):
"""
Triangle vertex coordinates as a deduplicated array.
Builds a lookup dictionary from vertex index to coordinate, then
assembles each triangle's three corners into a row of 9 values
``[x1,y1,z1, x2,y2,z2, x3,y3,z3]``.
Returns
-------
numpy.ndarray of shape (n_unique_triangles, 9)
Unique triangles, sorted lexicographically by
``numpy.unique``.
"""
triangle_numbers = self.mesh.cells[0].data
vertex_dic = {i:vertex for i, vertex in enumerate(self.mesh.points)}
triangle_array = numpy.array([numpy.hstack([vertex_dic[i] for i in triangle]) for triangle in triangle_numbers])
return numpy.unique(triangle_array, axis=0)
def _init_from_xyz(self, x, y, z, cells):
"""
Initialise the tsurf from raw coordinate and cell arrays.
Sets default header and coordinate system values and constructs
the internal ``meshio.Mesh`` from the supplied data.
Parameters
----------
x :
Sequence of x-coordinates of the mesh points.
y :
Sequence of y-coordinates of the mesh points.
z :
Sequence of z-coordinates of the mesh points.
cells :
Sequence of triangle connectivity lists, each containing
three zero-based vertex indices, e.g.
``[[0, 1, 2], [2, 1, 3], ...]``.
"""
points = numpy.array(list(zip(x, y, z)), dtype=numpy.float64)
self.x, self.y, self.z = x, y, z
cells = {"triangle": cells}
self.solid_color = self.default_solid_color
self.name = self.default_name
self.visible = self.default_visible
self.mesh = meshio.Mesh(points, cells)
# Deleting the following default values since I have no idea
# what they are.
"""
self.header = {'moveAs':'2', 'drawAs':'2', 'line':'3',
'clip':'0', 'intersect':'0', 'intercolor':' 1 0 0 1'}
"""
self.header = {}
self.csInfo = {}
self.NAME = self.default_NAME
self.ZPOSITIVE = self.default_ZPOSITIVE
self.AXIS_NAME = self.default_AXIS_NAME
self.AXIS_UNIT = self.default_AXIS_UNIT
[docs]
def write(self, outname):
"""
Write the tsurf to a GOCAD TSurf ``.ts`` file.
Writes the HEADER block, GOCAD_ORIGINAL_COORDINATE_SYSTEM block,
TFACE data (VRTX lines followed by TRGL lines), and the END
marker.
Parameters
----------
outname :
Output file path for the TSurf file.
Notes
-----
Triangle connectivity is written with 1-based vertex indices as
required by the GOCAD TSurf format specification. Only the first
cell block is written; multi-block meshes are not supported.
"""
with open(outname, 'w') as outfile:
# Write Header...
outfile.write('GOCAD TSurf 1\n')
outfile.write('HEADER {\n')
"""
for key in ['name', 'color', 'moveAs', 'drawAs', 'line', 'clip',
'intersect', 'intercolor']:
value = self.header[key]
"""
for key, value in six.iteritems(self.header):
if not isinstance(value, six.string_types):
try:
value = ' '.join(repr(item) for item in value)
except TypeError:
value = repr(value)
if key == 'name':
outfile.write('{}:{}\n'.format(key, value))
else:
outfile.write('*{}:{}\n'.format(key, value))
outfile.write('}\n')
# Write CS info...
outfile.write('GOCAD_ORIGINAL_COORDINATE_SYSTEM\n')
# for key, value in six.iteritems(self.csInfo):
# It seems likely the CS keys should be in a particular order.
for key in ['NAME', 'AXIS_NAME', 'AXIS_UNIT', 'ZPOSITIVE']:
value = self.csInfo[key]
if not isinstance(value, six.string_types):
try:
value = ' '.join(repr(item) for item in value)
except TypeError:
value = repr(value)
outfile.write('{} {}\n'.format(key, value))
outfile.write('END_ORIGINAL_COORDINATE_SYSTEM\n')
# Write data...
outfile.write('TFACE\n')
for i, (x, y, z) in enumerate(self.mesh.points, start=1):
template = '\t'.join(['VRTX {}'] + 3*['{: >9.3f}']) + '\n'
outfile.write(template.format(i, x, y, z))
# For now, assume only one set of cells, and that they are all
# triangles.
for a, b, c in self.mesh.cells[0].data:
outfile.write('TRGL {} {} {}\n'.format(a+1, b+1, c+1))
outfile.write('END\n')