diff --git a/README.md b/README.md index 7e81d18..35e4f14 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,140 @@ UVW - Universal VTK Writer ========================== UVW is a small utility library to write VTK files from data contained in Numpy arrays. It handles fully-fledged `ndarrays` defined over {1, 2, 3}-d domains, with arbitrary number of components. There are no constraints on the particular order of components, although copy of data can be avoided if the array is Fortran contiguous, as VTK files are written in Fortran order. Future developments will include multi-process write support. ## Getting Started Here is how to install and use `uvw`. ### Prerequisites * Python 3. It may work with python 2, but it hasn't been tested. * [Numpy](http://www.numpy.org/). This code has been tested with Numpy version 1.14.3. ### Installing This library can be installed with `pip`: ``` pip install --user uvw ``` ### Writing Numpy arrays As a first example, let us write a multi-component numpy array into a rectilinear grid: ```python import numpy as np from uvw import RectilinearGrid, DataArray # Creating coordinates x = np.linspace(-0.5, 0.5, 10) y = np.linspace(-0.5, 0.5, 20) z = np.linspace(-0.9, 0.9, 30) # Creating the file grid = RectilinearGrid('grid.vtr', (x, y, z)) # A centered ball x, y, z = np.meshgrid(x, y, z, indexing='ij') r = np.sqrt(x**2 + y**2 + z**2) ball = r < 0.3 # Some multi-component multi-dimensional data data = np.zeros([10, 20, 30, 3, 3]) data[ball, ...] = np.array([[0, 1, 0], [1, 0, 0], [0, 1, 1]]) # Adding the point data (see help(DataArray) for more info) grid.addPointData(DataArray(data, range(3), 'data')) grid.write() ``` UVW also supports writing data on 2D and 1D physical domains, for example: ```python +import sys import numpy as np from uvw import RectilinearGrid, DataArray # Creating coordinates x = np.linspace(-0.5, 0.5, 10) y = np.linspace(-0.5, 0.5, 20) # A centered disk xx, yy = np.meshgrid(x, y, indexing='ij') r = np.sqrt(xx**2 + yy**2) R = 0.3 disk = r < R data = np.zeros([10, 20]) data[disk] = np.sqrt(1-(r[disk]/R)**2) # File object can be used as a context manager -with RectilinearGrid('grid.vtr', (x, y)) as grid: +# and you can write to stdout! +with RectilinearGrid(sys.stdout, (x, y)) as grid: grid.addPointData(DataArray(data, range(2), 'data')) ``` ## List of features Here is a list of what is available in UVW: ### VTK file formats - Image data (`.vti`) - Rectilinear grid (`.vtr`) - Structured grid (`.vts`) ### Data representation - ASCII - Base64 (uncompressed) ### Planned developments Here is a list of future developments: - [x] Image data - [ ] Unstructured grid - [x] Structured grid - [ ] Parallel writing (multi-process) - [ ] Benchmarking + performance comparison with [pyevtk](https://bitbucket.org/pauloh/pyevtk) ## Developing These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. ### Git repository First clone the git repository: ``` git clone https://c4science.ch/source/uvw.git ``` Then you can use pip in development mode (possibly in [virtualenv](https://virtualenv.pypa.io/en/stable/)): ``` pip install --user -e . ``` ## Running the tests The tests can be run using [pytest](https://docs.pytest.org/en/latest/): ``` cd tests; pytest ``` ## License This project is licensed under the MIT License - see the LICENSE.md file for details. ## Acknowledgments * [@PurpleBooth](https://github.com/PurpleBooth)'s [README-Template](https://gist.github.com/PurpleBooth/109311bb0361f32d87a2) diff --git a/examples/multicomponents_3d.py b/examples/multicomponents_3d.py new file mode 100644 index 0000000..9205c30 --- /dev/null +++ b/examples/multicomponents_3d.py @@ -0,0 +1,26 @@ +import numpy as np +from uvw import RectilinearGrid, DataArray + +# Creating coordinates +x = np.linspace(-0.5, 0.5, 10) +y = np.linspace(-0.5, 0.5, 20) +z = np.linspace(-0.9, 0.9, 30) + +# Creating the file +grid = RectilinearGrid('grid.vtr', (x, y, z)) + +# A centered ball +x, y, z = np.meshgrid(x, y, z, indexing='ij') +r = np.sqrt(x**2 + y**2 + z**2) +ball = r < 0.3 + +# Some multi-component multi-dimensional data +data = np.zeros([10, 20, 30, 3, 3]) +data[ball, ...] = np.array([[0, 1, 0], + [1, 0, 0], + [0, 1, 1]]) + + +# Adding the point data (see help(DataArray) for more info) +grid.addPointData(DataArray(data, range(3), 'data')) +grid.write() \ No newline at end of file diff --git a/examples/python3 b/examples/python3 new file mode 100644 index 0000000..e69de29 diff --git a/examples/surface.py b/examples/surface.py new file mode 100644 index 0000000..8532405 --- /dev/null +++ b/examples/surface.py @@ -0,0 +1,21 @@ +import sys +import numpy as np +from uvw import RectilinearGrid, DataArray + +# Creating coordinates +x = np.linspace(-0.5, 0.5, 10) +y = np.linspace(-0.5, 0.5, 20) + +# A centered disk +xx, yy = np.meshgrid(x, y, indexing='ij') +r = np.sqrt(xx**2 + yy**2) +R = 0.3 +disk = r < R + +data = np.zeros([10, 20]) +data[disk] = np.sqrt(1-(r[disk]/R)**2) + +# File object can be used as a context manager +# and you can write to stdout! +with RectilinearGrid(sys.stdout, (x, y)) as grid: + grid.addPointData(DataArray(data, range(2), 'data')) diff --git a/uvw/writer.py b/uvw/writer.py index 007a54c..98d4ffc 100644 --- a/uvw/writer.py +++ b/uvw/writer.py @@ -1,112 +1,121 @@ -import xml.dom import xml.dom.minidom as dom import functools +import io import base64 import numpy as np def setAttributes(node, attributes): """Set attributes of a node""" for item in attributes.items(): node.setAttribute(*item) def encodeArray(array): # Mandatory number of bytes encoded as uint32 nbytes = array.nbytes bytes = base64.b64encode(np.array([nbytes], dtype=np.uint32)) bytes += base64.b64encode(array) return bytes class Component: """Generic component class capable of registering sub-components""" def __init__(self, name, parent_node, writer): self.writer = writer self.document = writer.document self.node = self.document.createElement(name) parent_node.appendChild(self.node) def setAttributes(self, attributes): setAttributes(self.node, attributes) def register(self, name, attributes=dict()): """Register a sub-component""" sub_component = Component(name, self.node, self.writer) setAttributes(sub_component.node, attributes) return sub_component def registerDataArray(self, data_array, vtk_format='binary'): """Register a DataArray object""" array_component = Component('DataArray', self.node, self.writer) attributes = data_array.attributes attributes['format'] = vtk_format if vtk_format == 'append': raise 'Feature does not work' attributes['offset'] = str(self.writer.offset) array = data_array.flat_data self.writer.offset += array.nbytes self.writer.offset += self.writer.size_indicator_bytes self.writer.append_data_arrays.append(data_array) elif vtk_format == 'ascii': data_as_str = functools.reduce( lambda x, y: x + str(y) + ' ', data_array.flat_data, "") array_component.node.appendChild( self.document.createTextNode(data_as_str)) elif vtk_format == 'binary': array_component.node.appendChild( self.document.createTextNode( encodeArray(data_array.flat_data).decode('ascii'))) setAttributes(array_component.node, attributes) class Writer: """Generic XML handler for VTK files""" - def __init__(self, vtk_format, vtk_version='0.1', byte_order='LittleEndian'): - self.document = dom.getDOMImplementation().createDocument(None, 'VTKFile', None) + def __init__(self, vtk_format, + vtk_version='0.1', + byte_order='LittleEndian'): + self.document = dom.getDOMImplementation() \ + .createDocument(None, 'VTKFile', None) self.root = self.document.documentElement self.root.setAttribute('type', vtk_format) self.root.setAttribute('version', vtk_version) self.root.setAttribute('byte_order', byte_order) self.data_node = self.document.createElement(vtk_format) self.root.appendChild(self.data_node) self.offset = 0 # Global offset self.size_indicator_bytes = np.dtype(np.uint32).itemsize self.append_data_arrays = [] def setDataNodeAttributes(self, attributes): """Set attributes for the entire dataset""" setAttributes(self.data_node, attributes) def registerPiece(self, attributes={}): """Register a piece element""" piece = Component('Piece', self.data_node, self) setAttributes(piece.node, attributes) return piece def registerAppend(self): append_node = Component('AppendedData', self.root, self) setAttributes(append_node.node, {'format': 'base64'}) self.root.appendChild(append_node.node) data_str = b"_" for data_array in self.append_data_arrays: data_str += encodeArray(data_array.flat_data) text = self.document.createTextNode(data_str.decode('ascii')) append_node.node.appendChild(text) - def write(self, filename): - with open(filename, 'w') as file: - self.document.writexml(file, indent="\n ", addindent=" ") + def write(self, fd): + if type(fd) == str: + with open(fd, 'w') as file: + self.write(file) + elif issubclass(type(fd), io.TextIOBase): + self.document.writexml(fd, indent="\n ", addindent=" ") + else: + raise RuntimeError("Expected a path or " + + "file handle, got {}".format(type(fd))) def __str__(self): """Print XML to string""" return self.document.toprettyxml()