Source code for postpic.io.vtk

#
# This file is part of postpic.
#
# postpic 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.
#
# postpic 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 postpic. If not, see <http://www.gnu.org/licenses/>.
#
# Stefan Tietze, 2017
# Alexander Blinne, 2017
# Stephan Kuschel, 2017
'''
The postpic.io.vtk module provides classes and functions to export fields to the vtk 2.0 legacy
file format.

The files are written in binary form and may have single or double precision.

The vtk exporter is built up from multiple classes, representing the different parts of data that
are put into a vtk file:

VtkFile: represents the actual file to be written. Works as a context-manager that opens and
         initializes the file on __enter__ and closes the file on __exit__. Next to the actual
         file object `vtkfile.file`, it carries the attributes `vtkfile.type`, vtkfile.dtype` and
         `vtkfile.mode`, which are later used to make sure the file is written in the intended
         format.

VtkData: represents the data that should be written to a file. Has a "tofile" method that will
         create a VtkFile and pass it to the "tofile" methods of the other objects that carry the
         actual data. VtkData stores one object that is an instance of `DataSet` that defines the
         grid that the data lives on and one or more objects that are instances of `Data` that
         contain the data to be exported.

DataSet: Superclass for different types of grid. Subclasses are `StructuredPoints` and
         `RectilinearGrid`. They have classmethods `.from_field` which allow the creation of
         objects from a given `Field`.

Data:    Superclass for representing either `PointData` or `CellData`. So far only
         `PointData` is used. This will be used to contain a subclass of `ArrayData`.

ArrayData: Superclass for representing a collection of `Scalars` or `Vectors` that are stored in
           an array that will be created from one or more `Field`s.

Using all of this, writing a vtk file with `Scalar` data attached to the points (`PointData`) of a
`StructuredGrid` is as simple as:

VtkData(StructuredPoints.from_field(scalarfield),
         PointData(Scalars(scalarfield))
       ).tofile(filename)

'''

import warnings

import numpy as np

from ..helper import product


[docs]class DataSet(object): ''' Superclass to represent different vtkDataSets ''' pass
[docs]class StructuredPoints(DataSet): ''' Class to represent a vtkStructuredPoints ''' def __init__(self, dimensions, origin, spacing): if len(dimensions) != 3 or len(origin) != 3 or len(spacing) != 3: raise ValueError('All arguments must have len(...) == 3.') self.dimensions = dimensions self.origin = origin self.spacing = spacing
[docs] @classmethod def from_field(cls, field): if field.dimensions != 3: raise ValueError("Field must have three dimensions.") dimensions = [len(ax) for ax in field.axes] origin = [ax.grid[0] for ax in field.axes] spacing = [ax.spacing for ax in field.axes] return cls(dimensions, origin, spacing)
[docs] def tofile(self, vtk): vtk.file.write(b'DATASET STRUCTURED_POINTS\n') vtk.file.write('DIMENSIONS {} {} {}\n'.format(*self.dimensions).encode('ascii')) vtk.file.write('ORIGIN {} {} {}\n'.format(*self.origin).encode('ascii')) vtk.file.write('SPACING {} {} {}\n'.format(*self.spacing).encode('ascii'))
[docs]class RectilinearGrid(DataSet): ''' Class to represent a vtkRectilinearGrid ''' def __init__(self, grid): if len(grid) != 3: raise ValueError('Grid must have three axes') self.grid = grid
[docs] @classmethod def from_field(cls, field): return cls(field.grid)
[docs] def tofile(self, vtk): vtk.file.write(b'DATASET RECTILINEAR_GRID\n') vtk.file.write('DIMENSIONS {} {} {}\n'.format(*(len(g) for g in self.grid)) .encode('ascii')) for axname, axis in zip('XYZ', self.grid): vtk.file.write('{}_COORDINATES {} {}\n'.format(axname, len(axis), vtk.type) .encode('ascii')) try: # ndarray.tobytes() was introduced in numpy 1.9 axis = axis.astype(vtk.dtype).tobytes() except AttributeError: # workaround for numpy <1.9, but only works in python 3 axis = axis.astype(vtk.dtype).data.tobytes() vtk.file.write(axis) vtk.file.write(b'\n')
[docs]class VtkFile(object): ''' Class used to write a .vtk file. Used by `VtkData`. ''' def __init__(self, fname, type='double', mode='binary'): self.fname = fname self.mode = mode if self.mode != 'binary': raise NotImplemented("Only 'mode'=='binary' is implemented.") self.type = type if type == 'double': self.dtype = np.dtype('>f8') elif type == 'float': self.dtype = np.dtype('>f4') else: raise ValueError("Invalid 'type', must be 'float' or 'double'.")
[docs] def __enter__(self): self.file = open(self.fname, 'wb') self.file.write(b'# vtk DataFile Version 2.0\n') self.file.write(b'PostPic exported data\n') self.file.write('{}\n'.format(self.mode.upper()).encode('ascii')) return self
[docs] def __exit__(self, exception_type, exception_value, traceback): self.file.close()
[docs]class VtkData(object): ''' Class to represent the data that should be written to a .vtk file. Uses `VtkFile`. ''' def __init__(self, dataset, *data): self.dataset = dataset if not isinstance(dataset, DataSet): raise TypeError("dataset must be of type DataSet") self.data = data if not all(isinstance(d, Data) for d in data): raise TypeError("all elements of data must be of type Data")
[docs] def tofile(self, fname, type='double', mode='binary'): with VtkFile(fname, mode=mode, type=type) as vtk: self.dataset.tofile(vtk) for d in self.data: d.tofile(vtk)
[docs]class Data(object): ''' Superclass to represent the attributed data associated with a `DataSet`. ''' def __init__(self, arraydata): self.arraydata = arraydata
[docs] def tofile(self, vtk): self.arraydata.tofile(vtk)
[docs]class PointData(Data): ''' PointData associated with a `DataSet` '''
[docs] def tofile(self, vtk): vtk.file.write('POINT_DATA {}\n'.format(len(self.arraydata)).encode('ascii')) super(PointData, self).tofile(vtk)
[docs]class CellData(Data): ''' CellData associated with a `DataSet` '''
[docs] def tofile(self, vtk): vtk.file.write('CELL_DATA {}\n'.format(len(self.arraydata)).encode('ascii')) super(PointData, self).tofile(vtk)
[docs]class ArrayData(object): ''' Superclass to represent different kinds of data that can be attributed to Points or Cells and are given as an iterable of Fields ''' def __init__(self, *fields, **kwargs): self.fields = fields self.name = kwargs.pop('name', None) if self.name is None: # catch not only the case that 'name' is not present in kwargs, # but also the case that None was explicitly passed self.name = getattr(fields[0], 'name', 'None') self.name = str(self.name).replace(' ', '_')
[docs] def transform_data(self, dtype): data = np.vstack((np.ravel(f, order='F') for f in self.fields)) data = data.ravel(order='F').astype(dtype) return data
[docs] def tofile(self, vtk): data = self.transform_data(vtk.dtype) try: # ndarray.tobytes() was introduced in numpy 1.9 data = data.tobytes() except AttributeError: # workaround for numpy <1.9, but only works in python 3 data = data.data.tobytes() vtk.file.write(data)
def __len__(self): return product(self.fields[0].shape)
[docs]class Vectors(ArrayData): ''' Class to represent Vectors ''' def __init__(self, *fields, **kwargs): if len(fields) != 3: raise ValueError('A Vector must have three components') super(Vectors, self).__init__(*fields, **kwargs)
[docs] def tofile(self, vtk): vtk.file.write('VECTORS {} {}\n'.format(self.name, vtk.type).encode('ascii')) super(Vectors, self).tofile(vtk)
[docs]class Scalars(ArrayData): ''' Class to represent a collection of Scalars ''' def __init__(self, *fields, **kwargs): if len(fields) > 4: raise ValueError('Vtk supports up to 4 Scalars in one DataSet') super(Scalars, self).__init__(*fields, **kwargs)
[docs] def tofile(self, vtk): vtk.file.write('SCALARS {} {} {}\n'.format(self.name, vtk.type, len(self.fields)) .encode('ascii')) vtk.file.write(b'LOOKUP_TABLE default\n') super(Scalars, self).tofile(vtk)
def _export_arraydata_vtk(filename, *fields, **kwargs): ''' Generic method to export multiple `fields` into `filename` as a classic vtk file. Keyword arguments may be: kwargs['kind']: one of `Vectors` or `Scalars` kwargs['name']: Name of the exported dataset kwargs['type']: 'float' or 'double' kwargs['unstagger']: True if fields should be automatically unstaggered if appropriate kwargs['skip_axes_check']: True if the check that fields for having the same axes should be skipped ''' ArrayDataSubClass = kwargs.pop('kind', Vectors) name = kwargs.pop('name', None) if name == '': name = 'Field' datatype = kwargs.pop('type', 'float') unstagger = kwargs.pop('unstagger', True) skip_axes_check = kwargs.pop('skip_axes_check', False) if not all([field.axes == fields[0].axes for field in fields[1:]]): if unstagger: from ..helper import unstagger_fields fields = unstagger_fields(*fields) elif not skip_axes_check: raise ValueError('All fields must have the same axes.') if len(fields[0].shape) > 3: raise ValueError("Fields have to many axes") if len(fields[0].shape) == 1: raise ValueError("1D-data not supported by legacy vtk file format") fields = [f.atleast_nd(3) for f in fields] if all([ax.islinear() for ax in fields[0].axes]): dataset = StructuredPoints.from_field(fields[0]) else: dataset = RectilinearGrid.from_field(fields[0]) arraydata = ArrayDataSubClass(*fields, name=name) data = PointData(arraydata) vtkdata = VtkData(dataset, data) vtkdata.tofile(filename, type=datatype)
[docs]def export_scalar_vtk(filename, scalarfield, **kwargs): ''' exports one 2D or 3D scalar field object to a VTK file which is suitable for viewing in ParaView. It is assumed that all fields are defined on the same grid. ''' export_scalars_vtk(filename, scalarfield, **kwargs)
[docs]def export_vector_vtk(filename, *fields, **kwargs): ''' exports a vector field to a VTK file suitable for viewing in ParaView. Three 3D fields are expected, which will form the X, Y and Z component of the vector field. If less than tree fields are given, the missing components will be assumed to be zero. ''' from ..datahandling import Field fields = [field if isinstance(field, Field) else Field(field) for field in fields] if len(fields) > 3: raise ValueError("Too many fields") while len(fields) < 3: fields.append(fields[0].replace_data(np.zeros_like(fields[0]))) kwargs['kind'] = Vectors _export_arraydata_vtk(filename, *fields, **kwargs)
[docs]def export_scalars_vtk(filename, *fields, **kwargs): ''' exports a set of scalar fields to a VTK file suitable for viewing in ParaView. Up to four fields may be given ''' from ..datahandling import Field fields = [field if isinstance(field, Field) else Field(field) for field in fields] if len(fields) > 4: raise ValueError("Too many fields") kwargs['kind'] = Scalars _export_arraydata_vtk(filename, *fields, **kwargs)