├── pyvisfile ├── py.typed ├── __init__.py ├── vtk │ ├── vtk_ordering.py │ └── __init__.py └── xdmf │ └── __init__.py ├── doc ├── upload-docs.sh ├── installing.rst ├── xdmf.rst ├── Makefile ├── vtk.rst ├── conf.py ├── index.rst └── faq.rst ├── examples ├── vtk-structured-2d-plain.py ├── vtk-structured-2d-curved.py ├── vtk-unstructured-points.py └── vtk-sample-elements.py ├── CITATION.cff ├── .editorconfig ├── test ├── ref-vtk-parallel.pvtu ├── test_vtk.py └── test_xdmf.py ├── LICENSE ├── README.rst ├── pyproject.toml └── .basedpyright └── baseline.json /pyvisfile/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/upload-docs.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | rsync --verbose --archive --delete _build/html/ doc-upload:doc/pyvisfile 4 | -------------------------------------------------------------------------------- /doc/installing.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: sh 2 | 3 | Installation 4 | ============ 5 | 6 | :mod:`pyvisfile` is a pure-Python package. This command should install it:: 7 | 8 | pip install pyvisfile 9 | -------------------------------------------------------------------------------- /examples/vtk-structured-2d-plain.py: -------------------------------------------------------------------------------- 1 | # contributed by Luke Olson 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | 6 | from pyvisfile.vtk import write_structured_grid 7 | 8 | 9 | n = 50 10 | x, y = np.meshgrid(np.linspace(-1, 1, n), np.linspace(-1, 1, n)) 11 | 12 | u = np.exp(-50 * (x**2 + y**2)) 13 | 14 | mesh = np.rollaxis(np.dstack((x, y)), 2) 15 | write_structured_grid( 16 | "test.vts", 17 | mesh, 18 | point_data=[("u", u[np.newaxis, :, :])]) 19 | -------------------------------------------------------------------------------- /doc/xdmf.rst: -------------------------------------------------------------------------------- 1 | Usage Reference for :mod:`pyvisfile.xdmf` 2 | ========================================= 3 | 4 | This implementation targets XDMF3 and takes available fields directly from the 5 | code in the `xdmf library `__. Additional 6 | documentation can be found `online `__, 7 | but (at the time of this writing, December 2020) does not appear to be kept up to date. 8 | 9 | .. automodule:: pyvisfile.xdmf 10 | -------------------------------------------------------------------------------- /pyvisfile/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from importlib import metadata 4 | 5 | 6 | def _parse_version(version: str) -> tuple[tuple[int, ...], str]: 7 | import re 8 | 9 | m = re.match(r"^([0-9.]+)([a-z0-9]*?)$", VERSION_TEXT) 10 | assert m is not None 11 | 12 | return tuple(int(nr) for nr in m.group(1).split(".")), m.group(2) 13 | 14 | 15 | VERSION_TEXT = __version__ = metadata.version("pyvisfile") 16 | VERSION, VERSION_STATUS = _parse_version(__version__) 17 | -------------------------------------------------------------------------------- /examples/vtk-structured-2d-curved.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from pytools import obj_array 6 | 7 | from pyvisfile.vtk import write_structured_grid 8 | 9 | 10 | angle_mesh = np.mgrid[1:2:10j, 0:2*np.pi:20j] 11 | 12 | r = angle_mesh[0, np.newaxis] 13 | phi = angle_mesh[1, np.newaxis] 14 | mesh = np.vstack([r*np.cos(phi), r*np.sin(phi)]) 15 | 16 | vec = obj_array.new_1d([np.cos(phi), np.sin(phi)]) 17 | write_structured_grid( 18 | "yo-2d.vts", 19 | mesh, 20 | point_data=[("phi", phi), ("vec", vec)]) 21 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Kloeckner" 5 | given-names: "Andreas" 6 | orcid: "https://orcid.org/0000-0003-1228-519X" 7 | - family-names: Fikl 8 | given-names: Alex 9 | - family-names: Statz 10 | given-names: Christoph 11 | - family-names: Diener 12 | given-names: Matthias 13 | - family-names: Smith 14 | given-names: Matt 15 | - family-names: Wala 16 | given-names: Matt 17 | 18 | title: "pyvisfile" 19 | version: 2022.1.1 20 | doi: 10.5281/zenodo.6533976 21 | date-released: 2022-05-09 22 | url: "https://github.com/inducer/pyvisfile" 23 | license: MIT 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | # https://github.com/editorconfig/editorconfig-vim 3 | # https://github.com/editorconfig/editorconfig-emacs 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.py] 15 | indent_size = 4 16 | 17 | [*.rst] 18 | indent_size = 4 19 | 20 | [*.cpp] 21 | indent_size = 2 22 | 23 | [*.hpp] 24 | indent_size = 2 25 | 26 | [*.yml] 27 | indent_size = 4 28 | 29 | # There may be one in doc/ 30 | [Makefile] 31 | indent_style = tab 32 | 33 | # https://github.com/microsoft/vscode/issues/1679 34 | [*.md] 35 | trim_trailing_whitespace = false 36 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= python $(shell which sphinx-build) 8 | SOURCEDIR = "." 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /test/ref-vtk-parallel.pvtu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-2016 Andreas Kloeckner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /doc/vtk.rst: -------------------------------------------------------------------------------- 1 | Usage Reference for :mod:`pyvisfile.vtk` 2 | ======================================== 3 | 4 | .. automodule:: pyvisfile.vtk 5 | .. moduleauthor:: Andreas Kloeckner 6 | 7 | .. automodule:: pyvisfile.vtk.vtk_ordering 8 | 9 | Examples 10 | -------- 11 | 12 | Writing a structured mesh 13 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 14 | 15 | .. literalinclude:: ../examples/vtk-structured-2d-plain.py 16 | 17 | (You can find this example as 18 | :download:`examples/vtk-structured-2d-plain.py <../examples/vtk-structured-2d-plain.py>` in the PyVisfile 19 | source distribution.) 20 | 21 | Writing a collection of points 22 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 23 | 24 | .. note:: 25 | 26 | Observe that this is written as a 'unstructured grid', even though there 27 | is not much grid here. However, by supplying connectivity data, it is 28 | possible to generalize from this to actual unstructured meshes. 29 | 30 | .. literalinclude:: ../examples/vtk-unstructured-points.py 31 | 32 | (You can find this example as 33 | :download:`examples/vtk-unstructured-points.py <../examples/vtk-unstructured-points.py>` in the PyVisfile 34 | source distribution.) 35 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from importlib import metadata 5 | from typing import TYPE_CHECKING 6 | from urllib.request import urlopen 7 | 8 | 9 | if TYPE_CHECKING: 10 | from sphinx.application import Sphinx 11 | 12 | 13 | _conf_url = "https://tiker.net/sphinxconfig-v0.py" 14 | with urlopen(_conf_url) as _inf: 15 | exec(compile(_inf.read(), _conf_url, "exec"), globals()) 16 | 17 | copyright = f"2010 - {datetime.today().year}, Andreas Kloeckner" 18 | release = metadata.version("pyvisfile") 19 | version = ".".join(release.split(".")[:2]) 20 | 21 | intersphinx_mapping = { 22 | "modepy": ("https://documen.tician.de/modepy", None), 23 | "numpy": ("https://numpy.org/doc/stable/", None), 24 | "python": ("https://docs.python.org/3/", None), 25 | "pytools": ("https://documen.tician.de/pytools", None), 26 | } 27 | 28 | 29 | sphinxconfig_missing_reference_aliases: dict[str, str] = { 30 | "np.dtype": "class:numpy.dtype", 31 | "np.ndarray": "class:numpy.ndarray", 32 | } 33 | 34 | 35 | def setup(app: Sphinx) -> None: 36 | app.connect("missing-reference", process_autodoc_missing_reference) # noqa: F821 37 | -------------------------------------------------------------------------------- /examples/vtk-unstructured-points.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | 5 | import numpy as np 6 | 7 | from pyvisfile.vtk import ( 8 | VF_LIST_OF_COMPONENTS, 9 | VF_LIST_OF_VECTORS, 10 | VTK_VERTEX, 11 | AppendedDataXMLGenerator, 12 | DataArray, 13 | UnstructuredGrid, 14 | ) 15 | 16 | 17 | rng = np.random.default_rng(seed=42) 18 | 19 | n = 5000 20 | points = rng.normal(size=(n, 3)) 21 | data = [ 22 | ("pressure", rng.normal(size=n)), 23 | ("velocity", rng.normal(size=(3, n)))] 24 | 25 | grid = UnstructuredGrid( 26 | (n, DataArray("points", points, vector_format=VF_LIST_OF_VECTORS)), 27 | cells=np.arange(n, dtype=np.uint32), 28 | cell_types=np.array([VTK_VERTEX] * n, dtype=np.uint8)) 29 | 30 | for name, field in data: 31 | grid.add_pointdata( 32 | DataArray(name, field, vector_format=VF_LIST_OF_COMPONENTS)) 33 | 34 | file_name = pathlib.Path("points.vtu") 35 | compressor = None 36 | 37 | if file_name.exists(): 38 | raise FileExistsError(f"Output file '{file_name}' already exists") 39 | 40 | with open(file_name, "w") as outf: 41 | AppendedDataXMLGenerator(compressor)(grid).write(outf) 42 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to :mod:`pyvisfile`'s documentation! 2 | ============================================ 3 | 4 | Pyvisfile allows you to write a variety of visualization file formats, 5 | including 6 | 7 | * `Kitware's `__ 8 | `XML-style `__ 9 | `VTK `__ data files. 10 | 11 | * `XDMF `__ data files. 12 | 13 | pyvisfile supports many mesh geometries, such such as unstructured 14 | and rectangular structured meshes, particle meshes, as well as 15 | scalar and vector variables on them. In addition, pyvisfile allows the 16 | semi-automatic writing of parallelization-segmented visualization files 17 | in VTK and XDMF formats. 18 | For updates, downloads and support, please visit the `PyVisfile web page 19 | `__. 20 | 21 | .. module:: pyvisfile 22 | 23 | Table of Contents 24 | ----------------- 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | 29 | installing 30 | vtk 31 | xdmf 32 | faq 33 | 🚀 Github 34 | 💾 Download Releases 35 | 36 | * :ref:`genindex` 37 | -------------------------------------------------------------------------------- /doc/faq.rst: -------------------------------------------------------------------------------- 1 | Licensing 2 | ========= 3 | 4 | Pylo is licensed to you under the MIT/X Consortium license: 5 | 6 | Copyright (c) 2008 Andreas Klöckner 7 | 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated documentation 10 | files (the "Software"), to deal in the Software without 11 | restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyVisfile: Write VTK/XDMF Visualization Files Efficiently 2 | --------------------------------------------------------- 3 | 4 | .. image:: https://gitlab.tiker.net/inducer/pyvisfile/badges/main/pipeline.svg 5 | :alt: Gitlab Build Status 6 | :target: https://gitlab.tiker.net/inducer/pyvisfile/commits/main 7 | .. image:: https://github.com/inducer/pyvisfile/actions/workflows/ci.yml/badge.svg 8 | :alt: Github Build Status 9 | :target: https://github.com/inducer/pyvisfile/actions/workflows/ci.yml 10 | .. image:: https://badge.fury.io/py/pyvisfile.svg 11 | :alt: Python Package Index Release Page 12 | :target: https://pypi.org/project/pyvisfile/ 13 | .. image:: https://zenodo.org/badge/1575355.svg 14 | :alt: Zenodo DOI for latest release 15 | :target: https://zenodo.org/badge/latestdoi/1575355 16 | 17 | PyVisfile allows you to write a variety of visualization file formats, 18 | including 19 | 20 | * `Kitware's `__ 21 | `XML-style `__ 22 | `VTK `__ data files. VTK files can be written without 23 | additional software installed (e.g. VTK's Python bindings). 24 | 25 | * `XDMF `__ data files. 26 | 27 | PyVisfile supports many mesh geometries, such as unstructured 28 | and rectangular structured meshes, particle meshes, as well as 29 | scalar and vector variables on them. In addition, PyVisfile allows the 30 | semi-automatic writing of parallelization-segmented visualization files 31 | in both VTK and XDMF formats. 32 | 33 | Resources: 34 | 35 | * `Documentation `_. 36 | * `Source Code `_. 37 | -------------------------------------------------------------------------------- /test/test_vtk.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | import pytools.obj_array as obj_array 9 | 10 | from pyvisfile.vtk import ( 11 | VF_LIST_OF_COMPONENTS, 12 | VF_LIST_OF_VECTORS, 13 | VTK_VERTEX, 14 | AppendedDataXMLGenerator, 15 | DataArray, 16 | ParallelXMLGenerator, 17 | UnstructuredGrid, 18 | write_structured_grid, 19 | ) 20 | 21 | 22 | def make_unstructured_grid(n: int) -> UnstructuredGrid: 23 | rng = np.random.default_rng(seed=42) 24 | points = rng.normal(size=(n, 3)) 25 | 26 | data = [ 27 | ("pressure", rng.normal(size=n)), 28 | ("velocity", rng.normal(size=(3, n))), 29 | ] 30 | 31 | grid = UnstructuredGrid( 32 | (n, DataArray("points", points, vector_format=VF_LIST_OF_VECTORS)), 33 | cells=np.arange(n, dtype=np.uint32), 34 | cell_types=np.asarray([VTK_VERTEX] * n, dtype=np.uint8)) 35 | 36 | for name, field in data: 37 | grid.add_pointdata( 38 | DataArray(name, field, vector_format=VF_LIST_OF_COMPONENTS)) 39 | 40 | return grid 41 | 42 | 43 | @pytest.mark.parametrize("n", [5000, 0]) 44 | def test_vtk_unstructured_points(n: int) -> None: 45 | grid = make_unstructured_grid(n) 46 | file_name = pathlib.Path(f"vtk-unstructured-{n:04d}.vtu") 47 | compressor = None 48 | 49 | if file_name.exists(): 50 | raise FileExistsError(f"Output file '{file_name}' already exists") 51 | 52 | with open(file_name, "w") as outf: 53 | AppendedDataXMLGenerator(compressor)(grid).write(outf) 54 | 55 | 56 | def test_vtk_structured_grid() -> None: 57 | angle_mesh = np.mgrid[1:2:10j, 0:2*np.pi:20j, 0:np.pi:30j] # type: ignore[misc] 58 | 59 | r = angle_mesh[0, np.newaxis] 60 | phi = angle_mesh[1, np.newaxis] 61 | theta = angle_mesh[2, np.newaxis] 62 | mesh = np.vstack(( 63 | r*np.sin(theta)*np.cos(phi), 64 | r*np.sin(theta)*np.sin(phi), 65 | r*np.cos(theta), 66 | )) 67 | 68 | vec = obj_array.new_1d([ 69 | np.sin(theta)*np.cos(phi), 70 | np.sin(theta)*np.sin(phi), 71 | np.cos(theta), 72 | ]) 73 | 74 | write_structured_grid( 75 | "vtk-structured.vts", 76 | mesh, 77 | point_data=[("phi", phi), ("vec", vec)]) 78 | 79 | 80 | def test_vtk_parallel() -> None: 81 | cwd = pathlib.Path(__file__).parent 82 | file_name = cwd / "vtk-parallel.pvtu" 83 | 84 | grid = make_unstructured_grid(1024) 85 | pathnames = [f"vtk-parallel-piece-{i}.vtu" for i in range(5)] 86 | 87 | if file_name.exists(): 88 | raise FileExistsError(f"Output file '{file_name}' already exists") 89 | 90 | with open(file_name, "w") as outf: 91 | ParallelXMLGenerator(pathnames)(grid).write(outf) 92 | 93 | import filecmp 94 | assert filecmp.cmp(file_name, cwd / "ref-vtk-parallel.pvtu") 95 | 96 | 97 | if __name__ == "__main__": 98 | import sys 99 | if len(sys.argv) > 1: 100 | exec(sys.argv[1]) 101 | else: 102 | from pytest import main 103 | main([__file__]) 104 | 105 | # vim: fdm=marker 106 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pyvisfile" 7 | version = "2024.2" 8 | description = "Large-scale Visualization Data Storage" 9 | readme = "README.rst" 10 | license = "MIT" 11 | authors = [ { name = "Andreas Kloeckner", email = "inform@tiker.net" } ] 12 | requires-python = ">=3.10" 13 | classifiers = [ 14 | "Development Status :: 4 - Beta", 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: Other Audience", 17 | "Intended Audience :: Science/Research", 18 | "Natural Language :: English", 19 | "Programming Language :: C++", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3 :: Only", 22 | "Topic :: Multimedia :: Graphics :: 3D Modeling", 23 | "Topic :: Scientific/Engineering", 24 | "Topic :: Scientific/Engineering :: Mathematics", 25 | "Topic :: Scientific/Engineering :: Physics", 26 | "Topic :: Scientific/Engineering :: Visualization", 27 | "Topic :: Software Development :: Libraries", 28 | ] 29 | dependencies = [ 30 | "numpy", 31 | "pytools>=2022.1", 32 | "typing_extensions>=4.6" 33 | ] 34 | 35 | [project.optional-dependencies] 36 | doc = [ 37 | "furo", 38 | "sphinx>=4", 39 | "sphinx-copybutton", 40 | ] 41 | test = [ 42 | "mypy", 43 | "pytest", 44 | "ruff", 45 | ] 46 | 47 | [project.urls] 48 | Documentation = "https://documen.tician.de/pyvisfile" 49 | Homepage = "https://mathema.tician.de/software/pyvisfile" 50 | Repository = "https://github.com/inducer/pyvisfile" 51 | 52 | [tool.hatch.build.targets.sdist] 53 | exclude = [ 54 | "/.git*", 55 | "/doc/_build", 56 | "/.editorconfig", 57 | "/run-*.sh", 58 | "/.basedpyright", 59 | ] 60 | 61 | [tool.ruff] 62 | preview = true 63 | 64 | [tool.ruff.lint] 65 | extend-select = [ 66 | "B", # flake8-bugbear 67 | "C", # flake8-comprehensions 68 | "E", # pycodestyle 69 | "F", # pyflakes 70 | "G", # flake8-logging-format 71 | "I", # flake8-isort 72 | "N", # pep8-naming 73 | "NPY", # numpy 74 | "Q", # flake8-quotes 75 | "RUF", # ruff 76 | "SIM", # flake8-simplify 77 | "TC", # flake8-type-checking 78 | "UP", # pyupgrade 79 | "W", # pycodestyle 80 | ] 81 | extend-ignore = [ 82 | "B028", # no explicit stacklevel in warnings 83 | "C90", # McCabe complexity 84 | "E226", # missing whitespace around arithmetic operator 85 | "E241", # multiple spaces after comma 86 | "E242", # tab after comma 87 | "E265", # comment should have a space 88 | "E402", # module level import not at the top of file 89 | ] 90 | 91 | [tool.ruff.lint.flake8-quotes] 92 | docstring-quotes = "double" 93 | inline-quotes = "double" 94 | multiline-quotes = "double" 95 | 96 | [tool.ruff.lint.isort] 97 | known-first-party = ["pytools"] 98 | known-local-folder = ["pyvisfile"] 99 | lines-after-imports = 2 100 | combine-as-imports = true 101 | required-imports = ["from __future__ import annotations"] 102 | 103 | [tool.typos.default] 104 | extend-ignore-re = [ 105 | "(?Rm)^.*(#|//)\\s*spellchecker:\\s*disable-line$" 106 | ] 107 | 108 | [tool.basedpyright] 109 | reportImplicitStringConcatenation = "none" 110 | reportUnnecessaryIsInstance = "none" 111 | reportUnusedCallResult = "none" 112 | reportExplicitAny = "none" 113 | reportPrivateUsage = "none" 114 | 115 | # Multiple reasons for this: 116 | # - make_subst_func is reported as having an incomplete type (but only in CI?) 117 | # - numpy scalar types are reported as incomplete (because of "any" precision) 118 | reportUnknownVariableType = "none" 119 | 120 | reportUnreachable = "hint" 121 | reportUnnecessaryComparison = "hint" 122 | 123 | # This reports even cycles that are qualified by 'if TYPE_CHECKING'. Not what 124 | # we care about at this moment. 125 | # https://github.com/microsoft/pyright/issues/746 126 | reportImportCycles = "none" 127 | 128 | pythonVersion = "3.10" 129 | pythonPlatform = "All" 130 | 131 | exclude = [ 132 | "doc", 133 | ".env", 134 | ".conda-root", 135 | "examples", 136 | ] 137 | 138 | [[tool.basedpyright.executionEnvironments]] 139 | root = "test" 140 | reportUnknownArgumentType = "none" 141 | reportUnknownVariableType = "none" 142 | reportUnknownMemberType = "hint" 143 | reportMissingParameterType = "none" 144 | reportAttributeAccessIssue = "none" 145 | reportMissingImports = "none" 146 | reportArgumentType = "hint" 147 | reportAny = "hint" 148 | -------------------------------------------------------------------------------- /examples/vtk-sample-elements.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example can be used to check if the VTK ordering and the ones implemented 3 | in :mod:`pyvisfil.vtk.vtk_ordering` match. It can be useful for debugging 4 | and implementing additional element types. 5 | 6 | To facilitate this comparison, and unlike the rest of the package, this example 7 | makes use of the VTK Python bindings. 8 | """ 9 | from __future__ import annotations 10 | 11 | import numpy as np 12 | import numpy.linalg as la 13 | 14 | 15 | try: 16 | import vtk 17 | from vtkmodules.util.numpy_support import vtk_to_numpy 18 | except ImportError as exc: 19 | print(f"Python bindings for VTK cannot be found: {exc}") 20 | raise SystemExit(0) from None 21 | 22 | 23 | VTK_LAGRANGE_SIMPLICES = [ 24 | "VTK_LAGRANGE_CURVE", 25 | "VTK_LAGRANGE_TRIANGLE", 26 | "VTK_LAGRANGE_TETRAHEDRON", 27 | ] 28 | 29 | VTK_LAGRANGE_QUADS = [ 30 | "VTK_LAGRANGE_QUADRILATERAL", 31 | "VTK_LAGRANGE_HEXAHEDRON", 32 | ] 33 | 34 | 35 | def plot_node_ordering(filename, points, show=False): 36 | import matplotlib.pyplot as plt 37 | 38 | if points.shape[0] == 1: 39 | points = np.hstack([points, np.zeros_like(points)]) 40 | 41 | if points.shape[0] == 2: 42 | fig = plt.figure(figsize=(8, 8), dpi=300) 43 | ax = fig.gca() 44 | ax.plot(points[0], points[1], "o") 45 | 46 | ax.set_xlim([-0.1, 1.1]) 47 | ax.set_xlabel("$x$") 48 | ax.set_ylim([-0.1, 1.1]) 49 | ax.set_ylabel("$y$") 50 | elif points.shape[0] == 3: 51 | from mpl_toolkits.mplot3d import art3d # noqa: F401 52 | 53 | fig = plt.figure(figsize=(8, 8), dpi=300) 54 | ax = fig.gca(projection="3d") 55 | ax.plot(points[0], points[1], points[2], "o-") 56 | 57 | ax.set_xlim([-0.1, 1.1]) 58 | ax.set_xlabel("$x$") 59 | ax.set_ylim([-0.1, 1.1]) 60 | ax.set_ylabel("$y$") 61 | ax.set_zlim([-0.1, 1.1]) 62 | ax.set_zlabel("$z$") 63 | 64 | ax.view_init(15, 45) 65 | else: 66 | raise ValueError(f"dimension not supported: {points.shape[0]}") 67 | 68 | ax.grid() 69 | for i, p in enumerate(points.T): 70 | ax.text(*p, str(i), color="k", fontsize=12) 71 | 72 | print(f"output: {filename}.png") 73 | fig.savefig(filename) 74 | if show: 75 | plt.show(block=True) 76 | 77 | 78 | def create_sample_element(cell_type, order=3, visualize=True): 79 | from distutils.version import LooseVersion 80 | if LooseVersion(vtk.VTK_VERSION) > "8.2.0": # noqa: SIM108 81 | vtk_version = (2, 2) 82 | else: 83 | vtk_version = (2, 1) 84 | 85 | cell_type = cell_type.upper() 86 | vtk_cell_type = getattr(vtk, cell_type, None) 87 | if vtk_cell_type is None: 88 | raise ValueError(f"unknown cell type: '{cell_type}'") 89 | 90 | source = vtk.vtkCellTypeSource() 91 | source.SetCellType(vtk_cell_type) 92 | source.SetBlocksDimensions(1, 1, 1) 93 | if "LAGRANGE" in cell_type: 94 | source.SetCellOrder(order) 95 | 96 | # 0 - single precision; 1 - double precision 97 | source.SetOutputPrecision(1) 98 | source.Update() 99 | 100 | grid = source.GetOutput() 101 | cell = grid.GetCell(0) 102 | points = vtk_to_numpy(cell.GetPoints().GetData()).copy().T 103 | 104 | dim = cell.GetCellDimension() 105 | points = points[0:dim] 106 | 107 | basename = f"sample_{cell_type.lower()}" 108 | if visualize: 109 | filename = f"{basename}.vtu" 110 | 111 | print(f"vtk xml version: {vtk_version}") 112 | print(f"cell type: {cell_type}") 113 | print(f"output: {filename}") 114 | 115 | writer = vtk.vtkXMLUnstructuredGridWriter() 116 | writer.SetFileName(filename) 117 | writer.SetCompressorTypeToNone() 118 | writer.SetDataModeToAscii() 119 | writer.SetInputData(grid) 120 | writer.Write() 121 | 122 | # NOTE: vtkCellTypeSource always tessellates a square and the way it does 123 | # that to get tetrahedra changed in 124 | # https://gitlab.kitware.com/vtk/vtk/-/merge_requests/6529 125 | if LooseVersion(vtk.VTK_VERSION) > "8.2.0" \ 126 | and cell_type == "VTK_LAGRANGE_TETRAHEDRON": 127 | rot = np.array([ 128 | [1, -1, 0], 129 | [0, 1, -1], 130 | [0, 0, 2] 131 | ]) 132 | points = rot @ points 133 | 134 | if cell_type in VTK_LAGRANGE_SIMPLICES: 135 | from pyvisfile.vtk.vtk_ordering import ( 136 | vtk_lagrange_simplex_node_tuples, 137 | vtk_lagrange_simplex_node_tuples_to_permutation, 138 | ) 139 | 140 | node_tuples = vtk_lagrange_simplex_node_tuples(dim, order, 141 | vtk_version=vtk_version) 142 | vtk_lagrange_simplex_node_tuples_to_permutation(node_tuples) 143 | 144 | nodes = np.array(node_tuples) / order 145 | error = la.norm(nodes - points.T) 146 | elif cell_type in VTK_LAGRANGE_QUADS: 147 | from pyvisfile.vtk.vtk_ordering import ( 148 | vtk_lagrange_quad_node_tuples, 149 | vtk_lagrange_quad_node_tuples_to_permutation, 150 | ) 151 | 152 | node_tuples = vtk_lagrange_quad_node_tuples(dim, order, 153 | vtk_version=vtk_version) 154 | vtk_lagrange_quad_node_tuples_to_permutation(node_tuples) 155 | 156 | nodes = np.array(node_tuples) / order 157 | error = la.norm(nodes - points.T) 158 | else: 159 | error = None 160 | 161 | # NOTE: skipping the curve check because the ordering is off in the 162 | # vtkCellTypeSource output 163 | # https://gitlab.kitware.com/vtk/vtk/-/merge_requests/6555 164 | if LooseVersion(vtk.VTK_VERSION) <= "8.2.0" \ 165 | and cell_type == "VTK_LAGRANGE_CURVE": 166 | error = None 167 | 168 | if error is not None: 169 | if error < 5.0e-15: 170 | print(f"\033[92m[PASSED] order {order:2d} error {error:.5e}\033[0m") 171 | else: 172 | print(f"\033[91m[FAILED] order {order:2d} error {error:.5e}\033[0m") 173 | 174 | if not visualize: 175 | return 176 | 177 | filename = f"{basename}_vtk" 178 | plot_node_ordering(filename, points, show=False) 179 | 180 | if cell_type in (VTK_LAGRANGE_SIMPLICES + VTK_LAGRANGE_QUADS): 181 | filename = f"{basename}_pyvisfile" 182 | plot_node_ordering(filename, nodes.T, show=False) 183 | 184 | 185 | def test_order(max_order=10): 186 | for cell_type in VTK_LAGRANGE_SIMPLICES: 187 | print("cell_type:", cell_type) 188 | for order in range(1, max_order + 1): 189 | create_sample_element(cell_type, order=order, visualize=False) 190 | 191 | for cell_type in VTK_LAGRANGE_QUADS: 192 | print("cell_type:", cell_type) 193 | for order in range(1, max_order + 1): 194 | create_sample_element(cell_type, order=order, visualize=False) 195 | 196 | 197 | if __name__ == "__main__": 198 | test_order() 199 | create_sample_element("VTK_LAGRANGE_TETRAHEDRON", order=3) 200 | -------------------------------------------------------------------------------- /test/test_xdmf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | import pytools.obj_array as obj_array 7 | 8 | from pyvisfile.xdmf import DataArray, NumpyDataArray 9 | 10 | 11 | # {{{ test_unstructured_vertex_grid 12 | 13 | @pytest.mark.parametrize("ambient_dim", [2, 3]) 14 | @pytest.mark.parametrize("dformat", ["xml", "hdf", "binary"]) 15 | def test_unstructured_vertex_grid(ambient_dim: int, 16 | dformat: str, 17 | npoints: int = 64) -> None: 18 | """Test constructing a vertex grid with different ways to define the 19 | points and connectivity. 20 | """ 21 | rng = np.random.default_rng(seed=42) 22 | 23 | # {{{ set up connectivity 24 | 25 | from pyvisfile.xdmf import _data_item_from_numpy 26 | connectivity_ary = np.arange(npoints, dtype=np.uint32) 27 | points_ary = rng.random(size=(ambient_dim, npoints)) 28 | 29 | if dformat == "xml": 30 | connectivity: DataArray = NumpyDataArray(connectivity_ary, name="connectivity") 31 | points: DataArray = NumpyDataArray(points_ary.T, name="points") 32 | elif dformat in ["hdf", "binary"]: 33 | if dformat == "hdf": 34 | cdata = "geometry.h5:/Grid/Connectivity" 35 | pdata = "geometry.h5:/Grid/Points" 36 | else: 37 | cdata = "connectivity.out" 38 | pdata = "points.out" 39 | 40 | connectivity = DataArray(( 41 | _data_item_from_numpy(connectivity_ary, 42 | name="connectivity", 43 | data=cdata), 44 | )) 45 | points = DataArray(( 46 | _data_item_from_numpy(points_ary.T, 47 | name="points", 48 | data=pdata), 49 | )) 50 | else: 51 | raise ValueError(f"unknown format: '{dformat}'") 52 | 53 | # }}} 54 | 55 | # {{{ set up grids 56 | 57 | from pyvisfile.xdmf import TopologyType, XdmfUnstructuredGrid 58 | grid = XdmfUnstructuredGrid( 59 | points, connectivity, 60 | topology_type=TopologyType.Polyvertex, 61 | name="polyvertex") 62 | 63 | # }}} 64 | 65 | from pyvisfile.xdmf import XdmfWriter 66 | writer = XdmfWriter((grid,)) 67 | 68 | filename = f"test_unstructured_vertex_{dformat}_{ambient_dim}d.xmf" 69 | writer.write_pretty(filename) 70 | 71 | # }}} 72 | 73 | 74 | # {{{ test_unstructured_simplex_grid 75 | 76 | def _simplex_box_connectivity(*, 77 | npoints: tuple[int, ...], 78 | nelements: int, 79 | nvertices: int) -> NumpyDataArray: 80 | # NOTE: largely copied from meshmode/mesh/generation.py::generate_box_mesh 81 | ambient_dim = len(npoints) 82 | 83 | point_indices = np.arange(np.prod(npoints)).reshape(npoints) 84 | connectivity = np.empty((nelements, nvertices), dtype=np.uint32) 85 | 86 | ielement = 0 87 | from itertools import product 88 | if ambient_dim == 1: 89 | raise NotImplementedError 90 | elif ambient_dim == 2: 91 | for i, j in product(range(npoints[0] - 1), repeat=ambient_dim): 92 | a = point_indices[i + 0, j + 0] 93 | b = point_indices[i + 1, j + 0] 94 | c = point_indices[i + 0, j + 1] 95 | d = point_indices[i + 1, j + 1] 96 | 97 | connectivity[ielement + 0, :] = (a, b, c) 98 | connectivity[ielement + 1, :] = (d, c, b) 99 | ielement += 2 100 | elif ambient_dim == 3: 101 | for i, j, k in product(range(npoints[0] - 1), repeat=ambient_dim): 102 | a000 = point_indices[i, j, k] 103 | a001 = point_indices[i, j, k+1] 104 | a010 = point_indices[i, j+1, k] 105 | a011 = point_indices[i, j+1, k+1] 106 | 107 | a100 = point_indices[i+1, j, k] 108 | a101 = point_indices[i+1, j, k+1] 109 | a110 = point_indices[i+1, j+1, k] 110 | a111 = point_indices[i+1, j+1, k+1] 111 | 112 | connectivity[ielement + 0, :] = (a000, a100, a010, a001) 113 | connectivity[ielement + 1, :] = (a101, a100, a001, a010) 114 | connectivity[ielement + 2, :] = (a101, a011, a010, a001) 115 | 116 | connectivity[ielement + 3, :] = (a100, a010, a101, a110) 117 | connectivity[ielement + 4, :] = (a011, a010, a110, a101) 118 | connectivity[ielement + 5, :] = (a011, a111, a101, a110) 119 | ielement += 6 120 | else: 121 | raise NotImplementedError 122 | 123 | assert ielement == nelements 124 | 125 | return NumpyDataArray(connectivity, name="connectivity") 126 | 127 | 128 | @pytest.mark.parametrize("ambient_dim", [2, 3]) 129 | def test_unstructured_simplex_grid(ambient_dim: int, nelements: int = 16) -> None: 130 | """Test constructing a grid with a more complicated topology.""" 131 | 132 | from pyvisfile.xdmf import TopologyType 133 | if ambient_dim == 1: 134 | topology_type = TopologyType.Polyline 135 | simplices_per_quad = 1 136 | if ambient_dim == 2: 137 | topology_type = TopologyType.Triangle 138 | simplices_per_quad = 2 139 | elif ambient_dim == 3: 140 | topology_type = TopologyType.Tetrahedron 141 | simplices_per_quad = 6 142 | else: 143 | raise ValueError("unsupported dimension") 144 | 145 | # {{{ points and connectivity 146 | 147 | x = np.linspace(-1.0, 1.0, nelements + 1) 148 | 149 | npoints = len(x) 150 | points_ary = np.empty((ambient_dim,) + (npoints,) * ambient_dim) 151 | for idim in range(ambient_dim): 152 | points_ary[idim] = x.reshape((npoints,) + (1,) * (ambient_dim - 1 - idim)) 153 | 154 | from pyvisfile.xdmf import NumpyDataArray 155 | points = NumpyDataArray(points_ary.reshape(ambient_dim, -1).T, name="points") 156 | 157 | from pyvisfile.xdmf import _XDMF_ELEMENT_NODE_COUNT 158 | connectivity = _simplex_box_connectivity( 159 | npoints=(npoints,) * ambient_dim, 160 | nelements=simplices_per_quad * nelements**ambient_dim, 161 | nvertices=_XDMF_ELEMENT_NODE_COUNT[topology_type] 162 | ) 163 | 164 | # }}} 165 | 166 | # {{{ attributes 167 | 168 | temperature = np.sin(2.0 * np.pi * points.ary[:, 0]) \ 169 | + np.cos(2.0 * np.pi * points.ary[:, 1]) 170 | temperature = NumpyDataArray(temperature, name="temperature") 171 | 172 | velocity = points.ary + np.array([0, 1, 2][:ambient_dim]).reshape(1, -1) 173 | velocity = NumpyDataArray(velocity, name="velocity") 174 | vorticity = NumpyDataArray( 175 | obj_array.new_1d(velocity.ary), 176 | name="vorticity") 177 | 178 | # }}} 179 | 180 | # {{{ write grids 181 | 182 | from pyvisfile.xdmf import XdmfUnstructuredGrid 183 | grid = XdmfUnstructuredGrid( 184 | points, connectivity, 185 | topology_type=topology_type, 186 | name="simplex") 187 | 188 | grid.add_attribute(temperature) 189 | grid.add_attribute(velocity) 190 | grid.add_attribute(vorticity) 191 | 192 | from pyvisfile.xdmf import XdmfWriter 193 | writer = XdmfWriter((grid,)) 194 | 195 | filename = f"test_unstructured_simplex_{ambient_dim}d.xmf" 196 | writer.write_pretty(filename) 197 | 198 | # }}} 199 | 200 | 201 | if __name__ == "__main__": 202 | import sys 203 | if len(sys.argv) > 1: 204 | exec(sys.argv[1]) 205 | else: 206 | pytest.main([__file__]) 207 | 208 | # vim: fdm=marker 209 | -------------------------------------------------------------------------------- /pyvisfile/vtk/vtk_ordering.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2020 Alexandru Fikl" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | from typing import TYPE_CHECKING, overload 27 | 28 | from pytools import generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam 29 | 30 | 31 | if TYPE_CHECKING: 32 | from collections.abc import Sequence 33 | 34 | 35 | __doc__ = """ 36 | VTK High-Order Lagrange Elements 37 | -------------------------------- 38 | 39 | The high-order elements are described in 40 | `this blog post `__. 41 | The ordering of the element nodes is as follows: 42 | 43 | 1. the vertices (in an order that matches the linear elements, 44 | e.g. :data:`~pyvisfile.vtk.VTK_TRIANGLE`). 45 | 2. the interior edge (or face in 2D) nodes, i.e. without the endpoints 46 | 3. the interior face (3D only) nodes, i.e. without the edge nodes. 47 | 4. the remaining interior nodes. 48 | 49 | For simplices, the interior nodes are defined recursively by using the same 50 | rules. However, for box elements the interior nodes are just listed in 51 | order, with the last coordinate moving slowest. 52 | 53 | To a large extent, the VTK ordering matches the ordering used by ``gmsh`` and 54 | described `here `__. 55 | 56 | .. autofunction:: vtk_lagrange_simplex_node_tuples 57 | .. autofunction:: vtk_lagrange_simplex_node_tuples_to_permutation 58 | 59 | .. autofunction:: vtk_lagrange_quad_node_tuples 60 | .. autofunction:: vtk_lagrange_quad_node_tuples_to_permutation 61 | """ 62 | 63 | 64 | # {{{ 65 | 66 | @overload 67 | def add_tuple_to_list(ary: Sequence[tuple[int, int]], 68 | x: tuple[int, int]) -> Sequence[tuple[int, int]]: 69 | ... 70 | 71 | 72 | @overload 73 | def add_tuple_to_list(ary: Sequence[tuple[int, int, int]], 74 | x: tuple[int, int, int]) -> Sequence[tuple[int, int, int]]: 75 | ... 76 | 77 | 78 | def add_tuple_to_list(ary: Sequence[tuple[int, ...]], 79 | x: tuple[int, ...]) -> Sequence[tuple[int, ...]]: 80 | return [tuple([xv + yv for xv, yv in zip(x, y, strict=True)]) for y in ary] # noqa: C409 81 | 82 | # }}} 83 | 84 | 85 | # {{{ VTK_LAGRANGE_CURVE / VTK_LAGRANGE_TRIANGLE / VTK_LAGRANGE_TETRAHEDRON 86 | 87 | def vtk_lagrange_curve_node_tuples(order: int) -> Sequence[tuple[int]]: 88 | return [(0,), (order,)] + [(i,) for i in range(1, order)] 89 | 90 | 91 | def vtk_lagrange_triangle_node_tuples(order: int) -> Sequence[tuple[int, int]]: 92 | nodes: list[tuple[int, int]] = [] 93 | offset = (0, 0) 94 | 95 | if order < 0: 96 | return nodes 97 | 98 | while order >= 0: 99 | if order == 0: 100 | nodes += [offset] 101 | break 102 | 103 | # y 104 | # ^ 105 | # | 106 | # 2 107 | # |`\ 108 | # | `\ 109 | # | `\ 110 | # | `\ 111 | # | `\ 112 | # 0----------1 --> x 113 | 114 | # add vertices 115 | vertices = [(0, 0), (order, 0), (0, order)] 116 | nodes += add_tuple_to_list(vertices, offset) 117 | if order == 1: 118 | break 119 | 120 | # add faces 121 | face_ids = range(1, order) 122 | faces = ( 123 | # vertex 0 -> 1 124 | [(i, 0) for i in face_ids] 125 | # vertex 1 -> 2 126 | + [(order - i, i) for i in face_ids] 127 | # vertex 2 -> 0 128 | + [(0, order - i) for i in face_ids]) 129 | nodes += add_tuple_to_list(faces, offset) 130 | 131 | order = order - 3 132 | offset = (offset[0] + 1, offset[1] + 1) 133 | 134 | return nodes 135 | 136 | 137 | def vtk_lagrange_tetrahedron_node_tuples( 138 | order: int) -> Sequence[tuple[int, int, int]]: 139 | nodes = [] 140 | offset = (0, 0, 0) 141 | 142 | while order >= 0: 143 | if order == 0: 144 | nodes += [offset] 145 | break 146 | 147 | # z 148 | # ,/ 149 | # 3 150 | # ,/|`\ 151 | # ,/ | `\ 152 | # ,/ '. `\ 153 | # ,/ | `\ 154 | # ,/ | `\ 155 | # 0-----------'.--------2---> y 156 | # `\. | ,/ 157 | # `\. | ,/ 158 | # `\. '. ,/ 159 | # `\. |/ 160 | # `1 161 | # `. 162 | # x 163 | 164 | vertices = [(0, 0, 0), (order, 0, 0), (0, order, 0), (0, 0, order)] 165 | nodes += add_tuple_to_list(vertices, offset) 166 | if order == 1: 167 | break 168 | 169 | # add edges 170 | edge_ids = range(1, order) 171 | edges = ( 172 | # vertex 0 -> 1 173 | [(i, 0, 0) for i in edge_ids] 174 | # vertex 1 -> 2 175 | + [(order - i, i, 0) for i in edge_ids] 176 | # vertex 2 -> 0 177 | + [(0, order - i, 0) for i in edge_ids] 178 | # vertex 0 -> 3 179 | + [(0, 0, i) for i in edge_ids] 180 | # vertex 1 -> 3 181 | + [(order - i, 0, i) for i in edge_ids] 182 | # vertex 2 -> 3 183 | + [(0, order - i, i) for i in edge_ids]) 184 | nodes += add_tuple_to_list(edges, offset) 185 | 186 | # add faces 187 | face_ids = add_tuple_to_list( 188 | vtk_lagrange_triangle_node_tuples(order - 3), (1, 1)) 189 | faces = ( 190 | # face between vertices (0, 2, 3) 191 | [(i, 0, j) for i, j in face_ids] 192 | # face between vertices (1, 2, 3) 193 | + [(j, order - (i + j), i) for i, j in face_ids] 194 | # face between vertices (0, 1, 3) 195 | + [(0, j, i) for i, j in face_ids] 196 | # face between vertices (0, 1, 2) 197 | + [(j, i, 0) for i, j in face_ids]) 198 | nodes += add_tuple_to_list(faces, offset) 199 | 200 | order = order - 4 201 | offset = (offset[0] + 1, offset[1] + 1, offset[2] + 1) 202 | 203 | return nodes 204 | 205 | 206 | def vtk_lagrange_simplex_node_tuples( 207 | dims: int, order: int, 208 | vtk_version: tuple[int, int] = (2, 1) 209 | ) -> Sequence[tuple[int, ...]]: 210 | """ 211 | :arg dims: dimension of the simplex, i.e. 1 corresponds to a curve, 2 to 212 | a triangle, etc. 213 | :arg order: order of the polynomial representation, which also defines 214 | the number of nodes on the simplex. 215 | :arg vtk_version: a :class:`tuple` of two elements containing the version 216 | of the VTK XML file format in use. The ordering of some of the 217 | high-order elements changed between versions `2.1` and `2.2`. 218 | 219 | :return: a :class:`list` of ``dims``-dimensional tuples of integers 220 | up to ``order`` in the ordering expected by VTK. 221 | """ 222 | if dims == 1: 223 | return vtk_lagrange_curve_node_tuples(order) 224 | elif dims == 2: 225 | return vtk_lagrange_triangle_node_tuples(order) 226 | elif dims == 3: 227 | return vtk_lagrange_tetrahedron_node_tuples(order) 228 | else: 229 | raise ValueError(f"unsupported dimension: {dims}") 230 | 231 | 232 | def vtk_lagrange_simplex_node_tuples_to_permutation( 233 | node_tuples: Sequence[tuple[int, ...]] 234 | ) -> Sequence[int]: 235 | """Construct a permutation from the simplex node ordering of VTK to that of 236 | :mod:`modepy`. 237 | 238 | :returns: a :class:`list` of indices in ``[0, len(node_tuples)]``. 239 | """ 240 | order = max(max(i) for i in node_tuples) 241 | dims = len(node_tuples[0]) 242 | 243 | node_to_index = { 244 | node_tuple: i 245 | for i, node_tuple in enumerate(gnitstam(order, dims)) 246 | } 247 | 248 | assert len(node_tuples) == len(node_to_index) 249 | return [node_to_index[v] for v in node_tuples] 250 | 251 | # }}} 252 | 253 | 254 | # {{{ VTK_LAGRANGE_CURVE / VTK_LAGRANGE_QUADRILATERAL / VTK_LAGRANGE_HEXAHEDRON 255 | 256 | def vtk_lagrange_quadrilateral_node_tuples(order: int) -> Sequence[tuple[int, int]]: 257 | nodes: list[tuple[int, int]] = [] 258 | 259 | if order < 0: 260 | return nodes 261 | 262 | if order == 0: 263 | return [(0, 0)] 264 | 265 | # y 266 | # ^ 267 | # | 268 | # 3----------2 269 | # | | 270 | # | | 271 | # | | 272 | # | | 273 | # | | 274 | # 0----------1 --> x 275 | 276 | # add vertices 277 | nodes += [(0, 0), (order, 0), (order, order), (0, order)] 278 | if order == 1: 279 | return nodes 280 | 281 | # add faces 282 | face_ids = range(1, order) 283 | nodes += ( 284 | # vertex 0 -> 1 285 | [(i, 0) for i in face_ids] 286 | # vertex 1 -> 2 287 | + [(order, i) for i in face_ids] 288 | # vertex 2 -> 3 289 | + [(i, order) for i in face_ids] 290 | # vertex 3 -> 0 291 | + [(0, i) for i in face_ids]) 292 | 293 | # add remaining interior nodes 294 | from itertools import product 295 | nodes += [(i, j) for j, i in product(range(1, order), repeat=2)] 296 | 297 | return nodes 298 | 299 | 300 | def vtk_lagrange_hexahedon_node_tuples( 301 | order: int, 302 | vtk_version: tuple[int, int] = (2, 1)) -> Sequence[tuple[int, int, int]]: 303 | nodes: list[tuple[int, int, int]] = [] 304 | 305 | if order < 0: 306 | return nodes 307 | 308 | if order == 0: 309 | return [(0, 0, 0)] 310 | 311 | # z 312 | # ^ 313 | # | 314 | # 4----------7 315 | # |\ |\ 316 | # | \ | \ 317 | # | \ | \ 318 | # | 5------+---6 319 | # | | | | 320 | # 0---+------3---|--> y 321 | # \ | \ | 322 | # \ | \ | 323 | # \| \| 324 | # 1----------2 325 | # \ 326 | # v x 327 | 328 | # add vertices 329 | nodes += [ 330 | # (0, 1, 2, 3) 331 | (0, 0, 0), (order, 0, 0), 332 | (order, order, 0), (0, order, 0), 333 | # (4, 5, 6, 7) 334 | (0, 0, order), (order, 0, order), 335 | (order, order, order), (0, order, order), 336 | ] 337 | if order == 1: 338 | return nodes 339 | 340 | # add edges 341 | edge_ids = range(1, order) 342 | nodes += ( 343 | # vertex 0 -> 1 344 | [(i, 0, 0) for i in edge_ids] 345 | # vertex 1 -> 2 346 | + [(order, i, 0) for i in edge_ids] 347 | # vertex 2 -> 3 348 | + [(i, order, 0) for i in edge_ids] 349 | # vertex 3 -> 0 350 | + [(0, i, 0) for i in edge_ids] 351 | 352 | # vertex 4 -> 5 353 | + [(i, 0, order) for i in edge_ids] 354 | # vertex 5 -> 6 355 | + [(order, i, order) for i in edge_ids] 356 | # vertex 6 -> 7 357 | + [(i, order, order) for i in edge_ids] 358 | # vertex 7 -> 4 359 | + [(0, i, order) for i in edge_ids] 360 | 361 | # vertex 0 -> 4 362 | + [(0, 0, i) for i in edge_ids] 363 | # vertex 1 -> 5 364 | + [(order, 0, i) for i in edge_ids] 365 | ) 366 | 367 | if vtk_version <= (2, 1): 368 | nodes += ( 369 | # vertex 3 -> 7 370 | [(0, order, i) for i in edge_ids] 371 | # vertex 2 -> 6 372 | + [(order, order, i) for i in edge_ids] 373 | ) 374 | else: 375 | nodes += ( 376 | # vertex 2 -> 6 377 | [(order, order, i) for i in edge_ids] 378 | # vertex 3 -> 7 379 | + [(0, order, i) for i in edge_ids] 380 | ) 381 | 382 | # add faces 383 | from itertools import product 384 | nodes += ( 385 | # face between (0, 4, 7, 3) 386 | [(0, i, j) for j, i in product(range(1, order), repeat=2)] 387 | # face between (1, 5, 6, 2) 388 | + [(order, i, j) for j, i in product(range(1, order), repeat=2)] 389 | # face between (0, 1, 5, 4) 390 | + [(i, 0, j) for j, i in product(range(1, order), repeat=2)] 391 | # face between (3, 2, 6, 7) 392 | + [(i, order, j) for j, i in product(range(1, order), repeat=2)] 393 | # face between (0, 1, 2, 3) 394 | + [(i, j, 0) for j, i in product(range(1, order), repeat=2)] 395 | # face between (4, 5, 6, 7) 396 | + [(i, j, order) for j, i in product(range(1, order), repeat=2)] 397 | ) 398 | 399 | # add interior 400 | nodes += [(i, j, k) for k, j, i in product(range(1, order), repeat=3)] 401 | 402 | return nodes 403 | 404 | 405 | def vtk_lagrange_quad_node_tuples( 406 | dims: int, order: int, 407 | vtk_version: tuple[int, int] = (2, 1), 408 | ) -> Sequence[tuple[int, ...]]: 409 | """ 410 | :arg dims: dimension of the box, i.e. 1 corresponds to a curve, 2 to 411 | a quadrilateral, and 3 to a hexahedron. 412 | :arg order: order of the polynomial representation, which also defines 413 | the number of nodes on the box. 414 | :arg vtk_version: a :class:`tuple` of two elements containing the version 415 | of the VTK XML file format in use. The ordering of some of the 416 | high-order elements changed between versions `2.1` and `2.2`. 417 | 418 | :return: a :class:`list` of ``dims``-dimensional tuples of integers 419 | up to ``order`` in the ordering expected by VTK. 420 | """ 421 | if dims == 1: 422 | return vtk_lagrange_curve_node_tuples(order) 423 | elif dims == 2: 424 | return vtk_lagrange_quadrilateral_node_tuples(order) 425 | elif dims == 3: 426 | return vtk_lagrange_hexahedon_node_tuples(order, vtk_version=vtk_version) 427 | else: 428 | raise ValueError(f"unsupported dimension: {dims}") 429 | 430 | 431 | def vtk_lagrange_quad_node_tuples_to_permutation( 432 | node_tuples: Sequence[tuple[int, ...]] 433 | ) -> Sequence[int]: 434 | """Construct a permutation from the quad node ordering of VTK to that of 435 | :mod:`modepy`. 436 | 437 | :returns: a :class:`list` of indices in ``[0, len(node_tuples)]``. 438 | """ 439 | order = max(max(i) for i in node_tuples) 440 | dims = len(node_tuples[0]) 441 | 442 | from itertools import product 443 | node_to_index = { 444 | node_tuple: i 445 | for i, node_tuple in enumerate(product(range(order + 1), repeat=dims)) 446 | } 447 | 448 | assert len(node_tuples) == len(node_to_index) 449 | return [node_to_index[v] for v in node_tuples] 450 | 451 | # }}} 452 | -------------------------------------------------------------------------------- /pyvisfile/vtk/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | """Generic support for new-style (XML) VTK visualization data files.""" 5 | 6 | __copyright__ = "Copyright (C) 2007 Andreas Kloeckner" 7 | 8 | __license__ = """ 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | """ 27 | 28 | import pathlib 29 | from abc import ABC, abstractmethod 30 | from typing import TYPE_CHECKING, Any, ClassVar, TextIO, TypeAlias, cast 31 | 32 | import numpy as np 33 | from typing_extensions import Buffer 34 | 35 | import pytools.obj_array as obj_array 36 | 37 | 38 | if TYPE_CHECKING: 39 | from collections.abc import ByteString, Sequence 40 | 41 | 42 | __doc__ = """ 43 | 44 | Constants 45 | --------- 46 | 47 | Vector formats 48 | ^^^^^^^^^^^^^^ 49 | 50 | .. data:: VF_LIST_OF_COMPONENTS 51 | 52 | ``[[x0, y0, z0], [x1, y1, z1]]`` 53 | 54 | .. data:: VF_LIST_OF_VECTORS 55 | 56 | ``[[x0, x1], [y0, y1], [z0, z1]]`` 57 | 58 | Element types 59 | ^^^^^^^^^^^^^ 60 | 61 | .. data:: VTK_VERTEX 62 | .. data:: VTK_POLY_VERTEX 63 | .. data:: VTK_LINE 64 | .. data:: VTK_POLY_LINE 65 | .. data:: VTK_TRIANGLE 66 | .. data:: VTK_TRIANGLE_STRIP 67 | .. data:: VTK_POLYGON 68 | .. data:: VTK_PIXEL 69 | .. data:: VTK_QUAD 70 | .. data:: VTK_TETRA 71 | .. data:: VTK_VOXEL 72 | .. data:: VTK_HEXAHEDRON 73 | .. data:: VTK_WEDGE 74 | .. data:: VTK_PYRAMID 75 | 76 | .. data:: VTK_LAGRANGE_CURVE 77 | .. data:: VTK_LAGRANGE_TRIANGLE 78 | .. data:: VTK_LAGRANGE_QUADRILATERAL 79 | .. data:: VTK_LAGRANGE_TETRAHEDRON 80 | .. data:: VTK_LAGRANGE_HEXAHEDRON 81 | .. data:: VTK_LAGRANGE_WEDGE 82 | 83 | XML elements 84 | ^^^^^^^^^^^^^^ 85 | 86 | .. autoclass:: XMLElementBase 87 | .. autoclass:: XMLElement 88 | :show-inheritance: 89 | .. autoclass:: XMLRoot 90 | :show-inheritance: 91 | 92 | Binary encoders 93 | ^^^^^^^^^^^^^^^ 94 | 95 | .. autoclass:: EncodedBuffer 96 | .. autoclass:: BinaryEncodedBuffer 97 | :show-inheritance: 98 | .. autoclass:: Base64EncodedBuffer 99 | :show-inheritance: 100 | .. autoclass:: Base64ZLibEncodedBuffer 101 | :show-inheritance: 102 | 103 | Building blocks 104 | --------------- 105 | 106 | .. autoclass:: Visitable 107 | .. autoclass:: DataArray 108 | :show-inheritance: 109 | .. autoclass:: UnstructuredGrid 110 | :show-inheritance: 111 | .. autoclass:: StructuredGrid 112 | :show-inheritance: 113 | 114 | XML generators 115 | ^^^^^^^^^^^^^^ 116 | 117 | .. autoclass:: XMLGenerator 118 | .. autoclass:: InlineXMLGenerator 119 | :show-inheritance: 120 | .. autoclass:: AppendedDataXMLGenerator 121 | :show-inheritance: 122 | .. autoclass:: ParallelXMLGenerator 123 | :show-inheritance: 124 | 125 | Convenience functions 126 | --------------------- 127 | 128 | .. autofunction:: write_structured_grid 129 | 130 | Type aliases 131 | ------------ 132 | 133 | .. class:: Child 134 | 135 | A type alias, union of :class:`str` and :class:`XMLElement`. 136 | """ 137 | 138 | # {{{ types 139 | 140 | VTK_INT8 = "Int8" 141 | VTK_UINT8 = "UInt8" 142 | VTK_INT16 = "Int16" 143 | VTK_UINT16 = "UInt16" 144 | VTK_INT32 = "Int32" 145 | VTK_UINT32 = "UInt32" 146 | VTK_INT64 = "Int64" 147 | VTK_UINT64 = "UInt64" 148 | VTK_FLOAT32 = "Float32" 149 | VTK_FLOAT64 = "Float64" 150 | 151 | 152 | NUMPY_TO_VTK_TYPES = { 153 | np.int8: VTK_INT8, 154 | np.uint8: VTK_UINT8, 155 | np.int16: VTK_INT16, 156 | np.uint16: VTK_UINT16, 157 | np.int32: VTK_INT32, 158 | np.uint32: VTK_UINT32, 159 | np.int64: VTK_INT64, 160 | np.uint64: VTK_UINT64, 161 | np.float32: VTK_FLOAT32, 162 | np.float64: VTK_FLOAT64, 163 | } 164 | 165 | # }}} 166 | 167 | 168 | # {{{ cell types 169 | 170 | # NOTE: should keep in sync with 171 | # https://gitlab.kitware.com/vtk/vtk/-/blob/master/Common/DataModel/vtkCellType.h 172 | 173 | # linear cells 174 | VTK_VERTEX = 1 175 | VTK_POLY_VERTEX = 2 176 | VTK_LINE = 3 177 | VTK_POLY_LINE = 4 178 | VTK_TRIANGLE = 5 179 | VTK_TRIANGLE_STRIP = 6 180 | VTK_POLYGON = 7 181 | VTK_PIXEL = 8 182 | VTK_QUAD = 9 183 | VTK_TETRA = 10 184 | VTK_VOXEL = 11 185 | VTK_HEXAHEDRON = 12 186 | VTK_WEDGE = 13 187 | VTK_PYRAMID = 14 188 | 189 | # NOTE: these were added in VTK 8.1 as part of the commit 190 | # https://gitlab.kitware.com/vtk/vtk/-/commit/cc5101a805386f205631357bba782b2a7d17531a 191 | 192 | # high-order Lagrange cells 193 | VTK_LAGRANGE_CURVE = 68 194 | VTK_LAGRANGE_TRIANGLE = 69 195 | VTK_LAGRANGE_QUADRILATERAL = 70 196 | VTK_LAGRANGE_TETRAHEDRON = 71 197 | VTK_LAGRANGE_HEXAHEDRON = 72 198 | VTK_LAGRANGE_WEDGE = 73 199 | 200 | # }}} 201 | 202 | 203 | # {{{ cell node counts 204 | 205 | CELL_NODE_COUNT = { 206 | VTK_VERTEX: 1, 207 | # VTK_POLY_VERTEX: no a-priori size 208 | VTK_LINE: 2, 209 | # VTK_POLY_LINE: no a-priori size 210 | VTK_TRIANGLE: 3, 211 | # VTK_TRIANGLE_STRIP: no a-priori size 212 | # VTK_POLYGON: no a-priori size 213 | VTK_PIXEL: 4, 214 | VTK_QUAD: 4, 215 | VTK_TETRA: 4, 216 | VTK_VOXEL: 8, 217 | VTK_HEXAHEDRON: 8, 218 | VTK_WEDGE: 6, 219 | VTK_PYRAMID: 5, 220 | # VTK_LAGRANGE_CURVE: no a-priori size 221 | # VTK_LAGRANGE_TRIANGLE: no a-priori size 222 | # VTK_LAGRANGE_QUADRILATERAL: no a-priori size 223 | # VTK_LAGRANGE_TETRAHEDRON: no a-priori size 224 | # VTK_LAGRANGE_HEXAHEDRON: no a-priori size 225 | # VTK_LAGRANGE_WEDGE: no a-priori size 226 | } 227 | 228 | # }}} 229 | 230 | 231 | # {{{ vector format 232 | 233 | # e.g. [[x0, y0, z0], [x1, y1, z1]] 234 | VF_LIST_OF_COMPONENTS = 0 235 | # e.g. [[x0, x1], [y0, y1], [z0, z1]] 236 | VF_LIST_OF_VECTORS = 1 237 | 238 | # }}} 239 | 240 | 241 | # {{{ xml 242 | 243 | # Ah, the joys of home-baked non-compliant XML goodness. 244 | 245 | Child: TypeAlias = "str | XMLElement" 246 | 247 | 248 | class XMLElementBase: 249 | """Base type for XML elements. 250 | 251 | .. attribute:: children 252 | :type: List[Union[str, XMLElement]] 253 | 254 | .. automethod:: add_child 255 | """ 256 | 257 | def __init__(self) -> None: 258 | self.children: list[Child] = [] 259 | 260 | def add_child(self, child: Child) -> None: 261 | """Append a new child to the current element.""" 262 | self.children.append(child) 263 | 264 | 265 | class XMLElement(XMLElementBase): 266 | """ 267 | .. automethod:: __init__ 268 | .. automethod:: copy 269 | .. automethod:: write 270 | """ 271 | 272 | def __init__(self, tag: str, **attributes: Any) -> None: 273 | super().__init__() 274 | self.tag = tag 275 | self.attributes = attributes 276 | 277 | def copy(self, children: Sequence[Child] | None = None) -> XMLElement: 278 | """Make a copy of the element with new children.""" 279 | if children is None: 280 | children = self.children 281 | 282 | result = type(self)(self.tag, **self.attributes) 283 | for child in children: 284 | result.add_child(child) 285 | 286 | return result 287 | 288 | def write(self, fd: TextIO) -> None: 289 | """Write the current element and all of its children to a file. 290 | 291 | :arg fd: a file descriptor or another object exposing the required methods. 292 | """ 293 | attr_string = "".join( 294 | f' {key}="{value}"' 295 | for key, value in self.attributes.items()) 296 | 297 | if self.children: 298 | fd.write(f"<{self.tag}{attr_string}>\n") 299 | for child in self.children: 300 | if isinstance(child, XMLElement): 301 | child.write(fd) 302 | else: 303 | # likely a string instance, write it directly 304 | fd.write(child) 305 | fd.write(f"\n") 306 | else: 307 | # NOTE: this one has an extra /> at the end 308 | fd.write(f"<{self.tag}{attr_string}/>\n") 309 | 310 | 311 | class XMLRoot(XMLElementBase): 312 | """ 313 | .. automethod:: __init__ 314 | .. automethod:: write 315 | """ 316 | 317 | def __init__(self, child: Child | None = None) -> None: 318 | super().__init__() 319 | if child: 320 | self.add_child(child) 321 | 322 | def write(self, fd: TextIO) -> None: 323 | """Write the current root and all of its children to a file. 324 | 325 | :arg fd: a file descriptor or another object exposing the required methods. 326 | """ 327 | fd.write('\n') 328 | 329 | for child in self.children: 330 | if isinstance(child, XMLElement): 331 | child.write(fd) 332 | else: 333 | # likely a string instance, write it directly 334 | fd.write(child) 335 | 336 | # }}} 337 | 338 | 339 | # {{{ encoded buffers 340 | 341 | _U32CHAR = np.dtype(np.uint32).char 342 | 343 | 344 | class EncodedBuffer(ABC): 345 | """An interface for binary buffers for XML data (inline and appended). 346 | 347 | .. automethod:: encoder 348 | .. automethod:: compressor 349 | .. automethod:: raw_buffer 350 | .. automethod:: add_to_xml_element 351 | """ 352 | 353 | @abstractmethod 354 | def encoder(self) -> str: 355 | """An identifier for the binary encoding used.""" 356 | 357 | @abstractmethod 358 | def compressor(self) -> str | None: 359 | """An identifier for the compressor used or *None*.""" 360 | 361 | @abstractmethod 362 | def raw_buffer(self) -> ByteString: 363 | """The raw buffer object that was used to construct this encoded buffer.""" 364 | 365 | @abstractmethod 366 | def add_to_xml_element(self, xml_element: XMLElement) -> int: 367 | """Add encoded buffer to the given *xml_element*. 368 | 369 | :returns: total size of encoded buffer in bytes. 370 | """ 371 | 372 | 373 | class BinaryEncodedBuffer(EncodedBuffer): 374 | """An encoded buffer that uses raw uncompressed binary data. 375 | 376 | .. automethod:: __init__ 377 | """ 378 | 379 | def __init__(self, buffer: ByteString) -> None: 380 | self.buffer = buffer 381 | 382 | def encoder(self) -> str: 383 | return "binary" 384 | 385 | def compressor(self) -> str | None: 386 | return None 387 | 388 | def raw_buffer(self) -> ByteString: 389 | return self.buffer 390 | 391 | def add_to_xml_element(self, xml_element: XMLElement) -> int: 392 | raise NotImplementedError 393 | 394 | 395 | class Base64EncodedBuffer(EncodedBuffer): 396 | """An encoded buffer that uses :mod:`base64` data. 397 | 398 | .. automethod:: __init__ 399 | """ 400 | 401 | def __init__(self, buffer: memoryview) -> None: 402 | from base64 import b64encode 403 | from struct import pack 404 | 405 | length = buffer.nbytes 406 | self.b64header = b64encode(pack(_U32CHAR, length)).decode() 407 | self.b64data = b64encode(buffer).decode() 408 | 409 | def encoder(self) -> str: 410 | return "base64" 411 | 412 | def compressor(self) -> str | None: 413 | return None 414 | 415 | def raw_buffer(self) -> ByteString: 416 | from base64 import b64decode 417 | return b64decode(self.b64data) 418 | 419 | def add_to_xml_element(self, xml_element: XMLElement) -> int: 420 | xml_element.add_child(self.b64header) 421 | xml_element.add_child(self.b64data) 422 | 423 | return len(self.b64header) + len(self.b64data) 424 | 425 | 426 | class Base64ZLibEncodedBuffer(EncodedBuffer): 427 | """An encoded buffer that uses :mod:`base64` and :mod:`zlib` compression. 428 | 429 | .. automethod:: __init__ 430 | """ 431 | 432 | def __init__(self, buffer: ByteString) -> None: 433 | from base64 import b64encode 434 | from struct import pack 435 | from zlib import compress 436 | 437 | comp_buffer = compress(buffer) 438 | comp_header = [1, len(buffer), len(buffer), len(comp_buffer)] 439 | 440 | self.b64header = b64encode(pack(_U32CHAR*len(comp_header), *comp_header)) 441 | self.b64data = b64encode(comp_buffer) 442 | 443 | def encoder(self) -> str: 444 | return "base64" 445 | 446 | def compressor(self) -> str | None: 447 | return "zlib" 448 | 449 | def raw_buffer(self) -> ByteString: 450 | from base64 import b64decode 451 | from zlib import decompress 452 | return decompress(b64decode(self.b64data)) 453 | 454 | def add_to_xml_element(self, xml_element: XMLElement) -> int: 455 | xml_element.add_child(self.b64header.decode()) 456 | xml_element.add_child(self.b64data.decode()) 457 | 458 | return len(self.b64header) + len(self.b64data) 459 | 460 | # }}} 461 | 462 | 463 | # {{{ data array 464 | 465 | class Visitable: 466 | """A generic class for objects that can be mapped to XML elements. 467 | 468 | .. autoattribute:: generator_method 469 | .. automethod:: invoke_visitor 470 | """ 471 | 472 | #: Name of the method called in :meth:`invoke_visitor`. 473 | generator_method: ClassVar[str] 474 | 475 | def invoke_visitor(self, visitor: XMLGenerator) -> XMLElement: 476 | """Visit the current object with the given *visitor* and generate the 477 | corresponding XML element. 478 | """ 479 | method = getattr(visitor, self.generator_method, None) 480 | if method is None: 481 | raise TypeError( 482 | f"{type(visitor).__name__} does not support {type(self)}" 483 | ) 484 | 485 | return cast("XMLElement", method(self)) 486 | 487 | 488 | class DataArray(Visitable): 489 | """A representation of a generic VTK DataArray. 490 | 491 | The storage format (inline or appended) is determined by the 492 | :class:`XMLGenerator` at writing time. 493 | 494 | .. automethod:: __init__ 495 | .. automethod:: get_encoded_buffer 496 | .. automethod:: encode 497 | """ 498 | 499 | generator_method = "gen_data_array" 500 | 501 | def __init__(self, 502 | name: str, 503 | container: Any, 504 | vector_padding: int = 3, 505 | vector_format: int = VF_LIST_OF_COMPONENTS, 506 | components: int | None = None) -> None: 507 | """ 508 | :arg name: name of the data array. 509 | :arg container: a :class:`numpy.ndarray` or another :class:`DataArray`. 510 | :arg vector_padding: pad any :class:`~numpy.ndarray` with additional 511 | zeros given by this variable. 512 | :arg vector_format: :data:`VF_LIST_OF_COMPONENTS` or 513 | :data:`VF_LIST_OF_VECTORS`. 514 | :arg components: number of components in the container (not used). 515 | """ 516 | self.name = name 517 | 518 | if isinstance(container, DataArray): 519 | self.type: str | None = container.type 520 | self.components: int = container.components 521 | self.encoded_buffer: EncodedBuffer = container.encoded_buffer 522 | return 523 | elif isinstance(container, np.ndarray): 524 | # NOTE: handled below 525 | pass 526 | else: 527 | raise ValueError( 528 | f"Cannot convert object of type '{type(container)}' to DataArray") 529 | 530 | if vector_format not in (VF_LIST_OF_COMPONENTS, VF_LIST_OF_VECTORS): 531 | raise ValueError(f"Unknown vector format: {vector_format}") 532 | 533 | if container.dtype.char == "O": 534 | for subvec in container: 535 | if not isinstance(subvec, np.ndarray): 536 | raise TypeError( 537 | f"Expected a numpy array, got '{type(subvec)}' instead") 538 | 539 | container = np.array(list(container)) 540 | assert container.dtype.char != "O" 541 | 542 | if len(container.shape) > 1: 543 | if vector_format == VF_LIST_OF_COMPONENTS: 544 | container = container.T.copy() 545 | 546 | if len(container.shape) != 2: 547 | raise ValueError("numpy vectors of rank>2 are not supported") 548 | if container.size and container.strides[1] != container.itemsize: 549 | raise ValueError("2D numpy arrays must be row-major") 550 | 551 | if vector_padding > container.shape[1]: 552 | container = np.asarray(np.hstack(( 553 | container, 554 | np.zeros(( 555 | container.shape[0], 556 | vector_padding-container.shape[1], 557 | ), 558 | container.dtype))), order="C") 559 | self.components = container.shape[1] 560 | else: 561 | self.components = 1 562 | 563 | self.type = NUMPY_TO_VTK_TYPES.get(container.dtype.type) 564 | if self.type is None: 565 | raise TypeError(f"Unsupported array dtype: '{container.dtype}'") 566 | 567 | if not container.flags.c_contiguous: 568 | container = container.copy() 569 | 570 | buf = memoryview(cast("Buffer", container)) 571 | self.encoded_buffer = BinaryEncodedBuffer(buf) 572 | 573 | def get_encoded_buffer(self, 574 | encoder: str, 575 | compressor: str | None = None) -> EncodedBuffer: 576 | """Re-encode the underlying buffer of the current :class:`DataArray`. 577 | 578 | :arg encoder: new encoder name. 579 | :arg compressor: new compressor name. 580 | """ 581 | have_encoder = self.encoded_buffer.encoder() 582 | have_compressor = self.encoded_buffer.compressor() 583 | 584 | if (encoder, compressor) != (have_encoder, have_compressor): 585 | raw_buf = self.encoded_buffer.raw_buffer() 586 | 587 | # avoid having three copies of the buffer around temporarily 588 | del self.encoded_buffer 589 | 590 | if (encoder, compressor) == ("binary", None): 591 | self.encoded_buffer = BinaryEncodedBuffer(raw_buf) 592 | elif (encoder, compressor) == ("base64", None): 593 | assert isinstance(raw_buf, memoryview) 594 | self.encoded_buffer = Base64EncodedBuffer(raw_buf) 595 | elif (encoder, compressor) == ("base64", "zlib"): 596 | self.encoded_buffer = Base64ZLibEncodedBuffer(raw_buf) 597 | else: 598 | self.encoded_buffer = BinaryEncodedBuffer(raw_buf) 599 | raise ValueError("invalid encoder/compressor pair") 600 | 601 | have_encoder = self.encoded_buffer.encoder() 602 | have_compressor = self.encoded_buffer.compressor() 603 | 604 | assert (encoder, compressor) == (have_encoder, have_compressor) 605 | 606 | return self.encoded_buffer 607 | 608 | def encode(self, compressor: str | None, xml_element: XMLElement) -> int: 609 | """Encode the underlying buffer with the given compressor and add it 610 | to the *xml_element*. 611 | 612 | The re-encoding is performed using :meth:`get_encoded_buffer` and the 613 | result is added to the element using 614 | :meth:`EncodedBuffer.add_to_xml_element`. The encoding is always done 615 | in :mod:`base64`. 616 | """ 617 | 618 | ebuf = self.get_encoded_buffer("base64", compressor) 619 | return ebuf.add_to_xml_element(xml_element) 620 | 621 | # }}} 622 | 623 | 624 | # {{{ grids 625 | 626 | class UnstructuredGrid(Visitable): 627 | """ 628 | .. automethod:: __init__ 629 | 630 | .. automethod:: vtk_extension 631 | .. automethod:: add_pointdata 632 | .. automethod:: add_celldata 633 | """ 634 | 635 | generator_method = "gen_unstructured_grid" 636 | 637 | def __init__(self, 638 | points: tuple[int, DataArray], 639 | cells: ( 640 | np.ndarray[Any, np.dtype[Any]] 641 | | tuple[int, DataArray, DataArray]), 642 | cell_types: np.ndarray[Any, np.dtype[Any]] | DataArray) -> None: 643 | """ 644 | :arg points: a tuple containing the point count and a :class:`DataArray` 645 | with the actual coordinates. 646 | :arg cells: if it is only an :class:`~numpy.ndarray`, then it is assumed 647 | that all the cells in the grid are uniform and have a fixed number 648 | of vertices. Otherwise, a tuple of ``(ncells, connectivity, offsets)`` 649 | should be provided. 650 | :arg cell_types: a :class:`DataArray` or :class:`~numpy.ndarray` of 651 | cell types. 652 | """ 653 | self.point_count, self.points = points 654 | assert self.points.name == "points" 655 | 656 | if isinstance(cells, tuple) and len(cells) == 3: 657 | self.cell_count, self.cell_connectivity, self.cell_offsets = cells 658 | elif isinstance(cells, np.ndarray): 659 | assert not isinstance(cell_types, DataArray) 660 | self.cell_count = len(cell_types) 661 | 662 | if self.cell_count > 0: 663 | offsets = np.cumsum( 664 | np.vectorize(CELL_NODE_COUNT.get)(cell_types), 665 | dtype=cells.dtype) 666 | else: 667 | offsets = np.empty((0,), dtype=cells.dtype) 668 | 669 | self.cell_connectivity = DataArray("connectivity", cells) 670 | self.cell_offsets = DataArray("offsets", offsets) 671 | else: 672 | raise TypeError(f"Unsupported 'cells' type: {type(cells)}") 673 | 674 | self.cell_types = DataArray("types", cell_types) 675 | 676 | self.pointdata: list[DataArray] = [] 677 | self.celldata: list[DataArray] = [] 678 | 679 | def copy(self) -> UnstructuredGrid: 680 | return UnstructuredGrid( 681 | (self.point_count, self.points), 682 | (self.cell_count, self.cell_connectivity, self.cell_offsets), 683 | self.cell_types) 684 | 685 | def vtk_extension(self) -> str: 686 | """Recommended extension for unstructured VTK grids.""" 687 | return "vtu" 688 | 689 | def add_pointdata(self, data_array: DataArray) -> None: 690 | """Add point data to the grid.""" 691 | self.pointdata.append(data_array) 692 | 693 | def add_celldata(self, data_array: DataArray) -> None: 694 | """Add cell data to the grid.""" 695 | self.celldata.append(data_array) 696 | 697 | 698 | class StructuredGrid(Visitable): 699 | """ 700 | .. automethod:: __init__ 701 | 702 | .. automethod:: vtk_extension 703 | .. automethod:: add_pointdata 704 | .. automethod:: add_celldata 705 | """ 706 | 707 | generator_method = "gen_structured_grid" 708 | 709 | def __init__(self, mesh: np.ndarray[Any, np.dtype[Any]]) -> None: 710 | """ 711 | :arg mesh: has shape ``(ndims, nx, ny, nz)``, depending on the dimension. 712 | """ 713 | self.mesh = mesh 714 | 715 | self.ndims = mesh.shape[0] 716 | transpose_arg = (*range(1, 1 + self.ndims), 0) 717 | mesh = mesh.transpose(transpose_arg).copy() 718 | 719 | self.shape = mesh.shape[:-1][::-1] 720 | mesh = mesh.reshape(-1, self.ndims) 721 | self.points = DataArray( 722 | "points", mesh, 723 | vector_format=VF_LIST_OF_VECTORS) 724 | 725 | self.pointdata: list[DataArray] = [] 726 | self.celldata: list[DataArray] = [] 727 | 728 | def copy(self) -> StructuredGrid: 729 | return StructuredGrid(self.mesh) 730 | 731 | def vtk_extension(self) -> str: 732 | """Recommended extension for structured VTK grids.""" 733 | return "vts" 734 | 735 | def add_pointdata(self, data_array: DataArray) -> None: 736 | """Add point data to the grid.""" 737 | self.pointdata.append(data_array) 738 | 739 | def add_celldata(self, data_array: DataArray) -> None: 740 | """Add cell data to the grid.""" 741 | self.celldata.append(data_array) 742 | 743 | # }}} 744 | 745 | 746 | # {{{ vtk xml writers 747 | 748 | def make_vtkfile(filetype: str, 749 | compressor: str | None = None, 750 | version: str = "0.1") -> XMLElement: 751 | import sys 752 | 753 | kwargs = {} 754 | if compressor == "zlib": 755 | kwargs["compressor"] = "vtkZLibDataCompressor" 756 | 757 | bo = "LittleEndian" if sys.byteorder == "little" else "BigEndian" 758 | return XMLElement("VTKFile", 759 | type=filetype, version=version, byte_order=bo, **kwargs) 760 | 761 | 762 | class XMLGenerator: 763 | """ 764 | .. automethod:: __init__ 765 | .. automethod:: __call__ 766 | """ 767 | def __init__(self, 768 | compressor: str | None = None, 769 | vtk_file_version: str | None = None) -> None: 770 | """ 771 | :arg vtk_file_version: a string ``"x.y"`` with the desired VTK 772 | XML file format version. Relevant versions are as follows: 773 | 774 | * ``"0.1"`` is the original version. 775 | * ``"1.0"`` added support for 64-bit indices and offsets, as 776 | described `here `__. 777 | * ``"2.0"`` added support for ghost array data, as 778 | described `here `__. 779 | * ``"2.1"``: added support for writing additional information 780 | attached to a :class:`DataArray` using 781 | `information keys `__. 782 | * ``"2.2"``: changed the node numbering of the hexahedron, as 783 | described `here `__. 784 | """ 785 | 786 | if compressor == "zlib": 787 | try: 788 | import zlib # noqa: F401 789 | except ImportError: 790 | compressor = None 791 | elif compressor is None: 792 | pass 793 | else: 794 | raise ValueError(f"invalid compressor name '{compressor}'") 795 | 796 | if vtk_file_version is None: 797 | # https://www.paraview.org/Wiki/VTK_XML_Formats 798 | vtk_file_version = "0.1" 799 | 800 | self.vtk_file_version = vtk_file_version 801 | self.compressor = compressor 802 | 803 | def __call__(self, vtkobj: Visitable) -> XMLRoot: 804 | """Generate an XML tree from the given *vtkobj*.""" 805 | 806 | child = self.rec(vtkobj) 807 | vtkf = make_vtkfile(child.tag, self.compressor, 808 | version=self.vtk_file_version) 809 | vtkf.add_child(child) 810 | 811 | return XMLRoot(vtkf) 812 | 813 | def rec(self, vtkobj: Visitable) -> XMLElement: 814 | """Recursively visit all the children of *vtkobj*.""" 815 | return vtkobj.invoke_visitor(self) 816 | 817 | 818 | class InlineXMLGenerator(XMLGenerator): 819 | """An XML generator that uses inline :class:`DataArray` entries.""" 820 | 821 | def gen_unstructured_grid(self, ugrid: UnstructuredGrid) -> XMLElement: 822 | el = XMLElement("UnstructuredGrid") 823 | piece = XMLElement("Piece", 824 | NumberOfPoints=ugrid.point_count, NumberOfCells=ugrid.cell_count) 825 | el.add_child(piece) 826 | 827 | if ugrid.pointdata: 828 | data_el = XMLElement("PointData") 829 | piece.add_child(data_el) 830 | for data_array in ugrid.pointdata: 831 | data_el.add_child(self.rec(data_array)) 832 | 833 | if ugrid.celldata: 834 | data_el = XMLElement("CellData") 835 | piece.add_child(data_el) 836 | for data_array in ugrid.celldata: 837 | data_el.add_child(self.rec(data_array)) 838 | 839 | points = XMLElement("Points") 840 | piece.add_child(points) 841 | points.add_child(self.rec(ugrid.points)) 842 | 843 | cells = XMLElement("Cells") 844 | piece.add_child(cells) 845 | cells.add_child(self.rec(ugrid.cell_connectivity)) 846 | cells.add_child(self.rec(ugrid.cell_offsets)) 847 | cells.add_child(self.rec(ugrid.cell_types)) 848 | 849 | return el 850 | 851 | def gen_structured_grid(self, sgrid: StructuredGrid) -> XMLElement: 852 | extent = [] 853 | for dim in range(3): 854 | extent.append(0) 855 | if dim < sgrid.ndims: 856 | extent.append(sgrid.shape[dim]-1) 857 | else: 858 | extent.append(0) 859 | extent_str = " ".join(str(i) for i in extent) 860 | 861 | el = XMLElement("StructuredGrid", WholeExtent=extent_str) 862 | piece = XMLElement("Piece", Extent=extent_str) 863 | el.add_child(piece) 864 | 865 | if sgrid.pointdata: 866 | data_el = XMLElement("PointData") 867 | piece.add_child(data_el) 868 | for data_array in sgrid.pointdata: 869 | data_el.add_child(self.rec(data_array)) 870 | 871 | if sgrid.celldata: 872 | data_el = XMLElement("CellData") 873 | piece.add_child(data_el) 874 | for data_array in sgrid.celldata: 875 | data_el.add_child(self.rec(data_array)) 876 | 877 | points = XMLElement("Points") 878 | piece.add_child(points) 879 | points.add_child(self.rec(sgrid.points)) 880 | return el 881 | 882 | def gen_data_array(self, data: DataArray) -> XMLElement: 883 | el = XMLElement("DataArray", type=data.type, Name=data.name, 884 | NumberOfComponents=data.components, format="binary") 885 | 886 | data.encode(self.compressor, el) 887 | el.add_child("\n") 888 | 889 | return el 890 | 891 | 892 | class AppendedDataXMLGenerator(InlineXMLGenerator): 893 | """An XML generator that uses appended data for :class:`DataArray` entries. 894 | 895 | This creates a special element called ``AppendedData`` and each data array 896 | will index into it. Additional compression can be added to the appended data. 897 | """ 898 | 899 | def __init__(self, compressor: str | None = None, 900 | vtk_file_version: str | None = None) -> None: 901 | super().__init__(compressor=compressor, vtk_file_version=vtk_file_version) 902 | 903 | self.base64_len = 0 904 | self.app_data = XMLElement("AppendedData", encoding="base64") 905 | self.app_data.add_child("_") 906 | 907 | def __call__(self, vtkobj: Visitable) -> XMLRoot: 908 | xmlroot = super().__call__(vtkobj) 909 | 910 | self.app_data.add_child("\n") 911 | child = xmlroot.children[0] 912 | 913 | assert isinstance(child, XMLElement) 914 | child.add_child(self.app_data) 915 | 916 | return xmlroot 917 | 918 | def gen_data_array(self, data: DataArray) -> XMLElement: 919 | el = XMLElement("DataArray", type=data.type, Name=data.name, 920 | NumberOfComponents=data.components, format="appended", 921 | offset=self.base64_len) 922 | 923 | self.base64_len += data.encode(self.compressor, self.app_data) 924 | 925 | return el 926 | 927 | 928 | class ParallelXMLGenerator(XMLGenerator): 929 | """An XML generator for parallel unstructured grids. 930 | 931 | .. automethod:: __init__ 932 | """ 933 | 934 | def __init__(self, pathnames: Sequence[str | pathlib.Path]) -> None: 935 | """ 936 | :arg pathnames: a list of paths to indivitual VTK files containing 937 | different pieces of a grid. 938 | """ 939 | super().__init__() 940 | self.pathnames = [str(p) for p in pathnames] 941 | 942 | def gen_unstructured_grid(self, ugrid: UnstructuredGrid) -> XMLElement: 943 | el = XMLElement("PUnstructuredGrid") 944 | 945 | pointdata = XMLElement("PPointData") 946 | el.add_child(pointdata) 947 | for data_array in ugrid.pointdata: 948 | pointdata.add_child(self.rec(data_array)) 949 | 950 | points = XMLElement("PPoints") 951 | el.add_child(points) 952 | points.add_child(self.rec(ugrid.points)) 953 | 954 | cells = XMLElement("PCells") 955 | el.add_child(cells) 956 | cells.add_child(self.rec(ugrid.cell_connectivity)) 957 | cells.add_child(self.rec(ugrid.cell_offsets)) 958 | cells.add_child(self.rec(ugrid.cell_types)) 959 | 960 | for pathname in self.pathnames: 961 | el.add_child(XMLElement("Piece", Source=pathname)) 962 | 963 | return el 964 | 965 | def gen_data_array(self, data: DataArray) -> XMLElement: 966 | el = XMLElement("PDataArray", type=data.type, Name=data.name, 967 | NumberOfComponents=data.components) 968 | return el 969 | 970 | 971 | def write_structured_grid( 972 | file_name: str | pathlib.Path, 973 | mesh: np.ndarray[Any, np.dtype[Any]], 974 | cell_data: Sequence[tuple[str, np.ndarray[Any, np.dtype[Any]]]] | None = None, 975 | point_data: Sequence[tuple[str, np.ndarray[Any, np.dtype[Any]]]] | None = None, 976 | overwrite: bool = False) -> None: 977 | """Write a structure grid to *filename*. 978 | 979 | This constructs a :class:`StructuredGrid` and adds the relevant point and 980 | cell data, as necessary. The data is all flattened to one dimensional 981 | arrays. 982 | 983 | :arg overwrite: if *True*, existing files are overwritten, otherwise an 984 | exception is raised. 985 | """ 986 | file_name = pathlib.Path(file_name) 987 | 988 | if cell_data is None: 989 | cell_data = [] 990 | 991 | if point_data is None: 992 | point_data = [] 993 | 994 | grid = StructuredGrid(mesh) 995 | 996 | def do_reshape(fld: np.ndarray[Any, Any]) -> np.ndarray[Any, np.dtype[Any]]: 997 | return fld.T.copy().reshape(-1) 998 | 999 | for name, field in cell_data: 1000 | reshaped_fld = obj_array.vectorize(do_reshape, field) # type: ignore[no-untyped-call] 1001 | grid.add_pointdata(DataArray(name, reshaped_fld)) 1002 | 1003 | for name, field in point_data: 1004 | reshaped_fld = obj_array.vectorize(do_reshape, field) # type: ignore[no-untyped-call] 1005 | grid.add_pointdata(DataArray(name, reshaped_fld)) 1006 | 1007 | if not overwrite and file_name.exists(): 1008 | raise FileExistsError(f"Output file '{file_name}' already exists") 1009 | 1010 | with open(file_name, "w") as outf: 1011 | AppendedDataXMLGenerator()(grid).write(outf) 1012 | 1013 | # }}} 1014 | -------------------------------------------------------------------------------- /.basedpyright/baseline.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "./pyvisfile/__init__.py": [ 4 | { 5 | "code": "reportUnusedParameter", 6 | "range": { 7 | "startColumn": 19, 8 | "endColumn": 26, 9 | "lineCount": 1 10 | } 11 | } 12 | ], 13 | "./pyvisfile/vtk/__init__.py": [ 14 | { 15 | "code": "reportAny", 16 | "range": { 17 | "startColumn": 35, 18 | "endColumn": 45, 19 | "lineCount": 1 20 | } 21 | }, 22 | { 23 | "code": "reportUnannotatedClassAttribute", 24 | "range": { 25 | "startColumn": 13, 26 | "endColumn": 16, 27 | "lineCount": 1 28 | } 29 | }, 30 | { 31 | "code": "reportUnannotatedClassAttribute", 32 | "range": { 33 | "startColumn": 13, 34 | "endColumn": 23, 35 | "lineCount": 1 36 | } 37 | }, 38 | { 39 | "code": "reportAny", 40 | "range": { 41 | "startColumn": 25, 42 | "endColumn": 30, 43 | "lineCount": 1 44 | } 45 | }, 46 | { 47 | "code": "reportUnannotatedClassAttribute", 48 | "range": { 49 | "startColumn": 13, 50 | "endColumn": 19, 51 | "lineCount": 1 52 | } 53 | }, 54 | { 55 | "code": "reportImplicitOverride", 56 | "range": { 57 | "startColumn": 8, 58 | "endColumn": 15, 59 | "lineCount": 1 60 | } 61 | }, 62 | { 63 | "code": "reportImplicitOverride", 64 | "range": { 65 | "startColumn": 8, 66 | "endColumn": 18, 67 | "lineCount": 1 68 | } 69 | }, 70 | { 71 | "code": "reportImplicitOverride", 72 | "range": { 73 | "startColumn": 8, 74 | "endColumn": 18, 75 | "lineCount": 1 76 | } 77 | }, 78 | { 79 | "code": "reportImplicitOverride", 80 | "range": { 81 | "startColumn": 8, 82 | "endColumn": 26, 83 | "lineCount": 1 84 | } 85 | }, 86 | { 87 | "code": "reportUnannotatedClassAttribute", 88 | "range": { 89 | "startColumn": 13, 90 | "endColumn": 22, 91 | "lineCount": 1 92 | } 93 | }, 94 | { 95 | "code": "reportUnannotatedClassAttribute", 96 | "range": { 97 | "startColumn": 13, 98 | "endColumn": 20, 99 | "lineCount": 1 100 | } 101 | }, 102 | { 103 | "code": "reportImplicitOverride", 104 | "range": { 105 | "startColumn": 8, 106 | "endColumn": 15, 107 | "lineCount": 1 108 | } 109 | }, 110 | { 111 | "code": "reportImplicitOverride", 112 | "range": { 113 | "startColumn": 8, 114 | "endColumn": 18, 115 | "lineCount": 1 116 | } 117 | }, 118 | { 119 | "code": "reportImplicitOverride", 120 | "range": { 121 | "startColumn": 8, 122 | "endColumn": 18, 123 | "lineCount": 1 124 | } 125 | }, 126 | { 127 | "code": "reportImplicitOverride", 128 | "range": { 129 | "startColumn": 8, 130 | "endColumn": 26, 131 | "lineCount": 1 132 | } 133 | }, 134 | { 135 | "code": "reportUnannotatedClassAttribute", 136 | "range": { 137 | "startColumn": 13, 138 | "endColumn": 22, 139 | "lineCount": 1 140 | } 141 | }, 142 | { 143 | "code": "reportUnannotatedClassAttribute", 144 | "range": { 145 | "startColumn": 13, 146 | "endColumn": 20, 147 | "lineCount": 1 148 | } 149 | }, 150 | { 151 | "code": "reportImplicitOverride", 152 | "range": { 153 | "startColumn": 8, 154 | "endColumn": 15, 155 | "lineCount": 1 156 | } 157 | }, 158 | { 159 | "code": "reportImplicitOverride", 160 | "range": { 161 | "startColumn": 8, 162 | "endColumn": 18, 163 | "lineCount": 1 164 | } 165 | }, 166 | { 167 | "code": "reportImplicitOverride", 168 | "range": { 169 | "startColumn": 8, 170 | "endColumn": 18, 171 | "lineCount": 1 172 | } 173 | }, 174 | { 175 | "code": "reportImplicitOverride", 176 | "range": { 177 | "startColumn": 8, 178 | "endColumn": 26, 179 | "lineCount": 1 180 | } 181 | }, 182 | { 183 | "code": "reportUnannotatedClassAttribute", 184 | "range": { 185 | "startColumn": 4, 186 | "endColumn": 20, 187 | "lineCount": 1 188 | } 189 | }, 190 | { 191 | "code": "reportAny", 192 | "range": { 193 | "startColumn": 17, 194 | "endColumn": 26, 195 | "lineCount": 1 196 | } 197 | }, 198 | { 199 | "code": "reportUnannotatedClassAttribute", 200 | "range": { 201 | "startColumn": 13, 202 | "endColumn": 17, 203 | "lineCount": 1 204 | } 205 | }, 206 | { 207 | "code": "reportAny", 208 | "range": { 209 | "startColumn": 55, 210 | "endColumn": 64, 211 | "lineCount": 1 212 | } 213 | }, 214 | { 215 | "code": "reportAny", 216 | "range": { 217 | "startColumn": 16, 218 | "endColumn": 22, 219 | "lineCount": 1 220 | } 221 | }, 222 | { 223 | "code": "reportAny", 224 | "range": { 225 | "startColumn": 65, 226 | "endColumn": 71, 227 | "lineCount": 1 228 | } 229 | }, 230 | { 231 | "code": "reportAny", 232 | "range": { 233 | "startColumn": 19, 234 | "endColumn": 34, 235 | "lineCount": 1 236 | } 237 | }, 238 | { 239 | "code": "reportAny", 240 | "range": { 241 | "startColumn": 19, 242 | "endColumn": 39, 243 | "lineCount": 1 244 | } 245 | }, 246 | { 247 | "code": "reportUnknownMemberType", 248 | "range": { 249 | "startColumn": 28, 250 | "endColumn": 39, 251 | "lineCount": 1 252 | } 253 | }, 254 | { 255 | "code": "reportUnknownMemberType", 256 | "range": { 257 | "startColumn": 28, 258 | "endColumn": 44, 259 | "lineCount": 1 260 | } 261 | }, 262 | { 263 | "code": "reportUnknownMemberType", 264 | "range": { 265 | "startColumn": 19, 266 | "endColumn": 34, 267 | "lineCount": 1 268 | } 269 | }, 270 | { 271 | "code": "reportUnknownArgumentType", 272 | "range": { 273 | "startColumn": 19, 274 | "endColumn": 34, 275 | "lineCount": 1 276 | } 277 | }, 278 | { 279 | "code": "reportUnknownMemberType", 280 | "range": { 281 | "startColumn": 15, 282 | "endColumn": 29, 283 | "lineCount": 1 284 | } 285 | }, 286 | { 287 | "code": "reportUnknownMemberType", 288 | "range": { 289 | "startColumn": 34, 290 | "endColumn": 51, 291 | "lineCount": 1 292 | } 293 | }, 294 | { 295 | "code": "reportUnknownMemberType", 296 | "range": { 297 | "startColumn": 58, 298 | "endColumn": 76, 299 | "lineCount": 1 300 | } 301 | }, 302 | { 303 | "code": "reportUnknownMemberType", 304 | "range": { 305 | "startColumn": 32, 306 | "endColumn": 47, 307 | "lineCount": 1 308 | } 309 | }, 310 | { 311 | "code": "reportUnknownArgumentType", 312 | "range": { 313 | "startColumn": 49, 314 | "endColumn": 45, 315 | "lineCount": 7 316 | } 317 | }, 318 | { 319 | "code": "reportUnknownArgumentType", 320 | "range": { 321 | "startColumn": 33, 322 | "endColumn": 29, 323 | "lineCount": 4 324 | } 325 | }, 326 | { 327 | "code": "reportUnknownMemberType", 328 | "range": { 329 | "startColumn": 28, 330 | "endColumn": 43, 331 | "lineCount": 1 332 | } 333 | }, 334 | { 335 | "code": "reportUnknownMemberType", 336 | "range": { 337 | "startColumn": 43, 338 | "endColumn": 58, 339 | "lineCount": 1 340 | } 341 | }, 342 | { 343 | "code": "reportUnknownMemberType", 344 | "range": { 345 | "startColumn": 28, 346 | "endColumn": 43, 347 | "lineCount": 1 348 | } 349 | }, 350 | { 351 | "code": "reportUnknownArgumentType", 352 | "range": { 353 | "startColumn": 28, 354 | "endColumn": 43, 355 | "lineCount": 1 356 | } 357 | }, 358 | { 359 | "code": "reportUnknownMemberType", 360 | "range": { 361 | "startColumn": 30, 362 | "endColumn": 45, 363 | "lineCount": 1 364 | } 365 | }, 366 | { 367 | "code": "reportUnknownMemberType", 368 | "range": { 369 | "startColumn": 43, 370 | "endColumn": 58, 371 | "lineCount": 1 372 | } 373 | }, 374 | { 375 | "code": "reportUnknownMemberType", 376 | "range": { 377 | "startColumn": 43, 378 | "endColumn": 63, 379 | "lineCount": 1 380 | } 381 | }, 382 | { 383 | "code": "reportUnknownArgumentType", 384 | "range": { 385 | "startColumn": 43, 386 | "endColumn": 63, 387 | "lineCount": 1 388 | } 389 | }, 390 | { 391 | "code": "reportUnknownMemberType", 392 | "range": { 393 | "startColumn": 57, 394 | "endColumn": 72, 395 | "lineCount": 1 396 | } 397 | }, 398 | { 399 | "code": "reportUnknownMemberType", 400 | "range": { 401 | "startColumn": 15, 402 | "endColumn": 30, 403 | "lineCount": 1 404 | } 405 | }, 406 | { 407 | "code": "reportUnknownMemberType", 408 | "range": { 409 | "startColumn": 15, 410 | "endColumn": 43, 411 | "lineCount": 1 412 | } 413 | }, 414 | { 415 | "code": "reportUnknownMemberType", 416 | "range": { 417 | "startColumn": 24, 418 | "endColumn": 38, 419 | "lineCount": 1 420 | } 421 | }, 422 | { 423 | "code": "reportUnannotatedClassAttribute", 424 | "range": { 425 | "startColumn": 4, 426 | "endColumn": 20, 427 | "lineCount": 1 428 | } 429 | }, 430 | { 431 | "code": "reportUnannotatedClassAttribute", 432 | "range": { 433 | "startColumn": 13, 434 | "endColumn": 24, 435 | "lineCount": 1 436 | } 437 | }, 438 | { 439 | "code": "reportUnannotatedClassAttribute", 440 | "range": { 441 | "startColumn": 31, 442 | "endColumn": 37, 443 | "lineCount": 1 444 | } 445 | }, 446 | { 447 | "code": "reportUnannotatedClassAttribute", 448 | "range": { 449 | "startColumn": 17, 450 | "endColumn": 27, 451 | "lineCount": 1 452 | } 453 | }, 454 | { 455 | "code": "reportUnannotatedClassAttribute", 456 | "range": { 457 | "startColumn": 34, 458 | "endColumn": 51, 459 | "lineCount": 1 460 | } 461 | }, 462 | { 463 | "code": "reportUnannotatedClassAttribute", 464 | "range": { 465 | "startColumn": 58, 466 | "endColumn": 70, 467 | "lineCount": 1 468 | } 469 | }, 470 | { 471 | "code": "reportAny", 472 | "range": { 473 | "startColumn": 24, 474 | "endColumn": 69, 475 | "lineCount": 1 476 | } 477 | }, 478 | { 479 | "code": "reportUnannotatedClassAttribute", 480 | "range": { 481 | "startColumn": 13, 482 | "endColumn": 23, 483 | "lineCount": 1 484 | } 485 | }, 486 | { 487 | "code": "reportUnannotatedClassAttribute", 488 | "range": { 489 | "startColumn": 4, 490 | "endColumn": 20, 491 | "lineCount": 1 492 | } 493 | }, 494 | { 495 | "code": "reportUnannotatedClassAttribute", 496 | "range": { 497 | "startColumn": 13, 498 | "endColumn": 17, 499 | "lineCount": 1 500 | } 501 | }, 502 | { 503 | "code": "reportAny", 504 | "range": { 505 | "startColumn": 8, 506 | "endColumn": 18, 507 | "lineCount": 1 508 | } 509 | }, 510 | { 511 | "code": "reportUnannotatedClassAttribute", 512 | "range": { 513 | "startColumn": 13, 514 | "endColumn": 18, 515 | "lineCount": 1 516 | } 517 | }, 518 | { 519 | "code": "reportAny", 520 | "range": { 521 | "startColumn": 21, 522 | "endColumn": 31, 523 | "lineCount": 1 524 | } 525 | }, 526 | { 527 | "code": "reportAny", 528 | "range": { 529 | "startColumn": 35, 530 | "endColumn": 49, 531 | "lineCount": 1 532 | } 533 | }, 534 | { 535 | "code": "reportAny", 536 | "range": { 537 | "startColumn": 39, 538 | "endColumn": 49, 539 | "lineCount": 1 540 | } 541 | }, 542 | { 543 | "code": "reportUnknownMemberType", 544 | "range": { 545 | "startColumn": 15, 546 | "endColumn": 29, 547 | "lineCount": 1 548 | } 549 | }, 550 | { 551 | "code": "reportUnknownMemberType", 552 | "range": { 553 | "startColumn": 15, 554 | "endColumn": 49, 555 | "lineCount": 1 556 | } 557 | }, 558 | { 559 | "code": "reportUnknownMemberType", 560 | "range": { 561 | "startColumn": 8, 562 | "endColumn": 18, 563 | "lineCount": 1 564 | } 565 | }, 566 | { 567 | "code": "reportUnannotatedClassAttribute", 568 | "range": { 569 | "startColumn": 13, 570 | "endColumn": 18, 571 | "lineCount": 1 572 | } 573 | }, 574 | { 575 | "code": "reportUnknownMemberType", 576 | "range": { 577 | "startColumn": 21, 578 | "endColumn": 31, 579 | "lineCount": 1 580 | } 581 | }, 582 | { 583 | "code": "reportUnknownMemberType", 584 | "range": { 585 | "startColumn": 15, 586 | "endColumn": 27, 587 | "lineCount": 1 588 | } 589 | }, 590 | { 591 | "code": "reportAny", 592 | "range": { 593 | "startColumn": 32, 594 | "endColumn": 42, 595 | "lineCount": 1 596 | } 597 | }, 598 | { 599 | "code": "reportAny", 600 | "range": { 601 | "startColumn": 32, 602 | "endColumn": 42, 603 | "lineCount": 1 604 | } 605 | }, 606 | { 607 | "code": "reportUnannotatedClassAttribute", 608 | "range": { 609 | "startColumn": 13, 610 | "endColumn": 19, 611 | "lineCount": 1 612 | } 613 | }, 614 | { 615 | "code": "reportUnusedImport", 616 | "range": { 617 | "startColumn": 23, 618 | "endColumn": 27, 619 | "lineCount": 1 620 | } 621 | }, 622 | { 623 | "code": "reportUnannotatedClassAttribute", 624 | "range": { 625 | "startColumn": 13, 626 | "endColumn": 29, 627 | "lineCount": 1 628 | } 629 | }, 630 | { 631 | "code": "reportUnannotatedClassAttribute", 632 | "range": { 633 | "startColumn": 13, 634 | "endColumn": 23, 635 | "lineCount": 1 636 | } 637 | }, 638 | { 639 | "code": "reportUnknownMemberType", 640 | "range": { 641 | "startColumn": 12, 642 | "endColumn": 25, 643 | "lineCount": 1 644 | } 645 | }, 646 | { 647 | "code": "reportAny", 648 | "range": { 649 | "startColumn": 21, 650 | "endColumn": 32, 651 | "lineCount": 1 652 | } 653 | }, 654 | { 655 | "code": "reportUnknownMemberType", 656 | "range": { 657 | "startColumn": 16, 658 | "endColumn": 29, 659 | "lineCount": 1 660 | } 661 | }, 662 | { 663 | "code": "reportUnknownMemberType", 664 | "range": { 665 | "startColumn": 30, 666 | "endColumn": 41, 667 | "lineCount": 1 668 | } 669 | }, 670 | { 671 | "code": "reportUnknownMemberType", 672 | "range": { 673 | "startColumn": 16, 674 | "endColumn": 29, 675 | "lineCount": 1 676 | } 677 | }, 678 | { 679 | "code": "reportUnknownArgumentType", 680 | "range": { 681 | "startColumn": 34, 682 | "endColumn": 35, 683 | "lineCount": 1 684 | } 685 | }, 686 | { 687 | "code": "reportUnannotatedClassAttribute", 688 | "range": { 689 | "startColumn": 13, 690 | "endColumn": 23, 691 | "lineCount": 1 692 | } 693 | }, 694 | { 695 | "code": "reportUnannotatedClassAttribute", 696 | "range": { 697 | "startColumn": 13, 698 | "endColumn": 21, 699 | "lineCount": 1 700 | } 701 | }, 702 | { 703 | "code": "reportImplicitOverride", 704 | "range": { 705 | "startColumn": 8, 706 | "endColumn": 16, 707 | "lineCount": 1 708 | } 709 | }, 710 | { 711 | "code": "reportImplicitOverride", 712 | "range": { 713 | "startColumn": 8, 714 | "endColumn": 22, 715 | "lineCount": 1 716 | } 717 | }, 718 | { 719 | "code": "reportUnannotatedClassAttribute", 720 | "range": { 721 | "startColumn": 13, 722 | "endColumn": 22, 723 | "lineCount": 1 724 | } 725 | }, 726 | { 727 | "code": "reportUnknownMemberType", 728 | "range": { 729 | "startColumn": 15, 730 | "endColumn": 20, 731 | "lineCount": 1 732 | } 733 | }, 734 | { 735 | "code": "reportUnknownMemberType", 736 | "range": { 737 | "startColumn": 15, 738 | "endColumn": 25, 739 | "lineCount": 1 740 | } 741 | }, 742 | { 743 | "code": "reportUnknownMemberType", 744 | "range": { 745 | "startColumn": 15, 746 | "endColumn": 35, 747 | "lineCount": 1 748 | } 749 | } 750 | ], 751 | "./pyvisfile/vtk/vtk_ordering.py": [ 752 | { 753 | "code": "reportUnusedParameter", 754 | "range": { 755 | "startColumn": 8, 756 | "endColumn": 19, 757 | "lineCount": 1 758 | } 759 | } 760 | ], 761 | "./pyvisfile/xdmf/__init__.py": [ 762 | { 763 | "code": "reportAny", 764 | "range": { 765 | "startColumn": 15, 766 | "endColumn": 18, 767 | "lineCount": 1 768 | } 769 | }, 770 | { 771 | "code": "reportUnknownArgumentType", 772 | "range": { 773 | "startColumn": 28, 774 | "endColumn": 29, 775 | "lineCount": 1 776 | } 777 | }, 778 | { 779 | "code": "reportAny", 780 | "range": { 781 | "startColumn": 15, 782 | "endColumn": 18, 783 | "lineCount": 1 784 | } 785 | }, 786 | { 787 | "code": "reportAny", 788 | "range": { 789 | "startColumn": 36, 790 | "endColumn": 37, 791 | "lineCount": 1 792 | } 793 | }, 794 | { 795 | "code": "reportUnannotatedClassAttribute", 796 | "range": { 797 | "startColumn": 13, 798 | "endColumn": 19, 799 | "lineCount": 1 800 | } 801 | }, 802 | { 803 | "code": "reportAny", 804 | "range": { 805 | "startColumn": 24, 806 | "endColumn": 30, 807 | "lineCount": 1 808 | } 809 | }, 810 | { 811 | "code": "reportAny", 812 | "range": { 813 | "startColumn": 8, 814 | "endColumn": 14, 815 | "lineCount": 1 816 | } 817 | }, 818 | { 819 | "code": "reportAny", 820 | "range": { 821 | "startColumn": 8, 822 | "endColumn": 11, 823 | "lineCount": 1 824 | } 825 | }, 826 | { 827 | "code": "reportAny", 828 | "range": { 829 | "startColumn": 27, 830 | "endColumn": 33, 831 | "lineCount": 1 832 | } 833 | }, 834 | { 835 | "code": "reportAny", 836 | "range": { 837 | "startColumn": 35, 838 | "endColumn": 38, 839 | "lineCount": 1 840 | } 841 | }, 842 | { 843 | "code": "reportUnannotatedClassAttribute", 844 | "range": { 845 | "startColumn": 13, 846 | "endColumn": 24, 847 | "lineCount": 1 848 | } 849 | }, 850 | { 851 | "code": "reportUnannotatedClassAttribute", 852 | "range": { 853 | "startColumn": 17, 854 | "endColumn": 21, 855 | "lineCount": 1 856 | } 857 | }, 858 | { 859 | "code": "reportAny", 860 | "range": { 861 | "startColumn": 23, 862 | "endColumn": 32, 863 | "lineCount": 1 864 | } 865 | }, 866 | { 867 | "code": "reportAny", 868 | "range": { 869 | "startColumn": 23, 870 | "endColumn": 32, 871 | "lineCount": 1 872 | } 873 | }, 874 | { 875 | "code": "reportAny", 876 | "range": { 877 | "startColumn": 23, 878 | "endColumn": 26, 879 | "lineCount": 1 880 | } 881 | }, 882 | { 883 | "code": "reportAny", 884 | "range": { 885 | "startColumn": 60, 886 | "endColumn": 63, 887 | "lineCount": 1 888 | } 889 | }, 890 | { 891 | "code": "reportUnannotatedClassAttribute", 892 | "range": { 893 | "startColumn": 13, 894 | "endColumn": 23, 895 | "lineCount": 1 896 | } 897 | }, 898 | { 899 | "code": "reportUnannotatedClassAttribute", 900 | "range": { 901 | "startColumn": 13, 902 | "endColumn": 17, 903 | "lineCount": 1 904 | } 905 | }, 906 | { 907 | "code": "reportUnannotatedClassAttribute", 908 | "range": { 909 | "startColumn": 13, 910 | "endColumn": 20, 911 | "lineCount": 1 912 | } 913 | }, 914 | { 915 | "code": "reportUnannotatedClassAttribute", 916 | "range": { 917 | "startColumn": 13, 918 | "endColumn": 18, 919 | "lineCount": 1 920 | } 921 | }, 922 | { 923 | "code": "reportAny", 924 | "range": { 925 | "startColumn": 12, 926 | "endColumn": 16, 927 | "lineCount": 1 928 | } 929 | }, 930 | { 931 | "code": "reportAny", 932 | "range": { 933 | "startColumn": 8, 934 | "endColumn": 16, 935 | "lineCount": 1 936 | } 937 | }, 938 | { 939 | "code": "reportAny", 940 | "range": { 941 | "startColumn": 19, 942 | "endColumn": 28, 943 | "lineCount": 1 944 | } 945 | }, 946 | { 947 | "code": "reportAny", 948 | "range": { 949 | "startColumn": 19, 950 | "endColumn": 37, 951 | "lineCount": 1 952 | } 953 | }, 954 | { 955 | "code": "reportAny", 956 | "range": { 957 | "startColumn": 29, 958 | "endColumn": 38, 959 | "lineCount": 1 960 | } 961 | }, 962 | { 963 | "code": "reportAny", 964 | "range": { 965 | "startColumn": 8, 966 | "endColumn": 12, 967 | "lineCount": 1 968 | } 969 | }, 970 | { 971 | "code": "reportAny", 972 | "range": { 973 | "startColumn": 15, 974 | "endColumn": 24, 975 | "lineCount": 1 976 | } 977 | }, 978 | { 979 | "code": "reportAny", 980 | "range": { 981 | "startColumn": 15, 982 | "endColumn": 30, 983 | "lineCount": 1 984 | } 985 | }, 986 | { 987 | "code": "reportAny", 988 | "range": { 989 | "startColumn": 37, 990 | "endColumn": 41, 991 | "lineCount": 1 992 | } 993 | }, 994 | { 995 | "code": "reportAny", 996 | "range": { 997 | "startColumn": 21, 998 | "endColumn": 25, 999 | "lineCount": 1 1000 | } 1001 | }, 1002 | { 1003 | "code": "reportAny", 1004 | "range": { 1005 | "startColumn": 36, 1006 | "endColumn": 46, 1007 | "lineCount": 1 1008 | } 1009 | }, 1010 | { 1011 | "code": "reportAny", 1012 | "range": { 1013 | "startColumn": 51, 1014 | "endColumn": 55, 1015 | "lineCount": 1 1016 | } 1017 | }, 1018 | { 1019 | "code": "reportAny", 1020 | "range": { 1021 | "startColumn": 48, 1022 | "endColumn": 52, 1023 | "lineCount": 1 1024 | } 1025 | }, 1026 | { 1027 | "code": "reportAny", 1028 | "range": { 1029 | "startColumn": 27, 1030 | "endColumn": 31, 1031 | "lineCount": 1 1032 | } 1033 | }, 1034 | { 1035 | "code": "reportUnannotatedClassAttribute", 1036 | "range": { 1037 | "startColumn": 13, 1038 | "endColumn": 16, 1039 | "lineCount": 1 1040 | } 1041 | }, 1042 | { 1043 | "code": "reportImplicitOverride", 1044 | "range": { 1045 | "startColumn": 8, 1046 | "endColumn": 20, 1047 | "lineCount": 1 1048 | } 1049 | }, 1050 | { 1051 | "code": "reportUnannotatedClassAttribute", 1052 | "range": { 1053 | "startColumn": 13, 1054 | "endColumn": 17, 1055 | "lineCount": 1 1056 | } 1057 | }, 1058 | { 1059 | "code": "reportAny", 1060 | "range": { 1061 | "startColumn": 34, 1062 | "endColumn": 13, 1063 | "lineCount": 5 1064 | } 1065 | }, 1066 | { 1067 | "code": "reportImplicitOverride", 1068 | "range": { 1069 | "startColumn": 8, 1070 | "endColumn": 13, 1071 | "lineCount": 1 1072 | } 1073 | } 1074 | ] 1075 | } 1076 | } -------------------------------------------------------------------------------- /pyvisfile/xdmf/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2020 Alexandru Fikl" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import enum 27 | import os 28 | from typing import Any, TypeVar 29 | from xml.etree.ElementTree import Element, ElementTree 30 | 31 | import numpy as np 32 | 33 | 34 | __doc__ = """ 35 | Xdmf Tags 36 | --------- 37 | 38 | DataItem 39 | ^^^^^^^^ 40 | 41 | .. autoclass:: DataItemType 42 | :show-inheritance: 43 | :members: 44 | :undoc-members: 45 | 46 | .. autoclass:: DataItemNumberType 47 | :show-inheritance: 48 | :members: 49 | :undoc-members: 50 | .. autoclass:: DataItemFormat 51 | :show-inheritance: 52 | :members: 53 | :undoc-members: 54 | .. autoclass:: DataItemEndian 55 | :show-inheritance: 56 | :members: 57 | :undoc-members: 58 | 59 | .. autoclass:: DataItem 60 | 61 | Domain 62 | ^^^^^^ 63 | 64 | .. autoclass:: Domain 65 | 66 | Grid 67 | ^^^^ 68 | 69 | .. autoclass:: GridType 70 | :show-inheritance: 71 | :members: 72 | :undoc-members: 73 | .. autoclass:: CollectionType 74 | :show-inheritance: 75 | :members: 76 | :undoc-members: 77 | .. autoclass:: Grid 78 | 79 | 80 | Topology 81 | ^^^^^^^^ 82 | 83 | .. autoclass:: TopologyType 84 | :show-inheritance: 85 | :members: 86 | :undoc-members: 87 | .. autoclass:: Topology 88 | 89 | Geometry 90 | ^^^^^^^^ 91 | 92 | .. autoclass:: GeometryType 93 | :show-inheritance: 94 | :members: 95 | :undoc-members: 96 | .. autoclass:: Geometry 97 | 98 | Attribute 99 | ^^^^^^^^^ 100 | 101 | .. autoclass:: AttributeType 102 | :show-inheritance: 103 | :members: 104 | :undoc-members: 105 | .. autoclass:: AttributeCenter 106 | :show-inheritance: 107 | :members: 108 | :undoc-members: 109 | .. autoclass:: Attribute 110 | 111 | Time 112 | ^^^^ 113 | 114 | .. autoclass:: Time 115 | :members: 116 | 117 | Information 118 | ^^^^^^^^^^^ 119 | 120 | .. autoclass:: Information 121 | :members: 122 | 123 | XInclude 124 | ^^^^^^^^ 125 | 126 | .. autoclass:: XInclude 127 | :members: 128 | 129 | Writing 130 | ------- 131 | 132 | .. autoclass:: DataArray 133 | :members: 134 | .. autoclass:: NumpyDataArray 135 | :members: 136 | 137 | .. autoclass:: XdmfGrid 138 | .. autoclass:: XdmfUnstructuredGrid 139 | :show-inheritance: 140 | 141 | .. autoclass:: XdmfWriter 142 | """ 143 | 144 | 145 | # {{{ utils 146 | 147 | def _stringify(obj: Any) -> str: 148 | if isinstance(obj, list | tuple): 149 | return " ".join(str(c) for c in obj) 150 | return str(obj) 151 | 152 | 153 | class XdmfElement(Element): 154 | """Base class for all the XDMF tags. 155 | 156 | .. attribute:: name 157 | 158 | The ``Name`` attribute the tags. This attribute is optional, so 159 | it can return *None* if not set. 160 | 161 | .. automethod:: replace 162 | """ 163 | 164 | def __init__( 165 | self, 166 | parent: Element | None, 167 | tag: str, 168 | attrib: dict[str, Any]) -> None: 169 | super().__init__(tag, attrib={ 170 | k: _stringify(v) for k, v in attrib.items() if v is not None 171 | }) 172 | 173 | self.parent = parent 174 | if parent is not None: 175 | parent.append(self) 176 | 177 | @property 178 | def name(self) -> str: 179 | name = self.get("Name") 180 | if name is None: 181 | raise AttributeError(name) 182 | 183 | return name 184 | 185 | def replace(self, **kwargs: Any) -> XdmfElement: 186 | """Duplicate the current tag with updated attributes from *kwargs*.""" 187 | parent = kwargs.pop("parent", self.parent) 188 | tag = kwargs.pop("tag", self.tag) 189 | 190 | attrib = self.attrib.copy() 191 | attrib.update(kwargs) 192 | 193 | return XdmfElement(parent, tag, attrib=attrib) 194 | 195 | # }}} 196 | 197 | 198 | # {{{ xdmf tags 199 | 200 | # {{{ attribute 201 | 202 | # {{{ unsupported 203 | 204 | # NOTE: these are taken from VTK source in 205 | # 206 | # https://gitlab.kitware.com/vtk/vtk/-/blob/c138bfae93f570b467e7f4fdc0c42d974cd684f4/IO/Xdmf3/vtkXdmf3DataSet.cxx#L2278 207 | # 208 | # and are currently unsupported here (someone needs to figure out how to write 209 | # them). They were introduced in 210 | # 211 | # https://gitlab.kitware.com/xdmf/xdmf/-/merge_requests/41 212 | # https://gitlab.kitware.com/vtk/vtk/-/merge_requests/3194 213 | 214 | class AttributeElementFamily(enum.Enum): 215 | """High-order element families for ``FiniteElementFunction``.""" 216 | DG = enum.auto() #: Discontinuous Galerkin elements for simplices. 217 | CG = enum.auto() #: Continuous Galerkin elements for simplices. 218 | Q = enum.auto() #: Continuous Galerkin elements for quadrilaterals. 219 | DQ = enum.auto() #: Discontinuous Galerkin elements for quadrilaterals. 220 | RT = enum.auto() #: Raviart-Thomas elements on triangles. 221 | 222 | 223 | class AttributeElementCell(enum.Enum): 224 | """Reference element types for ``FiniteElementFunction``.""" 225 | interval = enum.auto() 226 | triangle = enum.auto() 227 | tetrahedron = enum.auto() 228 | quadrilateral = enum.auto() 229 | hexahedron = enum.auto() 230 | 231 | # }}} 232 | 233 | 234 | class AttributeType(enum.Enum): 235 | """Rank of the attribute stored on the mesh.""" 236 | # NOTE: integer ids taken from 237 | # https://gitlab.kitware.com/xdmf/xdmf/-/blob/04a84bab0eb1568e0f1a27c8fb60c6931efda003/XdmfAttributeType.hpp#L129 238 | Scalar = 200 239 | Vector = 201 240 | Tensor = 202 241 | Matrix = 203 242 | Tensor6 = 204 243 | GlobalId = 205 244 | 245 | @staticmethod 246 | def from_shape(shape: tuple[int, ...]) -> AttributeType: 247 | # https://github.com/nschloe/meshio/blob/37673c8fb938ad73d92fb3171dee3eb193b5e7ac/meshio/xdmf/common.py#L162 248 | if len(shape) == 1 or (len(shape) == 2 and shape[1] == 1): 249 | return AttributeType.Scalar 250 | elif len(shape) == 2 and shape[0] in [2, 3]: 251 | return AttributeType.Vector 252 | elif len(shape) == 2 and shape[0] in [4, 9]: 253 | return AttributeType.Tensor 254 | elif len(shape) == 2 and shape[0] == 6: 255 | return AttributeType.Tensor6 256 | elif len(shape) == 3: 257 | return AttributeType.Matrix 258 | else: 259 | raise ValueError(f"cannot determine attribute type from shape '{shape}'") 260 | 261 | 262 | class AttributeCenter(enum.Enum): 263 | """Center of the attribute stored on the mesh.""" 264 | # NOTE: integer ids taken from 265 | # https://gitlab.kitware.com/xdmf/xdmf/-/blob/04a84bab0eb1568e0f1a27c8fb60c6931efda003/XdmfAttributeCenter.hpp#L126 266 | Grid = 100 267 | Cell = 101 268 | Face = 102 269 | Edge = 103 270 | Node = 104 271 | # NOTE: only for `FiniteElementFunction` attributes, which are not supported 272 | Other = 105 273 | 274 | 275 | class Attribute(XdmfElement): 276 | """ 277 | .. automethod:: __init__ 278 | """ 279 | 280 | def __init__( 281 | self, *, 282 | name: str | None = None, 283 | atype: AttributeType = AttributeType.Scalar, 284 | acenter: AttributeCenter = AttributeCenter.Node, 285 | parent: Element | None = None, 286 | ) -> None: 287 | """ 288 | :param parent: if provided, *self* is appended to the element. 289 | """ 290 | 291 | super().__init__(parent, "Attribute", { 292 | "Name": name, 293 | "Center": acenter.name, 294 | "AttributeType": atype.name, 295 | }) 296 | 297 | # }}} 298 | 299 | 300 | # {{{ data item 301 | 302 | class DataItemType(enum.Enum): 303 | """Data layout of an item.""" 304 | Uniform = enum.auto() 305 | HyperSlab = enum.auto() 306 | Function = enum.auto() 307 | 308 | 309 | class DataItemNumberType(enum.Enum): 310 | """Basic number types for an item.""" 311 | Char = enum.auto() 312 | UChar = enum.auto() 313 | Int = enum.auto() 314 | UInt = enum.auto() 315 | Float = enum.auto() 316 | 317 | @staticmethod 318 | def from_dtype(dtype: np.dtype[Any]) -> DataItemNumberType: 319 | if dtype.kind == "i": 320 | return DataItemNumberType.Int 321 | elif dtype.kind == "u": 322 | return DataItemNumberType.UInt 323 | elif dtype.kind == "f": 324 | return DataItemNumberType.Float 325 | else: 326 | raise ValueError(f"unsupported dtype: '{dtype}'") 327 | 328 | 329 | class DataItemFormat(enum.Enum): 330 | """Format in which the item is stored.""" 331 | XML = enum.auto() 332 | HDF = enum.auto() 333 | Binary = enum.auto() 334 | # NOTE: unsupported as it's pretty much undocumented 335 | TIFF = enum.auto() 336 | 337 | 338 | class DataItemEndian(enum.Enum): 339 | """Endianness of the data stored in the item.""" 340 | # NOTE: integer ids taken from 341 | # https://gitlab.kitware.com/xdmf/xdmf/-/blob/04a84bab0eb1568e0f1a27c8fb60c6931efda003/core/XdmfBinaryController.hpp#L211 342 | Big = 50 343 | Little = 51 344 | Native = 52 345 | 346 | @staticmethod 347 | def from_system() -> DataItemEndian: 348 | import sys 349 | if sys.byteorder == "little": 350 | return DataItemEndian.Little 351 | elif sys.byteorder == "big": 352 | return DataItemEndian.Big 353 | else: 354 | return DataItemEndian.Native 355 | 356 | 357 | class DataItem(XdmfElement): 358 | """A :class:`DataItem` describes the storage of actual values in an 359 | XDMF file. This can be inline ASCII data, the path to a binary file 360 | or a reference to another :class:`DataItem`. 361 | 362 | .. attribute:: dimensions 363 | 364 | Analogous to :attr:`numpy.ndarray.shape`. 365 | 366 | .. automethod:: __init__ 367 | .. automethod:: as_reference 368 | """ 369 | 370 | def __init__( 371 | self, *, 372 | dimensions: tuple[int, ...] | None = None, 373 | name: str | None = None, 374 | itype: DataItemType | None = DataItemType.Uniform, 375 | ntype: DataItemNumberType | None = DataItemNumberType.Float, 376 | precision: int | None = 4, 377 | reference: str | None = None, 378 | function: str | None = None, 379 | endian: DataItemEndian | None = DataItemEndian.Native, 380 | dformat: DataItemFormat | None = DataItemFormat.XML, 381 | parent: Element | None = None, 382 | data: str | None = None, 383 | ) -> None: 384 | """ 385 | :param parent: if provided, *self* is appended to the element. 386 | :param reference: path to another :class:`DataItem`. 387 | Use :meth:`as_reference` to populate. 388 | :param data: data contained inside the :class:`DataItem`. This is 389 | usually a path to a binary file. 390 | """ 391 | 392 | self._dimensions = dimensions 393 | 394 | super().__init__(parent, "DataItem", { 395 | "Name": name, 396 | "ItemType": itype.name if itype is not None else itype, 397 | "Dimensions": dimensions, 398 | "NumberType": ntype.name if ntype is not None else ntype, 399 | "Precision": precision, 400 | "Reference": reference, 401 | "Function": function, 402 | "Endian": endian.name if endian is not None else endian, 403 | "Format": dformat.name if dformat is not None else dformat, 404 | }) 405 | 406 | if data is not None: 407 | self.text = data 408 | 409 | @property 410 | def dimensions(self) -> tuple[int, ...]: 411 | if self._dimensions is None: 412 | raise AttributeError 413 | 414 | return self._dimensions 415 | 416 | @classmethod 417 | def as_reference(cls, 418 | reference_name: str, *, 419 | parent: Element | None = None) -> DataItem: 420 | """ 421 | :param reference_name: a name or an absolute reference to another 422 | :class:`DataItem`. The name is just the ``Name`` attribute of 423 | the item, which is assumed to be in the top :class:`Domain`. If 424 | another :class:`DataItem` needs to be references, or there are 425 | multiple domains, use an absolute reference path, as defined 426 | in the `XDMF docs `__. 427 | """ 428 | 429 | if not reference_name.startswith("/"): 430 | reference = f"/Xdmf/Domain/DataItem[@Name='{reference_name}']" 431 | else: 432 | reference = reference_name 433 | 434 | return cls( 435 | reference="XML", 436 | data=reference, 437 | itype=None, ntype=None, precision=None, 438 | endian=None, dformat=None, 439 | parent=parent) 440 | 441 | 442 | def _data_item_format_from_str(text: str) -> DataItemFormat: 443 | # NOTE: this handles two cases (not meant to be too smart about it) 444 | # 1. if the text is just `some_file.bin` or `some_file.out`, we assume it's 445 | # a custom binary file. 446 | # 2. if the text is `some_file.h5:/group/dataset`, we assume it's an HDF5 447 | # file. 448 | if ":" not in text: 449 | return DataItemFormat.Binary 450 | 451 | try: 452 | filename, _ = text.split(":") 453 | except ValueError as exc: 454 | raise ValueError("cannot determine format from text") from exc 455 | 456 | if filename.endswith(".h5"): 457 | return DataItemFormat.HDF 458 | else: 459 | raise ValueError("cannot determine format from text") 460 | 461 | 462 | def _data_item_from_numpy( 463 | ary: np.ndarray[Any, np.dtype[Any]], *, 464 | name: str | None = None, 465 | parent: Element | None = None, 466 | data: str | None = None, 467 | dformat: DataItemFormat | None = None) -> DataItem: 468 | """Create a :class:`DataItem` from a given :class:`~numpy.ndarray`. 469 | 470 | .. note:: 471 | 472 | This is meant for internal use only. Use :class:`NumpyDataArray` instead. 473 | """ 474 | 475 | if dformat is None: 476 | if data is None: 477 | dformat = DataItemFormat.XML 478 | else: 479 | dformat = _data_item_format_from_str(data) 480 | 481 | return DataItem( 482 | name=name, 483 | dimensions=ary.shape, 484 | itype=DataItemType.Uniform, 485 | ntype=DataItemNumberType.from_dtype(ary.dtype), 486 | precision=ary.dtype.itemsize, 487 | endian=DataItemEndian.from_system(), 488 | dformat=dformat, 489 | parent=parent, 490 | data=data, 491 | ) 492 | 493 | 494 | def _join_data_items( 495 | items: tuple[DataItem, ...], *, 496 | parent: Element | None = None) -> DataItem: 497 | r"""Joins several :class:`DataItem`\ s using a :attr:`DataItemType.Function` 498 | as:: 499 | 500 | JOIN($0, $1, ...) 501 | 502 | (Used for describing vectors from scalar data.) 503 | See the `Xdmf Function docs `__ 504 | for more information. 505 | 506 | :returns: the newly created :class:`DataItem` that joins the input items. 507 | """ 508 | 509 | if len(items) == 1: 510 | item = items[0] 511 | else: 512 | from pytools import is_single_valued 513 | if not is_single_valued(item.dimensions for item in items): 514 | raise ValueError("items must have the same dimension") 515 | 516 | dimensions = (len(items), *items[0].dimensions) 517 | ids = ", ".join(f"${i}" for i in range(dimensions[0])) 518 | 519 | item = DataItem( 520 | dimensions=dimensions, 521 | itype=DataItemType.Function, 522 | ntype=None, 523 | precision=None, 524 | function=f"JOIN({ids})", 525 | endian=None, 526 | dformat=None, 527 | ) 528 | 529 | for subitem in items: 530 | item.append(subitem) 531 | 532 | if parent is not None: 533 | parent.append(item) 534 | 535 | return item 536 | 537 | # }}} 538 | 539 | 540 | # {{{ grid 541 | 542 | class GridType(enum.Enum): 543 | """General structure of the connectivity.""" 544 | Uniform = enum.auto() 545 | Collection = enum.auto() 546 | Tree = enum.auto() 547 | SubSet = enum.auto() 548 | 549 | 550 | class CollectionType(enum.Enum): 551 | Spatial = enum.auto() 552 | Temporal = enum.auto() 553 | 554 | 555 | class Grid(XdmfElement): 556 | """ 557 | .. automethod:: __init__ 558 | """ 559 | 560 | def __init__( 561 | self, *, 562 | name: str | None = None, 563 | gtype: GridType = GridType.Uniform, 564 | ctype: CollectionType | None = None, 565 | parent: Element | None = None, 566 | ) -> None: 567 | """ 568 | :param parent: if provided, *self* is appended to the element. 569 | """ 570 | 571 | if gtype == GridType.Collection and ctype is None: 572 | ctype = CollectionType.Spatial 573 | 574 | super().__init__(parent, "Grid", { 575 | "Name": name, 576 | "GridType": gtype.name, 577 | "CollectionType": ctype.name if ctype is not None else ctype, 578 | }) 579 | 580 | # }}} 581 | 582 | 583 | # {{{ topology 584 | 585 | class TopologyType(enum.IntEnum): 586 | """Element and mesh layouts.""" 587 | NoTopology = 0x0 588 | Mixed = 0x70 589 | 590 | # linear elements from XdmfTopologyType.cpp 591 | Polyvertex = 0x1 592 | Polyline = 0x2 593 | Polygon = 0x3 594 | Triangle = 0x4 595 | Quadrilateral = 0x5 596 | Tetrahedron = 0x6 597 | Pyramid = 0x7 598 | Wedge = 0x8 599 | Hexahedron = 0x9 600 | Polyhedron = 0x10 601 | # quadratic elements from XdmfTopologyType.cpp 602 | Edge_3 = 0x22 603 | Triangle_6 = 0x24 604 | Quadrilateral_8 = 0x25 605 | Tetrahedron_10 = 0x26 606 | Pyramid_13 = 0x27 607 | Wedge_15 = 0x28 608 | Wedge_18 = 0x29 609 | Hexahedron_20 = 0x30 610 | # high-order elements from XdmfTopologyType.cpp 611 | Hexahedron_24 = 0x31 612 | Hexahedron_27 = 0x32 613 | Hexahedron_64 = 0x33 614 | Hexahedron_125 = 0x34 615 | Hexahedron_216 = 0x35 616 | Hexahedron_343 = 0x36 617 | Hexahedron_512 = 0x37 618 | Hexahedron_729 = 0x38 619 | Hexahedron_1000 = 0x39 620 | Hexahedron_1331 = 0x40 621 | # spectral elements from XdmfTopologyType.cpp 622 | Hexahedron_Spectral_64 = 0x41 623 | Hexahedron_Spectral_125 = 0x42 624 | Hexahedron_Spectral_216 = 0x43 625 | Hexahedron_Spectral_343 = 0x44 626 | Hexahedron_Spectral_512 = 0x45 627 | Hexahedron_Spectral_729 = 0x46 628 | Hexahedron_Spectral_1000 = 0x47 629 | Hexahedron_Spectral_1331 = 0x48 630 | 631 | # structured from XdmfCurvilinearGrid.cpp 632 | SMesh2D = 0x1110 # Curvilinear mesh 633 | SMesh3D = 0x1110 634 | # structured from XdfmRectilinearGrid.cpp 635 | RectMesh2D = 0x1101 # Mesh with perpendicular axes 636 | RectMesh3D = 0x1101 637 | # structured from XdmfRegularGrid.cpp 638 | CoRectMesh2D = 0x1102 # Mesh with equally spaced perpendicular axes 639 | CoRectMesh3D = 0x1102 640 | 641 | 642 | class Topology(XdmfElement): 643 | """ 644 | .. automethod:: __init__ 645 | """ 646 | 647 | def __init__( 648 | self, *, 649 | ttype: TopologyType, 650 | nodes_per_element: int | None = None, 651 | number_of_elements: int | None = None, 652 | dimensions: tuple[int, ...] | None = None, 653 | parent: Element | None = None, 654 | ) -> None: 655 | """ 656 | :param parent: if provided, *self* is appended to the element. 657 | """ 658 | 659 | ttype_name = _XDMF_TOPOLOGY_TYPE_TO_NAME.get(ttype, ttype.name) 660 | if ttype in _XDMF_ELEMENT_NODE_COUNT or ttype in _XDMF_STRUCTURED_GRIDS: 661 | if nodes_per_element is not None: 662 | raise ValueError(f"cannot set 'nodes_per_element' for {ttype_name}") 663 | else: 664 | if nodes_per_element is None: 665 | raise ValueError(f"'nodes_per_element' required for {ttype_name}") 666 | 667 | super().__init__(parent, "Topology", { 668 | "TopologyType": ttype_name, 669 | "Dimensions": dimensions, 670 | "NodesPerElement": nodes_per_element, 671 | "NumberOfElements": number_of_elements, 672 | }) 673 | 674 | 675 | _XDMF_ELEMENT_NODE_COUNT = { 676 | TopologyType.Polyvertex: 1, 677 | # TopologyType.Polyline: user-defined 678 | # TopologyType.Polygon: user-defined 679 | TopologyType.Triangle: 3, 680 | TopologyType.Quadrilateral: 4, 681 | TopologyType.Tetrahedron: 4, 682 | TopologyType.Pyramid: 5, 683 | TopologyType.Wedge: 6, 684 | TopologyType.Hexahedron: 8, 685 | # TopologyType.Polyhedron: user-defined 686 | TopologyType.Edge_3: 3, 687 | TopologyType.Triangle_6: 6, 688 | TopologyType.Quadrilateral_8: 8, 689 | TopologyType.Tetrahedron_10: 10, 690 | TopologyType.Pyramid_13: 13, 691 | TopologyType.Wedge_15: 15, 692 | TopologyType.Wedge_18: 18, 693 | TopologyType.Hexahedron_20: 20, 694 | TopologyType.Hexahedron_24: 24, 695 | TopologyType.Hexahedron_27: 27, 696 | TopologyType.Hexahedron_64: 64, 697 | TopologyType.Hexahedron_125: 125, 698 | TopologyType.Hexahedron_216: 216, 699 | TopologyType.Hexahedron_343: 343, 700 | TopologyType.Hexahedron_512: 512, 701 | TopologyType.Hexahedron_729: 729, 702 | TopologyType.Hexahedron_1000: 1000, 703 | TopologyType.Hexahedron_1331: 1331, 704 | TopologyType.Hexahedron_Spectral_64: 64, 705 | TopologyType.Hexahedron_Spectral_125: 125, 706 | TopologyType.Hexahedron_Spectral_216: 216, 707 | TopologyType.Hexahedron_Spectral_343: 343, 708 | TopologyType.Hexahedron_Spectral_512: 512, 709 | TopologyType.Hexahedron_Spectral_729: 729, 710 | TopologyType.Hexahedron_Spectral_1000: 1000, 711 | TopologyType.Hexahedron_Spectral_1331: 1331, 712 | } 713 | 714 | 715 | _XDMF_STRUCTURED_GRIDS = { 716 | TopologyType.SMesh2D, 717 | TopologyType.SMesh3D, 718 | TopologyType.RectMesh2D, 719 | TopologyType.RectMesh3D, 720 | TopologyType.CoRectMesh2D, 721 | TopologyType.CoRectMesh3D, 722 | } 723 | 724 | 725 | # NOTE: the names in TopologyType are weird because python identifiers cannot 726 | # start with a number, so 2DSMesh is not allowed 727 | _XDMF_TOPOLOGY_TYPE_TO_NAME = { 728 | TopologyType.SMesh2D: "2DSMesh", 729 | TopologyType.SMesh3D: "3DSMesh", 730 | TopologyType.RectMesh2D: "2DRectMesh", 731 | TopologyType.RectMesh3D: "3DRectMesh", 732 | TopologyType.CoRectMesh2D: "2DCoRectMesh", 733 | TopologyType.CoRectMesh3D: "3DCoRectMesh", 734 | } 735 | 736 | # }}} 737 | 738 | 739 | # {{{ geometry 740 | 741 | 742 | class GeometryType(enum.Enum): 743 | """Data layout of the node coordinates.""" 744 | XY = enum.auto() 745 | XYZ = enum.auto() 746 | # NOTE: all of these don't seem to be supported in VTK/Paraview with 747 | # XDMF3 for some reason, but are still mentioned in the XDMF source 748 | VXVY = enum.auto() 749 | VXVYVZ = enum.auto() 750 | ORIGIN_DXDY = enum.auto() 751 | ORIGIN_DXDYDZ = enum.auto() 752 | 753 | 754 | class Geometry(XdmfElement): 755 | """ 756 | .. automethod:: __init__ 757 | """ 758 | 759 | def __init__( 760 | self, *, 761 | name: str | None = None, 762 | gtype: GeometryType = GeometryType.XYZ, 763 | parent: Element | None = None, 764 | ): 765 | """ 766 | :param parent: if provided, *self* is appended to the element. 767 | """ 768 | 769 | super().__init__(parent, "Geometry", { 770 | "Name": name, 771 | "GeometryType": gtype.name, 772 | }) 773 | 774 | # }}} 775 | 776 | 777 | # {{{ time 778 | 779 | class Time(XdmfElement): 780 | """ 781 | .. automethod:: __init__ 782 | """ 783 | 784 | def __init__( 785 | self, *, 786 | value: str, 787 | parent: Element | None = None, 788 | ): 789 | """ 790 | :param parent: if provided, *self* is appended to the element. 791 | """ 792 | 793 | super().__init__(parent, "Time", { 794 | "Value": value, 795 | }) 796 | 797 | # }}} 798 | 799 | 800 | # {{{ domain 801 | 802 | class Domain(XdmfElement): 803 | """ 804 | .. automethod:: __init__ 805 | """ 806 | 807 | def __init__( 808 | self, *, 809 | name: str | None = None, 810 | parent: Element | None = None, 811 | ): 812 | """ 813 | :param parent: if provided, *self* is appended to the element. 814 | """ 815 | 816 | super().__init__(parent, "Domain", { 817 | "Name": name, 818 | }) 819 | 820 | # }}} 821 | 822 | 823 | # {{{ information 824 | 825 | class Information(XdmfElement): 826 | """ 827 | .. automethod:: __init__ 828 | """ 829 | 830 | def __init__( 831 | self, *, 832 | name: str, 833 | value: str, 834 | parent: Element | None = None, 835 | ): 836 | """ 837 | :param parent: if provided, *self* is appended to the element. 838 | """ 839 | 840 | super().__init__(parent, "Information", { 841 | "Name": name, 842 | "Value": value, 843 | }) 844 | 845 | # }}} 846 | 847 | 848 | # {{{ include 849 | 850 | class XInclude(XdmfElement): 851 | """ 852 | .. automethod:: __init__ 853 | """ 854 | 855 | def __init__( 856 | self, *, 857 | href: str | None, 858 | xpointer: str | None = None, 859 | parent: Element | None = None, 860 | ): 861 | """ 862 | :param parent: if provided, *self* is appended to the element. 863 | :param xpointer: path inside the file represented by *href*. 864 | """ 865 | if xpointer is not None: 866 | xpointer = f"xpointer({xpointer})" 867 | 868 | super().__init__(parent, "xi:include", { 869 | "href": href, 870 | "xpointer": xpointer, 871 | }) 872 | 873 | # }}} 874 | 875 | # }}} 876 | 877 | 878 | # {{{ data arrays 879 | 880 | def _ndarray_to_string(ary: Any) -> str: 881 | if not isinstance(ary, np.ndarray): 882 | raise TypeError(f"expected an 'ndarray', got '{type(ary).__name__}'") 883 | 884 | ntype = DataItemNumberType.from_dtype(ary.dtype) 885 | if ntype == DataItemNumberType.Int or ntype == DataItemNumberType.UInt: 886 | fmt = "%d" 887 | elif ntype == DataItemNumberType.Float: 888 | if ary.dtype.itemsize == 8: 889 | fmt = "%.16e" 890 | elif ary.dtype.itemsize == 4: 891 | fmt = "%.8e" 892 | else: 893 | raise ValueError(f"unsupported dtype item size: {ary.dtype.itemsize}") 894 | else: 895 | raise ValueError(f"unsupported dtype: '{ary.dtype}'") 896 | 897 | import io 898 | bio = io.BytesIO() 899 | np.savetxt(bio, ary, fmt=fmt) 900 | 901 | return "\n" + bio.getvalue().decode() 902 | 903 | 904 | def _geometry_type_from_points(points: DataArray) -> GeometryType: 905 | dims = points.shape[-1] 906 | 907 | if len(points.components) == 1: 908 | if dims == 2: 909 | return GeometryType.XY 910 | elif dims == 3: 911 | return GeometryType.XYZ 912 | else: 913 | raise ValueError(f"unsupported dimension: '{dims}'") 914 | else: 915 | if dims == 2: 916 | return GeometryType.VXVY 917 | elif dims == 3: 918 | return GeometryType.VXVYVZ 919 | else: 920 | raise ValueError(f"unsupported dimension: '{dims}'") 921 | 922 | 923 | class DataArray: 924 | r"""An array represented as a list of :class:`DataItem`\ s.""" 925 | 926 | def __init__( 927 | self, 928 | components: tuple[DataItem, ...], *, 929 | name: str | None = None, 930 | acenter: AttributeCenter | None = None, 931 | atype: AttributeType | None = None, 932 | ): 933 | r""" 934 | :param components: a description of each component of an array. 935 | :param name: name of the array. This name will be used if the array 936 | is added as an attribute, otherwise the names of the *components* 937 | are used. 938 | """ 939 | if not isinstance(components, tuple): 940 | raise TypeError("'components' should be a tuple") 941 | 942 | if name is None: 943 | if len(components) == 1: 944 | name = components[0].name 945 | else: 946 | # NOTE: assumes component names are {name}_{suffix} 947 | name = os.path.commonprefix([ 948 | c.name for c in components 949 | ]).strip("_") 950 | 951 | self.components = components 952 | 953 | self.name = name 954 | self.acenter = acenter 955 | self.atype = atype 956 | 957 | @property 958 | def shape(self) -> tuple[int, ...]: 959 | """The shape of the data array.""" 960 | if len(self.components) == 1: 961 | return self.components[0].dimensions 962 | else: 963 | return (len(self.components), *self.components[0].dimensions) 964 | 965 | def as_data_item(self, *, 966 | parent: Element | None = None) -> tuple[DataItem, ...]: 967 | r"""Finalize the :class:`DataArray` and construct :class:`DataItem`\ s 968 | to be written to a file. 969 | """ 970 | 971 | items = self.components[:] 972 | if parent is not None: 973 | for item in items: 974 | if parent.tag != "Attribute": 975 | item.set("Name", self.name) 976 | 977 | parent.append(item) 978 | 979 | return items 980 | 981 | @classmethod 982 | def from_dataset(cls, 983 | dset: Any, 984 | acenter: AttributeCenter = AttributeCenter.Node, 985 | atype: AttributeType | None = None) -> DataArray: 986 | """Create a :class:`DataArray` from an HDF5 ``Dataset``. 987 | 988 | :arg dset: an object that resembles an HDF5 dataset. We only access the 989 | fields *dtype*, *shape*, *name* and *file*. 990 | """ 991 | filename = dset.file.filename 992 | data = f"{filename}:{dset.name}" 993 | name = dset.name.split("/")[-1] 994 | 995 | item = _data_item_from_numpy(dset, 996 | data=data, 997 | dformat=DataItemFormat.HDF) 998 | 999 | return cls( 1000 | components=(item,), 1001 | name=name, 1002 | acenter=acenter, 1003 | atype=atype) 1004 | 1005 | 1006 | class NumpyDataArray(DataArray): 1007 | """ 1008 | .. automethod:: __init__ 1009 | """ 1010 | 1011 | def __init__( 1012 | self, 1013 | ary: np.ndarray[Any, np.dtype[Any]], *, 1014 | acenter: AttributeCenter | None = None, 1015 | name: str | None = None, 1016 | ): 1017 | """ 1018 | :param ary: if this is an :class:`object` array, each entry is considered 1019 | a different component and will consist of a separate 1020 | :class:`DataItem`. 1021 | """ 1022 | 1023 | if ary.dtype.char == "O": 1024 | from pytools import is_single_valued 1025 | if not is_single_valued(iary.shape for iary in ary): 1026 | raise ValueError("'ary' components must have the same size") 1027 | 1028 | items = tuple(_data_item_from_numpy(iary, name=f"{name}_{i}") 1029 | for i, iary in enumerate(ary)) 1030 | else: 1031 | items = (_data_item_from_numpy(ary, name=name),) 1032 | 1033 | super().__init__(items, name=name, acenter=acenter) 1034 | self.ary = ary 1035 | 1036 | def as_data_item(self, *, 1037 | parent: Element | None = None) -> tuple[DataItem, ...]: 1038 | items = super().as_data_item(parent=parent) 1039 | 1040 | ary = tuple(self.ary) if self.ary.dtype.char == "O" else (self.ary,) 1041 | for item, iary in zip(items, ary, strict=True): 1042 | item.text = _ndarray_to_string(iary) 1043 | 1044 | return items 1045 | 1046 | # }}} 1047 | 1048 | 1049 | # {{{ grids 1050 | 1051 | class XdmfGrid: 1052 | """ 1053 | .. automethod:: __init__ 1054 | .. automethod:: add_attribute 1055 | """ 1056 | 1057 | def __init__(self, root: Grid): 1058 | self.root = root 1059 | 1060 | def getroot(self) -> Grid: 1061 | return self.root 1062 | 1063 | def add_attribute(self, ary: DataArray, *, join: bool = True) -> Attribute: 1064 | """ 1065 | :param ary: 1066 | :param join: If *True* and *ary* has multiple components, they are 1067 | joined using an XDMF Function. 1068 | """ 1069 | 1070 | acenter = ary.acenter 1071 | if acenter is None: 1072 | acenter = AttributeCenter.Node 1073 | 1074 | atype = ary.atype 1075 | if atype is None: 1076 | atype = AttributeType.from_shape(ary.shape[::-1]) 1077 | 1078 | attr = Attribute( 1079 | name=ary.name, 1080 | atype=atype, 1081 | acenter=acenter, 1082 | parent=self.getroot(), 1083 | ) 1084 | 1085 | if join: 1086 | items = ary.as_data_item() 1087 | _join_data_items(items, parent=attr) 1088 | else: 1089 | ary.as_data_item(parent=attr) 1090 | 1091 | return attr 1092 | 1093 | 1094 | class XdmfUnstructuredGrid(XdmfGrid): 1095 | """ 1096 | .. automethod:: __init__ 1097 | """ 1098 | 1099 | def __init__(self, 1100 | points: DataArray, 1101 | connectivity: DataArray, *, 1102 | topology_type: Topology | TopologyType, 1103 | name: str | None = None, 1104 | geometry_type: GeometryType | None = None): 1105 | if geometry_type is None: 1106 | geometry_type = _geometry_type_from_points(points) 1107 | 1108 | if geometry_type not in (GeometryType.XY, GeometryType.XYZ): 1109 | # NOTE: Paraview 5.8 seems confused when using VXVY geometry types 1110 | raise ValueError(f"unsupported geometry type: '{geometry_type}'") 1111 | 1112 | grid = Grid(parent=None, name=name) 1113 | 1114 | nelements = int(np.prod(connectivity.shape[:-1])) 1115 | if isinstance(topology_type, TopologyType): 1116 | nodes_per_element = 2 if topology_type == TopologyType.Polyline else None 1117 | topology: XdmfElement = Topology( 1118 | parent=grid, 1119 | ttype=topology_type, 1120 | nodes_per_element=nodes_per_element, 1121 | number_of_elements=nelements) 1122 | elif isinstance(topology_type, Topology): 1123 | topology = topology_type.replace(**{ 1124 | "parent": grid, 1125 | "number_of_elements": nelements 1126 | }) 1127 | else: 1128 | raise TypeError(f"unsupported type: {type(topology_type).__name__}") 1129 | 1130 | connectivity.as_data_item(parent=topology) 1131 | 1132 | geometry = Geometry(parent=grid, gtype=geometry_type) 1133 | points.as_data_item(parent=geometry) 1134 | 1135 | super().__init__(grid) 1136 | 1137 | # }}} 1138 | 1139 | 1140 | # {{{ writer 1141 | 1142 | T = TypeVar("T") 1143 | 1144 | 1145 | def not_none(obj: T | None) -> T: 1146 | assert obj is not None 1147 | return obj 1148 | 1149 | 1150 | class XdmfWriter(ElementTree): 1151 | """ 1152 | .. automethod:: __init__ 1153 | .. automethod:: write 1154 | .. automethod:: write_pretty 1155 | """ 1156 | 1157 | def __init__(self, 1158 | grids: tuple[XdmfGrid, ...], *, 1159 | arrays: tuple[DataArray, ...] | None = None, 1160 | tags: tuple[Element, ...] | None = None): 1161 | r""" 1162 | :param grids: a :class:`tuple` of grids to be added to the 1163 | top :class:`Domain`. Currently only a single domain is supported. 1164 | :param arrays: additional :class:`DataArray`\ s to be added to the 1165 | top :class:`Domain`, as opposed to as attribute on the grids. 1166 | """ 1167 | root = Element("Xdmf", { 1168 | "xmlns:xi": "http://www.w3.org/2001/XInclude", 1169 | "Version": "3.0", 1170 | }) 1171 | 1172 | domain = Domain(parent=root) 1173 | if arrays is not None: 1174 | for ary in arrays: 1175 | ary.as_data_item(parent=domain) 1176 | 1177 | if tags is not None: 1178 | for tag in tags: 1179 | domain.append(tag) 1180 | 1181 | for grid in grids: 1182 | domain.append(grid.getroot()) 1183 | 1184 | super().__init__(root) 1185 | 1186 | def write_pretty(self, filename: str) -> None: 1187 | """Produces a nicer-looking XML file with clean indentation.""" 1188 | # https://stackoverflow.com/a/1206856 1189 | from xml.dom import minidom 1190 | from xml.etree.ElementTree import tostring 1191 | dom = minidom.parseString(tostring( 1192 | not_none(self.getroot()), 1193 | encoding="utf-8", 1194 | short_empty_elements=False, 1195 | )) 1196 | 1197 | with open(filename, "wb") as fd: 1198 | fd.write(dom.toprettyxml(indent=" ", encoding="utf-8")) 1199 | 1200 | def write(self, filename: str) -> None: # pyright: ignore[reportIncompatibleMethodOverride] 1201 | """Write the the XDMF file.""" 1202 | super().write( 1203 | filename, 1204 | encoding="utf-8", 1205 | xml_declaration=True, 1206 | short_empty_elements=False, 1207 | ) 1208 | 1209 | # }}} 1210 | --------------------------------------------------------------------------------