├── .flake8 ├── .gitignore ├── .isort.cfg ├── .readthedocs.yml ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── conf.py └── index.rst ├── examples └── mapping.ipynb ├── pymapping ├── __about__.py ├── __init__.py ├── __main__.py ├── cli.py └── main.py ├── setup.py ├── tasks.py ├── test ├── test_1d.py └── test_2d.py └── test_requirements.txt /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203,E266,E501,W503 3 | max-line-length = 80 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output=3 3 | include_trailing_comma=True 4 | force_grid_wrap=0 5 | use_parentheses=True 6 | line_length=88 7 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.7 5 | install: 6 | - method: pip 7 | path: . 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | addons: 4 | apt: 5 | packages: 6 | - gmsh 7 | 8 | python: 9 | - "3.6" 10 | 11 | before_install: 12 | - pip3 install -U -r test_requirements.txt 13 | 14 | install: 15 | - pip3 install -U . 16 | 17 | script: 18 | - cd test 19 | - pytest --cov pymapping 20 | 21 | after_success: 22 | - codecov 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Tianyi Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapping finite element data between meshes 2 | 3 | [![travis](https://img.shields.io/travis/tianyikillua/pymapping.svg?style=flat-square)](https://travis-ci.org/tianyikillua/pymapping) 4 | [![codecov](https://img.shields.io/codecov/c/github/tianyikillua/pymapping.svg?style=flat-square)](https://codecov.io/gh/tianyikillua/pymapping) 5 | [![readthedocs](https://readthedocs.org/projects/pymapping/badge/?version=latest&style=flat-square)](https://readthedocs.org/projects/pymapping/?badge=latest) 6 | [![pypi](https://img.shields.io/pypi/v/pymapping.svg?style=flat-square)](https://pypi.org/project/pymapping) 7 | 8 | This package is a handy Python (re-)wrapper of [MEDCoupling](https://docs.salome-platform.org/latest/dev/MEDCoupling/developer/index.html). It can be used to transfer finite element data defined on nodes (P1 fields) or on cells (P0 fields) between two [meshio](https://github.com/nschloe/meshio)-compatible meshes. 9 | 10 |

11 | 12 |

13 | 14 | Some notebook examples can be found in `examples`. 15 | 16 | Documentation is available [here](https://pymapping.readthedocs.io). 17 | 18 | ### Installation 19 | 20 | To install `pymapping`, you are invited to use `pip` and its associated options 21 | 22 | ``` 23 | pip install -U pymapping 24 | ``` 25 | 26 | The MEDCoupling library is a strong dependency of this package. Currently by installing via the previous `pip` command it will automatically install the [medcoupling](https://github.com/tianyikillua/medcoupling) Python package (a repackaging of the official library). 27 | 28 | ### Testing 29 | 30 | To run the `pymapping` unit tests, check out this repository and type 31 | 32 | ``` 33 | pytest 34 | ``` 35 | 36 | ### License 37 | 38 | `pymapping` is published under the [MIT license](https://en.wikipedia.org/wiki/MIT_License). 39 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("../")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "pymapping" 22 | copyright = "2019-2021, Tianyi Li" 23 | author = "Tianyi Li" 24 | 25 | 26 | # -- General configuration --------------------------------------------------- 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named "sphinx.ext.*") or your custom 30 | # ones. 31 | extensions = [ 32 | "sphinx.ext.autodoc", 33 | "sphinx.ext.mathjax", 34 | "sphinx.ext.napoleon", 35 | "sphinx.ext.todo", 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ["_templates"] 40 | 41 | # The master toctree document. 42 | master_doc = "index" 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 48 | 49 | # Display todos by setting to True 50 | todo_include_todos = True 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = "default" 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | html_static_path = ["_static"] 63 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Mapping finite element data between meshes 2 | ========================================== 3 | 4 | .. automodule:: pymapping 5 | :members: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | Indices and tables 11 | ================== 12 | 13 | * :ref:`genindex` 14 | * :ref:`modindex` 15 | * :ref:`search` 16 | -------------------------------------------------------------------------------- /pymapping/__about__.py: -------------------------------------------------------------------------------- 1 | __author__ = "Tianyi Li" 2 | __email__ = "tianyikillua@gmail.com" 3 | __copyright__ = "Copyright (c) 2019-2020 {} <{}>".format(__author__, __email__) 4 | __license__ = "License :: OSI Approved :: MIT License" 5 | __version__ = "0.2.0" 6 | __status__ = "Development Status :: 4 - Beta" 7 | -------------------------------------------------------------------------------- /pymapping/__init__.py: -------------------------------------------------------------------------------- 1 | from . import cli 2 | from .__about__ import __author__, __email__, __license__, __status__, __version__ 3 | from .main import ( 4 | Mapper, 5 | MappingResult, 6 | cleanup_mesh_meshio, 7 | field_mc_from_meshio, 8 | mesh_mc_from_meshio, 9 | ) 10 | 11 | __all__ = [ 12 | "__author__", 13 | "__email__", 14 | "__license__", 15 | "__version__", 16 | "__status__", 17 | "cli", 18 | "Mapper", 19 | "cleanup_mesh_meshio", 20 | "MappingResult", 21 | "mesh_mc_from_meshio", 22 | "field_mc_from_meshio", 23 | ] 24 | -------------------------------------------------------------------------------- /pymapping/__main__.py: -------------------------------------------------------------------------------- 1 | import pymapping 2 | 3 | if __name__ == "__main__": 4 | pymapping.cli.main() 5 | -------------------------------------------------------------------------------- /pymapping/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import numpy as np 4 | 5 | from .__about__ import __copyright__, __version__ 6 | from .main import Mapper 7 | 8 | 9 | def main(argv=None): 10 | # Parse command line arguments. 11 | parser = _get_parser() 12 | args = parser.parse_args(argv) 13 | 14 | import meshio 15 | 16 | mapper = Mapper(verbose=args.verbose) 17 | 18 | mesh_source = meshio.read(args.mesh_source) 19 | mesh_target = meshio.read(args.mesh_target) 20 | mapper.prepare(mesh_source, mesh_target, args.method, args.intersection_type) 21 | res = mapper.transfer(args.field_name, args.nature) 22 | 23 | if ".txt" in args.outfile: 24 | np.savetxt(args.outfile, res.array()) 25 | elif ".npy" in args.outfile: 26 | np.save(args.outfile, res.array()) 27 | elif ".vtu" in args.outfile: 28 | res.export_vtk(args.outfile) 29 | else: 30 | mesh_target = res.mesh_meshio() 31 | meshio.write(args.outfile, mesh_target) 32 | 33 | return 34 | 35 | 36 | def _get_parser(): 37 | import argparse 38 | 39 | parser = argparse.ArgumentParser( 40 | description=("Mapping finite element data between meshes"), 41 | formatter_class=argparse.RawTextHelpFormatter, 42 | ) 43 | 44 | parser.add_argument( 45 | "mesh_source", type=str, help="meshio-compatible source mesh file" 46 | ) 47 | 48 | parser.add_argument( 49 | "mesh_target", type=str, help="meshio-compatible target mesh file" 50 | ) 51 | 52 | parser.add_argument( 53 | "field_name", 54 | type=str, 55 | help="field defined in the source mesh to transfer to the target mesh", 56 | ) 57 | 58 | parser.add_argument( 59 | "outfile", 60 | type=str, 61 | help="file to store mapped data: .txt, .npy or meshio-compatible mesh", 62 | ) 63 | 64 | parser.add_argument( 65 | "--method", 66 | type=str, 67 | choices=["P1P1", "P1P0", "P0P1", "P0P0"], 68 | default="P1P1", 69 | help="mapping method", 70 | ) 71 | 72 | parser.add_argument("--intersection_type", type=str, help="intersection algorithm") 73 | 74 | parser.add_argument( 75 | "--nature", 76 | type=str, 77 | default="IntensiveMaximum", 78 | help="physical nature of the field", 79 | ) 80 | 81 | parser.add_argument( 82 | "--verbose", 83 | action="store_true", 84 | default=False, 85 | help="increase output verbosity", 86 | ) 87 | 88 | version_text = "\n".join( 89 | [ 90 | "pymapping {} [Python {}.{}.{}]".format( 91 | __version__, 92 | sys.version_info.major, 93 | sys.version_info.minor, 94 | sys.version_info.micro, 95 | ), 96 | __copyright__, 97 | ] 98 | ) 99 | parser.add_argument( 100 | "--version", 101 | "-v", 102 | action="version", 103 | version=version_text, 104 | help="display version information", 105 | ) 106 | 107 | return parser 108 | -------------------------------------------------------------------------------- /pymapping/main.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | import medcoupling as mc 4 | import numpy as np 5 | from meshio import CellBlock 6 | 7 | meshio_to_mc_type = { 8 | "vertex": mc.NORM_POINT1, 9 | "line": mc.NORM_SEG2, 10 | "triangle": mc.NORM_TRI3, 11 | "quad": mc.NORM_QUAD4, 12 | "tetra": mc.NORM_TETRA4, 13 | "pyramid": mc.NORM_PYRA5, 14 | "hexahedron": mc.NORM_HEXA8, 15 | } 16 | mc_to_meshio_type = {v: k for k, v in meshio_to_mc_type.items()} 17 | 18 | celltype_3d = ["tetra", "pyramid", "hexahedron"] 19 | celltype_2d = ["triangle", "quad"] 20 | celltype_1d = ["line"] 21 | celltype_0d = ["vertex"] 22 | 23 | 24 | def meshdim(mesh): 25 | """ 26 | Determine the mesh dimension of a meshio mesh 27 | 28 | Returns: 29 | int: Mesh dimension 30 | """ 31 | celltypes = list(mesh.cells_dict.keys()) 32 | if len(set(celltype_3d).intersection(celltypes)) > 0: 33 | meshdim = 3 34 | elif len(set(celltype_2d).intersection(celltypes)) > 0: 35 | meshdim = 2 36 | else: 37 | meshdim = 1 38 | return meshdim 39 | 40 | 41 | def cleanup_mesh_meshio(mesh): 42 | """ 43 | Drop all cells with a lower dimension from a meshio mesh 44 | """ 45 | meshdim_ = meshdim(mesh) 46 | if meshdim_ == 3: 47 | celltypes = celltype_2d + celltype_1d + celltype_0d 48 | elif meshdim_ == 2: 49 | celltypes = celltype_1d + celltype_0d 50 | else: 51 | celltypes = celltype_0d 52 | 53 | cells = mesh.cells_dict 54 | cell_data = mesh.cell_data_dict 55 | for celltype in celltypes: 56 | cells.pop(celltype, None) 57 | for key in cell_data: 58 | for celltype in celltypes: 59 | cell_data[key].pop(celltype, None) 60 | 61 | mesh.cells = [] 62 | for celltype, data in cells.items(): 63 | mesh.cells.append(CellBlock(celltype, data)) 64 | for key in mesh.cell_data: 65 | mesh.cell_data[key] = list(cell_data[key].values()) 66 | 67 | 68 | def mesh_mc_from_meshio(mesh, check=False): 69 | """ 70 | Convert a meshio mesh to a medcoupling mesh 71 | 72 | Args: 73 | mesh (meshio mesh): Mesh object 74 | check (bool): Check if the medcoupling mesh is consist 75 | """ 76 | # Initialization 77 | mesh_mc = mc.MEDCouplingUMesh("mesh", meshdim(mesh)) 78 | 79 | # Point coordinates 80 | coords = mc.DataArrayDouble(mesh.points.copy()) 81 | mesh_mc.setCoords(coords) 82 | 83 | # Cells 84 | cells_dict = mesh.cells_dict 85 | conn = np.array([], dtype=np.int32) 86 | conn_index = np.array([], dtype=np.int32) 87 | for celltype in cells_dict: 88 | celltype_ = meshio_to_mc_type[celltype] 89 | ncells_celltype, npoints_celltype = cells_dict[celltype].shape 90 | col_celltype = celltype_ * np.ones((ncells_celltype, 1), dtype=np.int32) 91 | conn_celltype = np.hstack([col_celltype, cells_dict[celltype]]).flatten() 92 | conn_index_celltype = len(conn) + (1 + npoints_celltype) * np.arange( 93 | ncells_celltype, dtype=np.int32 94 | ) 95 | conn = np.hstack([conn, conn_celltype]) 96 | conn_index = np.hstack([conn_index, conn_index_celltype]) 97 | conn_index = np.hstack([conn_index, [len(conn)]]).astype(np.int32) 98 | conn = mc.DataArrayInt(conn.astype(np.int32)) 99 | conn_index = mc.DataArrayInt(conn_index) 100 | mesh_mc.setConnectivity(conn, conn_index) 101 | 102 | if check: 103 | mesh_mc.checkConsistency() 104 | return mesh_mc 105 | 106 | 107 | def field_mc_from_meshio( 108 | mesh, field_name, on="points", mesh_mc=None, nature="IntensiveMaximum" 109 | ): 110 | """ 111 | Convert a meshio field to a medcoupling field 112 | 113 | Args: 114 | mesh (meshio mesh): Mesh object 115 | field_name (str): Name of the field defined in the ``meshio`` mesh 116 | on (str): Support of the field (``points`` or ``cells``) 117 | mesh_mc (medcoupling mesh): MEDCoupling mesh of the current ``meshio`` mesh 118 | nature (str): Physical nature of the field (``IntensiveMaximum``, ``IntensiveConservation``, ``ExtensiveMaximum`` or ``ExtensiveConservation``) 119 | """ 120 | assert on in ["points", "cells"] 121 | if on == "points": 122 | field = mc.MEDCouplingFieldDouble(mc.ON_NODES, mc.NO_TIME) 123 | else: 124 | field = mc.MEDCouplingFieldDouble(mc.ON_CELLS, mc.NO_TIME) 125 | field.setName(field_name) 126 | if mesh_mc is None: 127 | mesh_mc = mesh_mc_from_meshio(mesh) 128 | field.setMesh(mesh_mc) 129 | 130 | # Point fields 131 | if on == "points": 132 | assert field_name in mesh.point_data 133 | field.setArray(mc.DataArrayDouble(mesh.point_data[field_name].copy())) 134 | else: 135 | # Cell fields 136 | assert on == "cells" 137 | assert field_name in mesh.cell_data_dict 138 | array = None 139 | celltypes_mc = mesh_mc.getAllGeoTypesSorted() 140 | for celltype_mc in celltypes_mc: 141 | celltype = mc_to_meshio_type[celltype_mc] 142 | assert celltype in mesh.cell_data_dict[field_name] 143 | values = mesh.cell_data_dict[field_name][celltype] 144 | if array is None: 145 | array = values 146 | else: 147 | array = np.concatenate([array, values]) 148 | field.setArray(mc.DataArrayDouble(array)) 149 | 150 | field.setNature(eval("mc." + nature)) 151 | return field 152 | 153 | 154 | class MappingResult: 155 | """ 156 | Container class for mapped field on the target mesh 157 | """ 158 | 159 | def __init__(self, field_target, mesh_target=None): 160 | self.field_target = field_target 161 | self.dis = self.field_target.getDiscretization() 162 | 163 | self.mesh_target = mesh_target 164 | 165 | def array(self): 166 | """ 167 | Return the ``numpy`` array of the mapped field 168 | """ 169 | return self.field_target.getArray().toNumPyArray() 170 | 171 | def discretization_points(self): 172 | """ 173 | Return the location of discretization points on which 174 | the field array is defined 175 | """ 176 | return self.dis.getLocalizationOfDiscValues( 177 | self.field_target.getMesh() 178 | ).toNumPyArray() 179 | 180 | def export_vtk(self, vtkfile): 181 | """ 182 | Export the mapped field to a VTK file 183 | """ 184 | self.field_target.writeVTK(vtkfile) 185 | 186 | def mesh_meshio(self): 187 | """ 188 | Return the ``meshio`` mesh object containing the 189 | mapped field 190 | """ 191 | assert self.mesh_target is not None 192 | mesh_target = deepcopy(self.mesh_target) 193 | name = self.field_target.getName() 194 | 195 | # Point fields 196 | array = self.array() 197 | if self.dis.getRepr() == "P1": 198 | mesh_target.point_data[name] = array 199 | else: 200 | # Cell fields 201 | mesh_mc = self.field_target.getMesh() 202 | celltypes_mc = mesh_mc.getAllGeoTypesSorted() 203 | begin = None 204 | end = None 205 | cell_data = {} 206 | for celltype_mc in celltypes_mc: 207 | celltype = mc_to_meshio_type[celltype_mc] 208 | len_mesh_celltype = mesh_mc.getNumberOfCellsWithType(celltype_mc) 209 | if begin is None: 210 | begin = 0 211 | end = len_mesh_celltype 212 | else: 213 | begin = end 214 | end = begin + len_mesh_celltype 215 | cell_data[celltype] = array[begin:end] 216 | 217 | mesh_target.cell_data[name] = [] 218 | for celltype in mesh_target.cells_dict.keys(): 219 | for key in cell_data: 220 | if celltype == key: 221 | mesh_target.cell_data[name].append(cell_data[key]) 222 | break 223 | 224 | return mesh_target 225 | 226 | 227 | class Mapper: 228 | """ 229 | Class for mapping finite element data between meshes 230 | 231 | Args: 232 | verbose (bool): Whehter print out progress information 233 | """ 234 | 235 | def __init__(self, verbose=True): 236 | self.verbose = verbose 237 | 238 | self.mesh_source = None 239 | self.mesh_source_mc = None 240 | self.mesh_target = None 241 | self.mesh_target_mc = None 242 | self.field_source = None 243 | self.field_target = None 244 | 245 | self._mapper = mc.MEDCouplingRemapper() 246 | 247 | def prepare(self, mesh_source, mesh_target, method="P1P0", intersection_type=None): 248 | """ 249 | Prepare field mapping between meshes, must be run before 250 | :py:meth:`~.Mapper.transfer`. The source mesh must contain 251 | the field that you want to transfer to the target mesh. 252 | 253 | Args: 254 | mesh_source (meshio mesh): Source mesh 255 | mesh_target (meshio mesh): Target mesh 256 | method (str): Mapping methods: ``P1P0``, ``P1P1``, ``P0P0`` or ``P0P1`` 257 | intersection_type (str): Intersection algorithm depending on meshes and the method 258 | Most used types: ``Triangulation``, ``PointLocator`` 259 | """ 260 | # Select intersection type 261 | assert method in ["P1P0", "P1P1", "P0P0", "P0P1"] 262 | if intersection_type is not None: 263 | self._mapper.setIntersectionType(eval("mc." + intersection_type)) 264 | elif method[:2] == "P1": 265 | self._mapper.setIntersectionType(mc.PointLocator) 266 | self.method = method 267 | 268 | self._print("Loading source mesh...") 269 | cleanup_mesh_meshio(mesh_source) 270 | self.mesh_source = mesh_source 271 | self.mesh_source_mc = mesh_mc_from_meshio(mesh_source) 272 | 273 | self._print("Loading target mesh...") 274 | cleanup_mesh_meshio(mesh_target) 275 | self.mesh_target = mesh_target 276 | self.mesh_target_mc = mesh_mc_from_meshio(mesh_target) 277 | 278 | self._print("Preparing...") 279 | self._mapper.prepare(self.mesh_source_mc, self.mesh_target_mc, method) 280 | 281 | def transfer(self, field_name, nature="IntensiveMaximum", default_value=np.nan): 282 | """ 283 | Transfer field from the source mesh to the target mesh, after 284 | :py:meth:`~.Mapper.prepare`. 285 | 286 | Args: 287 | field_name (str): Name of the field defined in the source mesh 288 | nature (str): Physical nature of the field (``IntensiveMaximum``, ``IntensiveConservation``, ``ExtensiveMaximum`` or ``ExtensiveConservation``) 289 | default_value (float): Default value when mapping is not possible 290 | """ 291 | self._print("Transfering...") 292 | if self.method[:2] == "P1": 293 | on = "points" 294 | else: 295 | on = "cells" 296 | self.field_source = field_mc_from_meshio( 297 | self.mesh_source, 298 | field_name, 299 | on=on, 300 | mesh_mc=self.mesh_source_mc, 301 | nature=nature, 302 | ) 303 | self.field_target = self._mapper.transferField( 304 | self.field_source, dftValue=default_value 305 | ) 306 | self.field_target.setName(field_name) 307 | return MappingResult(self.field_target, self.mesh_target) 308 | 309 | def _print(self, blabla): 310 | if self.verbose: 311 | print(blabla) 312 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | # https://packaging.python.org/single_source_version/ 6 | base_dir = os.path.abspath(os.path.dirname(__file__)) 7 | about = {} 8 | with open(os.path.join(base_dir, "pymapping", "__about__.py"), "rb") as f: 9 | exec(f.read(), about) 10 | 11 | 12 | setup( 13 | name="pymapping", 14 | version=about["__version__"], 15 | packages=find_packages(), 16 | url="https://github.com/tianyikillua/pymapping", 17 | author=about["__author__"], 18 | author_email=about["__email__"], 19 | install_requires=["numpy", "meshio", "medcoupling"], 20 | description="Mapping finite element data between meshes", 21 | long_description=open("README.md").read(), 22 | long_description_content_type="text/markdown", 23 | license=about["__license__"], 24 | classifiers=[ 25 | about["__license__"], 26 | about["__status__"], 27 | # See for all classifiers. 28 | "Operating System :: Microsoft :: Windows", 29 | "Operating System :: Unix", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Intended Audience :: Science/Research", 34 | "Topic :: Scientific/Engineering", 35 | ], 36 | entry_points={"console_scripts": ["pymapping = pymapping.cli:main"]}, 37 | ) 38 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import shutil 3 | 4 | from invoke import task 5 | 6 | import pymapping 7 | 8 | VERSION = pymapping.__version__ 9 | 10 | 11 | @task 12 | def build(c): 13 | print(f"Building v{VERSION}...") 14 | shutil.rmtree("dist", ignore_errors=True) 15 | if platform.system() == "Windows": 16 | python = "python" 17 | else: 18 | python = "python3" 19 | c.run(f"{python} setup.py sdist") 20 | c.run(f"{python} setup.py bdist_wheel") 21 | 22 | 23 | @task 24 | def tag(c): 25 | print(f"Tagging v{VERSION}...") 26 | c.run(f"git tag v{VERSION}") 27 | c.run("git push --tags") 28 | 29 | 30 | @task 31 | def upload(c): 32 | print("Uploading wheels from dist/* to PyPI...") 33 | c.run("twine upload dist/*") 34 | 35 | 36 | @task 37 | def docs(c): 38 | print("Building docs...") 39 | c.run("sphinx-build docs docs/_build") 40 | 41 | 42 | @task 43 | def format(c): 44 | c.run("isort -rc .") 45 | c.run("black .") 46 | -------------------------------------------------------------------------------- /test/test_1d.py: -------------------------------------------------------------------------------- 1 | import meshio 2 | import numpy as np 3 | import pytest 4 | 5 | import pymapping 6 | 7 | 8 | def mesh_unit_interval(N): 9 | points = np.linspace(0, 1, N) 10 | cells_line = np.array([(i, i + 1) for i in range(len(points) - 1)], dtype=int) 11 | cells = {"line": cells_line} 12 | return meshio.Mesh(points, cells) 13 | 14 | 15 | mesh_source = mesh_unit_interval(100) 16 | f = np.sin(2 * np.pi * mesh_source.points) + 0.5 * np.sin( 17 | 6 * np.pi * mesh_source.points 18 | ) 19 | mesh_source.point_data = {"f(x)": f} 20 | mesh_target = mesh_unit_interval(10) 21 | mapper = pymapping.Mapper(verbose=False) 22 | 23 | 24 | @pytest.mark.parametrize("intersection_type", ["Triangulation", "PointLocator"]) 25 | def test_P1P1(intersection_type): 26 | mapper.prepare( 27 | mesh_source, mesh_target, method="P1P1", intersection_type=intersection_type 28 | ) 29 | res = mapper.transfer("f(x)") 30 | 31 | if intersection_type == "PointLocator": 32 | x_target = res.discretization_points() 33 | y_target_from_source = np.interp(x_target, mesh_source.points, f) 34 | assert np.allclose(y_target_from_source, res.array()) 35 | 36 | 37 | def test_P1P0(): 38 | mapper.prepare( 39 | mesh_source, mesh_target, method="P1P0", intersection_type="Triangulation" 40 | ) 41 | mapper.transfer("f(x)") 42 | -------------------------------------------------------------------------------- /test/test_2d.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pygmsh 3 | import pytest 4 | 5 | import pymapping 6 | 7 | 8 | def mesh_TUB(h, recombine=False): 9 | with pygmsh.geo.Geometry() as geom: 10 | polygon = geom.add_polygon( 11 | [[1 / 3, 1 / 3, 0], [1, 0, 0], [0.5, 0.5, 0]], mesh_size=h 12 | ) 13 | if recombine: 14 | geom.set_recombined_surfaces([polygon]) 15 | mesh = geom.generate_mesh(dim=2, verbose=False) 16 | mesh.points = mesh.points[:, :2] 17 | pymapping.cleanup_mesh_meshio(mesh) 18 | return mesh 19 | 20 | 21 | mesh_source = mesh_TUB(0.01, recombine=True) 22 | f = 2 * mesh_source.points[:, 0] + mesh_source.points[:, 1] 23 | mesh_source.point_data = {"f(x)": f} 24 | mesh_source.cell_data["f(x)"] = [] 25 | for cells in mesh_source.cells: 26 | array = np.zeros(len(cells.data)) 27 | for i, cell in enumerate(cells.data): 28 | centroid = np.mean(mesh_source.points[cell], axis=0) 29 | array[i] = 2 * centroid[0] + centroid[1] 30 | mesh_source.cell_data["f(x)"].append(array) 31 | 32 | mesh_target = mesh_TUB(0.02, recombine=True) 33 | mapper = pymapping.Mapper(verbose=False) 34 | 35 | 36 | @pytest.mark.parametrize("method", ["P1P1", "P1P0", "P0P1", "P0P0"]) 37 | def test_TUB_triangle(method): 38 | mapper.prepare( 39 | mesh_source, mesh_target, method=method, intersection_type="Triangulation" 40 | ) 41 | res = mapper.transfer("f(x)") 42 | 43 | integral_source = mapper.field_source.integral(0, True) 44 | integral_target = res.field_target.integral(0, True) 45 | assert np.isclose(integral_source, integral_target, rtol=1e-4) 46 | 47 | 48 | def test_cli(): 49 | import tempfile 50 | import meshio 51 | 52 | mesh_source_file = tempfile.NamedTemporaryFile(suffix=".vtu").name 53 | mesh_target_file = tempfile.NamedTemporaryFile(suffix=".vtu").name 54 | meshio.write(mesh_source_file, mesh_source) 55 | meshio.write(mesh_target_file, mesh_target) 56 | 57 | outfile = tempfile.NamedTemporaryFile(suffix=".npy").name 58 | pymapping.cli.main( 59 | [ 60 | mesh_source_file, 61 | mesh_target_file, 62 | "f(x)", 63 | outfile, 64 | "--method", 65 | "P1P0", 66 | "--intersection_type", 67 | "Triangulation", 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | pygmsh 3 | pytest 4 | pytest-cov 5 | codecov 6 | --------------------------------------------------------------------------------