├── .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 | [](https://travis-ci.org/tianyikillua/pymapping)
4 | [](https://codecov.io/gh/tianyikillua/pymapping)
5 | [](https://readthedocs.org/projects/pymapping/?badge=latest)
6 | [](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 |
--------------------------------------------------------------------------------