├── discretize ├── _extensions │ ├── __init__.py │ ├── interputils_cython.pxd │ ├── geom.pxd │ ├── triplet.h │ ├── meson.build │ ├── geom.h │ ├── tree.pxd │ ├── tree.h │ ├── interputils_cython.pyx │ ├── simplex_helpers.pyx │ └── geom.cpp ├── Tests │ ├── meson.build │ └── __init__.py ├── operators │ ├── meson.build │ └── __init__.py ├── base │ ├── meson.build │ └── __init__.py ├── mixins │ ├── meson.build │ ├── __init__.py │ ├── omf_mod.py │ └── mesh_io.py ├── utils │ ├── codeutils.py │ ├── matutils.py │ ├── meshutils.py │ ├── coordutils.py │ ├── curvutils.py │ ├── interputils.py │ ├── meson.build │ ├── io_utils.py │ ├── __init__.py │ ├── coordinate_utils.py │ ├── interpolation_utils.py │ ├── code_utils.py │ └── curvilinear_utils.py ├── View.py ├── meson.build ├── __init__.py └── tensor_cell.py ├── meson.options ├── CITATION.rst ├── LICENSE ├── AUTHORS.rst ├── meson.build ├── README.rst └── pyproject.toml /discretize/_extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /meson.options: -------------------------------------------------------------------------------- 1 | option('cy_line_trace', type : 'boolean', value : false) -------------------------------------------------------------------------------- /discretize/Tests/meson.build: -------------------------------------------------------------------------------- 1 | 2 | python_sources = [ 3 | '__init__.py', 4 | ] 5 | 6 | py.install_sources( 7 | python_sources, 8 | subdir: 'discretize/Tests' 9 | ) 10 | -------------------------------------------------------------------------------- /discretize/_extensions/interputils_cython.pxd: -------------------------------------------------------------------------------- 1 | cimport numpy as np 2 | cdef np.int64_t _bisect_left(np.float64_t[:] a, np.float64_t x) nogil 3 | cdef np.int64_t _bisect_right(np.float64_t[:] a, np.float64_t x) nogil 4 | -------------------------------------------------------------------------------- /discretize/operators/meson.build: -------------------------------------------------------------------------------- 1 | 2 | python_sources = [ 3 | '__init__.py', 4 | 'differential_operators.py', 5 | 'inner_products.py', 6 | ] 7 | 8 | py.install_sources( 9 | python_sources, 10 | subdir: 'discretize/operators' 11 | ) 12 | -------------------------------------------------------------------------------- /discretize/base/meson.build: -------------------------------------------------------------------------------- 1 | 2 | python_sources = [ 3 | '__init__.py', 4 | 'base_mesh.py', 5 | 'base_regular_mesh.py', 6 | 'base_tensor_mesh.py', 7 | ] 8 | 9 | py.install_sources( 10 | python_sources, 11 | subdir: 'discretize/base' 12 | ) 13 | -------------------------------------------------------------------------------- /discretize/mixins/meson.build: -------------------------------------------------------------------------------- 1 | 2 | python_sources = [ 3 | '__init__.py', 4 | 'mesh_io.py', 5 | 'mpl_mod.py', 6 | 'omf_mod.py', 7 | 'vtk_mod.py', 8 | ] 9 | 10 | py.install_sources( 11 | python_sources, 12 | subdir: 'discretize/mixins' 13 | ) 14 | -------------------------------------------------------------------------------- /discretize/utils/codeutils.py: -------------------------------------------------------------------------------- 1 | from discretize.utils.code_utils import * # NOQA F401,F403 2 | 3 | raise ImportError( 4 | "Importing from discretize.codeutils is deprecated behavoir. Please import " 5 | "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", 6 | ) 7 | -------------------------------------------------------------------------------- /discretize/utils/matutils.py: -------------------------------------------------------------------------------- 1 | from discretize.utils.matrix_utils import * # NOQA F401,F403 2 | 3 | raise ImportError( 4 | "Importing from discretize.matutils is deprecated behavoir. Please import " 5 | "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", 6 | ) 7 | -------------------------------------------------------------------------------- /discretize/utils/meshutils.py: -------------------------------------------------------------------------------- 1 | from discretize.utils.mesh_utils import * # NOQA F401,F403 2 | 3 | raise ImportError( 4 | "Importing from discretize.meshutils is deprecated behavoir. Please import " 5 | "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", 6 | ) 7 | -------------------------------------------------------------------------------- /discretize/utils/coordutils.py: -------------------------------------------------------------------------------- 1 | from discretize.utils.coordinate_utils import * # NOQA F401,F403 2 | 3 | raise ImportError( 4 | "Importing from discretize.coordutils is deprecated behavoir. Please import " 5 | "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", 6 | ) 7 | -------------------------------------------------------------------------------- /discretize/utils/curvutils.py: -------------------------------------------------------------------------------- 1 | from discretize.utils.curvilinear_utils import * # NOQA F401,F403 2 | 3 | raise ImportError( 4 | "Importing from discretize.curvutils is deprecated behavoir. Please import " 5 | "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", 6 | ) 7 | -------------------------------------------------------------------------------- /discretize/utils/interputils.py: -------------------------------------------------------------------------------- 1 | from discretize.utils.interpolation_utils import * # NOQA F401,F403 2 | 3 | raise ImportError( 4 | "Importing from discretize.interputils is deprecated behavoir. Please import " 5 | "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", 6 | ) 7 | -------------------------------------------------------------------------------- /discretize/View.py: -------------------------------------------------------------------------------- 1 | """Deprecated view module.""" 2 | 3 | from discretize.utils.code_utils import deprecate_module 4 | 5 | deprecate_module( 6 | "discretize.View", 7 | "discretize.mixins.mpl_mod", 8 | removal_version="1.0.0", 9 | error=True, 10 | ) 11 | try: 12 | from discretize.mixins.mpl_mod import Slicer # NOQA F401 13 | except ImportError: 14 | pass 15 | -------------------------------------------------------------------------------- /discretize/Tests/__init__.py: -------------------------------------------------------------------------------- 1 | from discretize.tests import * # NOQA F401,F403 2 | from discretize.utils.code_utils import deprecate_module 3 | 4 | # note this needs to be a module with an __init__ so we can avoid name clash 5 | # with tests.py in the discretize directory on systems that are agnostic to Case. 6 | deprecate_module( 7 | "discretize.Tests", "discretize.tests", removal_version="1.0.0", error=True 8 | ) 9 | -------------------------------------------------------------------------------- /discretize/utils/meson.build: -------------------------------------------------------------------------------- 1 | 2 | python_sources = [ 3 | '__init__.py', 4 | 'code_utils.py', 5 | 'coordinate_utils.py', 6 | 'curvilinear_utils.py', 7 | 'interpolation_utils.py', 8 | 'io_utils.py', 9 | 'matrix_utils.py', 10 | 'mesh_utils.py', 11 | 'codeutils.py', 12 | 'coordutils.py', 13 | 'curvutils.py', 14 | 'interputils.py', 15 | 'matutils.py', 16 | 'meshutils.py', 17 | ] 18 | 19 | py.install_sources( 20 | python_sources, 21 | subdir: 'discretize/utils' 22 | ) 23 | -------------------------------------------------------------------------------- /discretize/meson.build: -------------------------------------------------------------------------------- 1 | 2 | python_sources = [ 3 | '__init__.py', 4 | 'curvilinear_mesh.py', 5 | 'cylindrical_mesh.py', 6 | 'tensor_cell.py', 7 | 'tensor_mesh.py', 8 | 'tests.py', 9 | 'tree_mesh.py', 10 | 'unstructured_mesh.py', 11 | 'View.py', 12 | ] 13 | 14 | py.install_sources( 15 | python_sources, 16 | subdir: 'discretize' 17 | ) 18 | 19 | subdir('base') 20 | subdir('_extensions') 21 | subdir('mixins') 22 | subdir('operators') 23 | subdir('utils') 24 | subdir('Tests') 25 | -------------------------------------------------------------------------------- /discretize/base/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================== 3 | Base Mesh (:mod:`discretize.base`) 4 | ================================== 5 | .. currentmodule:: discretize.base 6 | 7 | The ``base`` sub-package houses the fundamental classes for all meshes in ``discretize``. 8 | 9 | Base Mesh Class 10 | --------------- 11 | .. autosummary:: 12 | :toctree: generated/ 13 | 14 | BaseMesh 15 | BaseRegularMesh 16 | BaseRectangularMesh 17 | BaseTensorMesh 18 | """ 19 | 20 | from discretize.base.base_mesh import BaseMesh 21 | from discretize.base.base_regular_mesh import BaseRegularMesh, BaseRectangularMesh 22 | from discretize.base.base_tensor_mesh import BaseTensorMesh 23 | -------------------------------------------------------------------------------- /discretize/operators/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================================ 3 | Discrete Operators (:mod:`discretize.operators`) 4 | ================================================ 5 | .. currentmodule:: discretize.operators 6 | 7 | The ``operators`` package contains the classes discretize meshes with regular structure 8 | use to construct discrete versions of the differential operators. 9 | 10 | Operator Classes 11 | ---------------- 12 | .. autosummary:: 13 | :toctree: generated/ 14 | 15 | DiffOperators 16 | InnerProducts 17 | """ 18 | 19 | from discretize.operators.differential_operators import DiffOperators 20 | from discretize.operators.inner_products import InnerProducts 21 | -------------------------------------------------------------------------------- /CITATION.rst: -------------------------------------------------------------------------------- 1 | Citing discretize 2 | ----------------- 3 | 4 | There is a `paper about discretize `_ as it is used in SimPEG, 5 | if you use this code, please help our scientific visibility by citing our work! 6 | 7 | 8 | Cockett, R., Kang, S., Heagy, L. J., Pidlisecky, A., & Oldenburg, D. W. (2015). SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications. Computers & Geosciences. 9 | 10 | 11 | BibTex: 12 | 13 | .. code:: Latex 14 | 15 | @article{Cockett2015, 16 | title={SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications}, 17 | author={Cockett, Rowan and Kang, Seogi and Heagy, Lindsey J and Pidlisecky, Adam and Oldenburg, Douglas W}, 18 | journal={Computers \& Geosciences}, 19 | year={2015}, 20 | publisher={Elsevier} 21 | } 22 | 23 | -------------------------------------------------------------------------------- /discretize/_extensions/geom.pxd: -------------------------------------------------------------------------------- 1 | from libcpp cimport bool 2 | 3 | cdef extern from "geom.h": 4 | ctypedef int int_t 5 | cdef cppclass Ball: 6 | Ball() except + 7 | Ball(int_t dim, double * x0, double r) except + 8 | 9 | cdef cppclass Line: 10 | Line() except + 11 | Line(int_t dim, double * x0, double *x1) except + 12 | 13 | cdef cppclass Box: 14 | Box() except + 15 | Box(int_t dim, double * x0, double *x1) except + 16 | 17 | cdef cppclass Plane: 18 | Plane() except + 19 | Plane(int_t dim, double * origin, double *normal) except + 20 | 21 | cdef cppclass Triangle: 22 | Triangle() except + 23 | Triangle(int_t dim, double * x0, double *x1, double *x2) except + 24 | 25 | cdef cppclass VerticalTriangularPrism: 26 | VerticalTriangularPrism() except + 27 | VerticalTriangularPrism(int_t dim, double * x0, double *x1, double *x2, double h) except + 28 | 29 | cdef cppclass Tetrahedron: 30 | Tetrahedron() except + 31 | Tetrahedron(int_t dim, double * x0, double *x1, double *x2, double *x3) except + -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2025 SimPEG Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | - Rowan Cockett, (`@rowanc1 `_) 2 | - Lindsey Heagy, (`@lheagy `_) 3 | - Seogi Kang, (`@sgkang `_) 4 | - Brendan Smithyman, (`@bsmithyman `_) 5 | - Gudni Rosenkjaer, (`@grosenkj `_) 6 | - Dom Fournier, (`@fourndo `_) 7 | - Dave Marchant, (`@dwfmarchant `_) 8 | - Lars Ruthotto, (`@lruthotto `_) 9 | - Mike Wathen, (`@wathenmp `_) 10 | - Luz Angelica Caudillo-Mata, (`@lacmajedrez `_) 11 | - Eldad Haber, (`@eldadhaber `_) 12 | - Doug Oldenburg, (`@dougoldenburg `_) 13 | - Devin Cowan, (`@dccowan `_) 14 | - Adam Pidlisecky, (`@aPid1 `_) 15 | - Dieter Werthmüller, (`@prisae `_) 16 | - Bane Sullivan, (`@banesullivan `_) 17 | - Joseph Capriotti, (`@jcapriot `_) 18 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'discretize', 3 | 'c', 'cpp', 'cython', 4 | # Note that the git commit hash cannot be added dynamically here 5 | # (it is dynamically generated though setuptools_scm) 6 | version: run_command('python', 7 | [ 8 | '-c', 9 | ''' 10 | from setuptools_scm import get_version 11 | print(get_version())''' 12 | ], 13 | check: true 14 | ).stdout().strip(), 15 | 16 | license: 'MIT', 17 | meson_version: '>= 1.4.0', 18 | default_options: [ 19 | 'buildtype=debugoptimized', 20 | 'b_ndebug=if-release', 21 | 'cpp_std=c++17', 22 | ], 23 | ) 24 | 25 | # https://mesonbuild.com/Python-module.html 26 | py_mod = import('python') 27 | py = py_mod.find_installation(pure: false) 28 | py_dep = py.dependency() 29 | 30 | cc = meson.get_compiler('c') 31 | cpp = meson.get_compiler('cpp') 32 | cy = meson.get_compiler('cython') 33 | # generator() doesn't accept compilers, only found programs - cast it. 34 | cython = find_program(cy.cmd_array()[0]) 35 | 36 | _global_c_args = cc.get_supported_arguments( 37 | '-Wno-unused-but-set-variable', 38 | '-Wno-unused-function', 39 | '-Wno-conversion', 40 | '-Wno-misleading-indentation', 41 | ) 42 | add_project_arguments(_global_c_args, language : 'c') 43 | 44 | # We need -lm for all C code (assuming it uses math functions, which is safe to 45 | # assume for SciPy). For C++ it isn't needed, because libstdc++/libc++ is 46 | # guaranteed to depend on it. 47 | m_dep = cc.find_library('m', required : false) 48 | if m_dep.found() 49 | add_project_link_arguments('-lm', language : 'c') 50 | endif 51 | 52 | subdir('discretize') -------------------------------------------------------------------------------- /discretize/_extensions/triplet.h: -------------------------------------------------------------------------------- 1 | #ifndef __TRIPLET_H 2 | #define __TRIPLET_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | template 9 | struct triplet{ 10 | T v1{}; 11 | U v2{}; 12 | V v3{}; 13 | 14 | triplet(){} 15 | 16 | triplet(T first, U second, V third){ 17 | v1 = first; 18 | v2 = second; 19 | v3 = third; 20 | } 21 | bool operator==(const triplet &other) const{ 22 | return (v1 == other.v1 23 | && v2 == other.v2 24 | && v3 == other.v3); 25 | } 26 | bool operator<(const triplet &other) const{ 27 | return ! ( 28 | (v1 > other.v1) 29 | || (v2 > other.v2) 30 | || (v3 > other.v3) 31 | ); 32 | } 33 | }; 34 | 35 | namespace std { 36 | 37 | template 38 | struct hash > 39 | { 40 | std::size_t operator()(const triplet& k) const 41 | { 42 | using std::hash; 43 | // Compute individual hash values for first, 44 | // second and third and combine them using XOR 45 | // and bit shifting: 46 | 47 | return ((hash()(k.v1) 48 | ^ (hash()(k.v2) << 1)) >> 1) 49 | ^ (hash()(k.v3) << 1); 50 | } 51 | }; 52 | 53 | template 54 | struct hash > 55 | { 56 | size_t operator()(const pair& k) const 57 | { 58 | using std::hash; 59 | 60 | return ((hash()(k.first) 61 | ^ (hash()(k.second) << 1)) >> 1); 62 | } 63 | }; 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /discretize/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================== 3 | Mixins (:mod:`discretize.mixins`) 4 | ================================== 5 | .. currentmodule:: discretize.mixins 6 | 7 | The ``mixins`` module provides a set of tools for interfacing ``discretize`` 8 | with external libraries such as VTK, OMF, and matplotlib. These modules are only 9 | imported if those external packages are available in the active Python environment and 10 | provide extra functionality that different finite volume meshes can inherit. 11 | 12 | Mixin Classes 13 | ------------- 14 | .. autosummary:: 15 | :toctree: generated/ 16 | 17 | TensorMeshIO 18 | TreeMeshIO 19 | InterfaceMPL 20 | InterfaceVTK 21 | InterfaceOMF 22 | 23 | Other Optional Classes 24 | ---------------------- 25 | .. autosummary:: 26 | :toctree: generated/ 27 | 28 | Slicer 29 | """ 30 | 31 | import importlib.util 32 | from .mesh_io import TensorMeshIO, TreeMeshIO, SimplexMeshIO 33 | 34 | AVAILABLE_MIXIN_CLASSES = [] 35 | SIMPLEX_MIXIN_CLASSES = [] 36 | 37 | if importlib.util.find_spec("vtk"): 38 | from .vtk_mod import InterfaceVTK 39 | 40 | AVAILABLE_MIXIN_CLASSES.append(InterfaceVTK) 41 | 42 | if importlib.util.find_spec("omf"): 43 | from .omf_mod import InterfaceOMF 44 | 45 | AVAILABLE_MIXIN_CLASSES.append(InterfaceOMF) 46 | 47 | # keep this one last in defaults in case anything else wants to overwrite 48 | # plot commands 49 | if importlib.util.find_spec("matplotlib"): 50 | from .mpl_mod import Slicer, InterfaceMPL 51 | 52 | AVAILABLE_MIXIN_CLASSES.append(InterfaceMPL) 53 | 54 | 55 | # # Python 3 friendly 56 | class InterfaceMixins(*AVAILABLE_MIXIN_CLASSES): 57 | """Class to handle all the avaialble mixins that can be inherrited 58 | directly onto ``discretize.base.BaseMesh`` 59 | """ 60 | 61 | pass 62 | -------------------------------------------------------------------------------- /discretize/_extensions/meson.build: -------------------------------------------------------------------------------- 1 | # NumPy include directory 2 | numpy_nodepr_api = ['-DNPY_NO_DEPRECATED_API=NPY_1_22_API_VERSION'] 3 | np_dep = dependency('numpy') 4 | 5 | # Deal with M_PI & friends; add `use_math_defines` to c_args or cpp_args 6 | # Cython doesn't always get this right itself (see, e.g., gh-16800), so 7 | # explicitly add the define as a compiler flag for Cython-generated code. 8 | is_windows = host_machine.system() == 'windows' 9 | if is_windows 10 | use_math_defines = ['-D_USE_MATH_DEFINES'] 11 | else 12 | use_math_defines = [] 13 | endif 14 | 15 | c_undefined_ok = ['-Wno-maybe-uninitialized'] 16 | 17 | cython_c_args = [numpy_nodepr_api, use_math_defines] 18 | 19 | cy_line_trace = get_option('cy_line_trace') 20 | if cy_line_trace 21 | cython_c_args += ['-DCYTHON_TRACE_NOGIL=1'] 22 | endif 23 | 24 | cython_args = [] 25 | if cy.version().version_compare('>=3.1.0') 26 | cython_args += ['-Xfreethreading_compatible=True'] 27 | endif 28 | 29 | cython_cpp_args = cython_c_args 30 | 31 | module_path = 'discretize/_extensions' 32 | 33 | py.extension_module( 34 | 'interputils_cython', 35 | 'interputils_cython.pyx', 36 | cython_args: cython_args, 37 | c_args: cython_c_args, 38 | install: true, 39 | subdir: module_path, 40 | dependencies : [py_dep, np_dep], 41 | ) 42 | 43 | py.extension_module( 44 | 'tree_ext', 45 | ['tree_ext.pyx' , 'tree.cpp', 'geom.cpp'], 46 | cython_args: cython_args, 47 | cpp_args: cython_cpp_args, 48 | install: true, 49 | subdir: module_path, 50 | dependencies : [py_dep, np_dep], 51 | override_options : ['cython_language=cpp'], 52 | ) 53 | 54 | py.extension_module( 55 | 'simplex_helpers', 56 | 'simplex_helpers.pyx', 57 | cython_args: cython_args, 58 | cpp_args: cython_cpp_args, 59 | install: true, 60 | subdir: module_path, 61 | dependencies : [py_dep, np_dep], 62 | override_options : ['cython_language=cpp'], 63 | ) 64 | 65 | python_sources = [ 66 | '__init__.py', 67 | ] 68 | 69 | py.install_sources( 70 | python_sources, 71 | subdir: module_path 72 | ) -------------------------------------------------------------------------------- /discretize/_extensions/geom.h: -------------------------------------------------------------------------------- 1 | #ifndef __GEOM_H 2 | #define __GEOM_H 3 | // simple geometric objects for intersection tests with an aabb 4 | 5 | typedef std::size_t int_t; 6 | 7 | class Geometric{ 8 | public: 9 | int_t dim; 10 | 11 | Geometric(); 12 | Geometric(int_t dim); 13 | virtual bool intersects_cell(double *a, double *b) const = 0; 14 | }; 15 | 16 | class Ball : public Geometric{ 17 | public: 18 | double *x0; 19 | double r; 20 | double rsq; 21 | 22 | Ball(); 23 | Ball(int_t dim, double* x0, double r); 24 | virtual bool intersects_cell(double *a, double *b) const; 25 | }; 26 | 27 | class Line : public Geometric{ 28 | public: 29 | double *x0; 30 | double *x1; 31 | double inv_dx[3]; 32 | 33 | Line(); 34 | Line(int_t dim, double* x0, double *x1); 35 | virtual bool intersects_cell(double *a, double *b) const; 36 | }; 37 | 38 | class Box : public Geometric{ 39 | public: 40 | double *x0; 41 | double *x1; 42 | 43 | Box(); 44 | Box(int_t dim, double* x0, double *x1); 45 | virtual bool intersects_cell(double *a, double *b) const; 46 | }; 47 | 48 | class Plane : public Geometric{ 49 | public: 50 | double *origin; 51 | double *normal; 52 | 53 | Plane(); 54 | Plane(int_t dim, double* origin, double *normal); 55 | virtual bool intersects_cell(double *a, double *b) const; 56 | }; 57 | 58 | class Triangle : public Geometric{ 59 | public: 60 | double *x0; 61 | double *x1; 62 | double *x2; 63 | double e0[3]; 64 | double e1[3]; 65 | double e2[3]; 66 | double normal[3]; 67 | 68 | Triangle(); 69 | Triangle(int_t dim, double* x0, double *x1, double *x2); 70 | virtual bool intersects_cell(double *a, double *b) const; 71 | }; 72 | 73 | class VerticalTriangularPrism : public Triangle{ 74 | public: 75 | double h; 76 | 77 | VerticalTriangularPrism(); 78 | VerticalTriangularPrism(int_t dim, double* x0, double *x1, double *x2, double h); 79 | virtual bool intersects_cell(double *a, double *b) const; 80 | }; 81 | 82 | class Tetrahedron : public Geometric{ 83 | public: 84 | double *x0; 85 | double *x1; 86 | double *x2; 87 | double *x3; 88 | double edge_tans[6][3]; 89 | double face_normals[4][3]; 90 | 91 | Tetrahedron(); 92 | Tetrahedron(int_t dim, double* x0, double *x1, double *x2, double *x3); 93 | virtual bool intersects_cell(double *a, double *b) const; 94 | }; 95 | 96 | #endif -------------------------------------------------------------------------------- /discretize/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ===================================== 3 | Discretize Meshes (:mod:`discretize`) 4 | ===================================== 5 | .. currentmodule:: discretize 6 | 7 | The ``discretize`` package contains four types of meshes for soliving partial differential 8 | equations using the finite volume method. 9 | 10 | Mesh Classes 11 | ============ 12 | .. autosummary:: 13 | :toctree: generated/ 14 | 15 | TensorMesh 16 | CylindricalMesh 17 | CurvilinearMesh 18 | TreeMesh 19 | SimplexMesh 20 | 21 | Mesh Cells 22 | ========== 23 | The :class:`~discretize.tensor_cell.TensorCell` and 24 | :class:`~discretize.tree_mesh.TreeCell` classes were designed specifically to 25 | define the cells within tensor and tree meshes, respectively. 26 | Instances of :class:`~discretize.tree_mesh.TreeCell` and 27 | :class:`~discretize.tensor_cell.TensorCell` are not meant to be created on 28 | their own. 29 | However, they can be returned directly by indexing a particular cell within 30 | a tensor or tree mesh. 31 | 32 | .. autosummary:: 33 | :toctree: generated/ 34 | 35 | tensor_cell.TensorCell 36 | tree_mesh.TreeCell 37 | """ 38 | 39 | from discretize.tensor_mesh import TensorMesh 40 | from discretize.cylindrical_mesh import CylMesh, CylindricalMesh 41 | from discretize.curvilinear_mesh import CurvilinearMesh 42 | from discretize.unstructured_mesh import SimplexMesh 43 | from discretize.utils.io_utils import load_mesh 44 | from .tensor_cell import TensorCell 45 | 46 | try: 47 | from discretize.tree_mesh import TreeMesh 48 | except ImportError as err: 49 | import os 50 | 51 | # Check if being called from non-standard location (i.e. a git repository) 52 | # is tree_ext.pyx here? will not be in the folder if installed to site-packages... 53 | file_test = os.path.dirname(os.path.abspath(__file__)) + "/_extensions/tree_ext.pyx" 54 | if os.path.isfile(file_test): 55 | # Then we are being run from a repository 56 | raise ImportError( 57 | """ 58 | It would appear that discretize is being imported from its source code 59 | directory and is unable to load its compiled extension modules. Try changing 60 | your directory and re-launching your python interpreter. 61 | 62 | If this was intentional, you need to install discretize in an editable mode. 63 | """ 64 | ) 65 | else: 66 | raise err 67 | from discretize import tests 68 | 69 | __author__ = "SimPEG Team" 70 | __license__ = "MIT" 71 | __copyright__ = "2013 - 2023, SimPEG Developers, https://simpeg.xyz" 72 | 73 | from importlib.metadata import version, PackageNotFoundError 74 | 75 | # Version 76 | try: 77 | # - Released versions just tags: 0.8.0 78 | # - GitHub commits add .dev#+hash: 0.8.1.dev4+g2785721 79 | # - Uncommitted changes add timestamp: 0.8.1.dev4+g2785721.d20191022 80 | __version__ = version("discretize") 81 | except PackageNotFoundError: 82 | # If it was not installed, then we don't know the version. We could throw a 83 | # warning here, but this case *should* be rare. discretize should be 84 | # installed properly! 85 | from datetime import datetime 86 | 87 | __version__ = "unknown-" + datetime.today().strftime("%Y%m%d") 88 | -------------------------------------------------------------------------------- /discretize/_extensions/tree.pxd: -------------------------------------------------------------------------------- 1 | from libcpp cimport bool 2 | from libcpp.vector cimport vector 3 | from libcpp.map cimport map 4 | 5 | cdef extern from "tree.h": 6 | ctypedef int int_t 7 | 8 | cdef cppclass Node: 9 | int_t location_ind[3] 10 | double location[3] 11 | int_t key 12 | int_t reference 13 | int_t index 14 | bool hanging 15 | Node *parents[4] 16 | Node() 17 | Node(int_t, int_t, int_t, double, double, double) 18 | double operator[](int_t) 19 | 20 | cdef cppclass Edge: 21 | int_t location_ind[3] 22 | double location[3] 23 | int_t key 24 | int_t reference 25 | int_t index 26 | double length 27 | bool hanging 28 | Node *points[2] 29 | Edge *parents[2] 30 | Edge() 31 | Edge(Node& p1, Node& p2) 32 | double operator[](int_t) 33 | 34 | cdef cppclass Face: 35 | int_t location_ind[3] 36 | double location[3] 37 | int_t key 38 | int_t reference 39 | int_t index 40 | double area 41 | bool hanging 42 | Node *points[4] 43 | Edge *edges[4] 44 | Face *parent 45 | Face() 46 | Face(Node& p1, Node& p2, Node& p3, Node& p4) 47 | double operator[](int_t) 48 | 49 | ctypedef map[int_t, Node *] node_map_t 50 | ctypedef map[int_t, Edge *] edge_map_t 51 | ctypedef map[int_t, Face *] face_map_t 52 | 53 | cdef cppclass Cell: 54 | int_t n_dim 55 | Cell *parent 56 | Cell *children[8] 57 | Cell *neighbors[6] 58 | Node *points[8] 59 | Edge *edges[12] 60 | Face *faces[6] 61 | int_t location_ind[3] 62 | double location[3] 63 | int_t key, level, max_level 64 | long long int index 65 | double volume 66 | inline bool is_leaf() 67 | inline Node* min_node() 68 | inline Node* max_node() 69 | double operator[](int_t) 70 | 71 | ctypedef int (*eval_func_ptr)(void*, Cell*) 72 | cdef cppclass PyWrapper: 73 | PyWrapper() 74 | void set(void*, eval_func_ptr eval) 75 | 76 | cdef cppclass Tree: 77 | int_t n_dim 78 | int_t max_level, nx, ny, nz 79 | 80 | vector[Cell *] cells 81 | node_map_t nodes 82 | edge_map_t edges_x, edges_y, edges_z 83 | face_map_t faces_x, faces_y, faces_z 84 | vector[Node *] hanging_nodes 85 | vector[Edge *] hanging_edges_x, hanging_edges_y, hanging_edges_z 86 | vector[Face *] hanging_faces_x, hanging_faces_y, hanging_faces_z 87 | 88 | Tree() 89 | 90 | void set_dimension(int_t) 91 | void set_levels(int_t, int_t, int_t) 92 | void set_xs(double*, double*, double*) 93 | void refine_function(PyWrapper *, bool) 94 | 95 | void refine_geom[T](const T&, int_t, bool) 96 | 97 | void refine_image(double*, bool) 98 | 99 | void number() 100 | void initialize_roots() 101 | void insert_cell(double *new_center, int_t p_level, bool) 102 | void finalize_lists() 103 | Cell * containing_cell(double, double, double) 104 | vector[int_t] find_cells_geom[T](const T& geom) 105 | void shift_cell_centers(double*) 106 | -------------------------------------------------------------------------------- /discretize/utils/io_utils.py: -------------------------------------------------------------------------------- 1 | """Simple input/output routines.""" 2 | 3 | from urllib.request import urlretrieve 4 | import os 5 | import importlib 6 | import json 7 | 8 | 9 | def load_mesh(file_name): 10 | """Load discretize mesh saved to json file. 11 | 12 | For a discretize mesh that has been converted to dictionary and 13 | written to a json file, the function **load_mesh** loads the 14 | json file and reconstructs the mesh object. 15 | 16 | Parameters 17 | ---------- 18 | file_name : str 19 | Name of the json file being read in. Contains all information required to 20 | reconstruct the mesh. 21 | 22 | Returns 23 | ------- 24 | discretize.base.BaseMesh 25 | A discretize mesh defined by the class and parameters stored in the json file 26 | """ 27 | with open(file_name, "r") as outfile: 28 | jsondict = json.load(outfile) 29 | module_name = jsondict.pop( 30 | "__module__", "discretize" 31 | ) # default to loading from discretize 32 | class_name = jsondict.pop("__class__") 33 | mod = importlib.import_module(module_name) 34 | cls = getattr(mod, class_name) 35 | if "_n" in jsondict: 36 | jsondict["shape_cells"] = jsondict.pop( 37 | "_n" 38 | ) # need to catch this old _n property here 39 | data = cls(**jsondict) 40 | return data 41 | 42 | 43 | def download(url, folder=".", overwrite=False, verbose=True): 44 | """ 45 | Download file(s) stored in a cloud directory. 46 | 47 | Parameters 48 | ---------- 49 | url : str or list of str 50 | url or list of urls for the file(s) being downloaded 51 | folder : str, optional 52 | Local folder where downloaded files are to be stored 53 | overwrite : bool, optional 54 | Overwrite files if they have the same name as newly downloaded files 55 | verbose : bool, optional 56 | Print progress when downloading multiple files 57 | 58 | Returns 59 | ------- 60 | os.path or list of os.path 61 | The path or a list of paths for all downloaded files 62 | """ 63 | 64 | def rename_path(downloadpath): 65 | splitfullpath = downloadpath.split(os.path.sep) 66 | 67 | # grab just the file name 68 | fname = splitfullpath[-1] 69 | fnamesplit = fname.split(".") 70 | newname = fnamesplit[0] 71 | 72 | # check if we have already re-numbered 73 | newnamesplit = newname.split("(") 74 | 75 | # add (num) to the end of the file name 76 | if len(newnamesplit) == 1: 77 | num = 1 78 | else: 79 | num = int(newnamesplit[-1][:-1]) 80 | num += 1 81 | 82 | newname = "{}({}).{}".format(newnamesplit[0], num, fnamesplit[-1]) 83 | return os.path.sep.join(splitfullpath[:-1] + newnamesplit[:-1] + [newname]) 84 | 85 | # ensure we are working with absolute paths and home directories dealt with 86 | folder = os.path.abspath(os.path.expanduser(folder)) 87 | 88 | # make the directory if it doesn't currently exist 89 | if not os.path.exists(folder): 90 | os.makedirs(folder) 91 | 92 | if isinstance(url, str): 93 | file_names = [url.split("/")[-1]] 94 | elif isinstance(url, list): 95 | file_names = [u.split("/")[-1] for u in url] 96 | 97 | downloadpath = [os.path.sep.join([folder, f]) for f in file_names] 98 | 99 | # check if the directory already exists 100 | for i, download in enumerate(downloadpath): 101 | if os.path.exists(download): 102 | if overwrite: 103 | if verbose: 104 | print("overwriting {}".format(download)) 105 | else: 106 | while os.path.exists(download): 107 | download = rename_path(download) 108 | 109 | if verbose: 110 | print("file already exists, new file is called {}".format(download)) 111 | downloadpath[i] = download 112 | 113 | # download files 114 | urllist = url if isinstance(url, list) else [url] 115 | for u, f in zip(urllist, downloadpath): 116 | print("Downloading {}".format(u)) 117 | urlretrieve(u, f) 118 | print(" saved to: " + f) 119 | 120 | print("Download completed!") 121 | return downloadpath if isinstance(url, list) else downloadpath[0] 122 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://raw.github.com/simpeg/discretize/main/docs/images/discretize-logo.png 2 | :alt: Discretize Logo 3 | 4 | discretize 5 | ========== 6 | 7 | .. image:: https://img.shields.io/pypi/v/discretize.svg 8 | :target: https://pypi.python.org/pypi/discretize 9 | :alt: Latest PyPI version 10 | 11 | .. image:: https://anaconda.org/conda-forge/discretize/badges/version.svg 12 | :target: https://anaconda.org/conda-forge/discretize 13 | :alt: Latest conda-forge version 14 | 15 | .. image:: https://img.shields.io/github/license/simpeg/simpeg.svg 16 | :target: https://github.com/simpeg/discretize/blob/main/LICENSE 17 | :alt: MIT license 18 | 19 | .. image:: https://dev.azure.com/simpeg/discretize/_apis/build/status/simpeg.discretize?branchName=main 20 | :target: https://dev.azure.com/simpeg/discretize/_build/latest?definitionId=1&branchName=main 21 | :alt: Azure pipelines build status 22 | 23 | .. image:: https://codecov.io/gh/simpeg/discretize/branch/main/graph/badge.svg 24 | :target: https://codecov.io/gh/simpeg/discretize 25 | :alt: Coverage status 26 | 27 | .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.596411.svg 28 | :target: https://doi.org/10.5281/zenodo.596411 29 | 30 | .. image:: https://img.shields.io/discourse/users?server=http%3A%2F%2Fsimpeg.discourse.group%2F 31 | :target: http://simpeg.discourse.group/ 32 | 33 | .. image:: https://img.shields.io/badge/simpeg-purple?logo=mattermost&label=Mattermost 34 | :target: https://mattermost.softwareunderground.org/simpeg 35 | 36 | .. image:: https://img.shields.io/badge/Youtube%20channel-GeoSci.xyz-FF0000.svg?logo=youtube 37 | :target: https://www.youtube.com/channel/UCBrC4M8_S4GXhyHht7FyQqw 38 | 39 | 40 | **discretize** - A python package for finite volume discretization. 41 | 42 | The vision is to create a package for finite volume simulation with a 43 | focus on large scale inverse problems. 44 | This package has the following features: 45 | 46 | * modular with respect to the spacial discretization 47 | * built with the inverse problem in mind 48 | * supports 1D, 2D and 3D problems 49 | * access to sparse matrix operators 50 | * access to derivatives to mesh variables 51 | 52 | .. image:: https://raw.githubusercontent.com/simpeg/figures/master/finitevolume/cell-anatomy-tensor.png 53 | 54 | Currently, discretize supports: 55 | 56 | * Tensor Meshes (1D, 2D and 3D) 57 | * Cylindrically Symmetric Meshes 58 | * QuadTree and OcTree Meshes (2D and 3D) 59 | * Logically Rectangular Meshes (2D and 3D) 60 | * Triangular (2D) and Tetrahedral (3D) Meshes 61 | 62 | Installing 63 | ^^^^^^^^^^ 64 | **discretize** is on conda-forge, and is the recommended installation method. 65 | 66 | .. code:: shell 67 | 68 | conda install -c conda-forge discretize 69 | 70 | Prebuilt wheels of **discretize** are on pypi for most platforms 71 | 72 | .. code:: shell 73 | 74 | pip install discretize 75 | 76 | To install from source, note this requires a `c++` compiler supporting the `c++17` standard. 77 | 78 | .. code:: shell 79 | 80 | git clone https://github.com/simpeg/discretize.git 81 | cd discretize 82 | pip install . 83 | 84 | Citing discretize 85 | ^^^^^^^^^^^^^^^^^ 86 | 87 | Please cite the SimPEG paper when using discretize in your work: 88 | 89 | 90 | Cockett, R., Kang, S., Heagy, L. J., Pidlisecky, A., & Oldenburg, D. W. (2015). SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications. Computers & Geosciences. 91 | 92 | **BibTex:** 93 | 94 | .. code:: Latex 95 | 96 | @article{cockett2015simpeg, 97 | title={SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications}, 98 | author={Cockett, Rowan and Kang, Seogi and Heagy, Lindsey J and Pidlisecky, Adam and Oldenburg, Douglas W}, 99 | journal={Computers \& Geosciences}, 100 | year={2015}, 101 | publisher={Elsevier} 102 | } 103 | 104 | Links 105 | ^^^^^ 106 | 107 | Website: 108 | http://simpeg.xyz 109 | 110 | Documentation: 111 | http://discretize.simpeg.xyz 112 | 113 | Code: 114 | https://github.com/simpeg/discretize 115 | 116 | Tests: 117 | https://dev.azure.com/simpeg/discretize/_build 118 | 119 | Bugs & Issues: 120 | https://github.com/simpeg/discretize/issues 121 | 122 | Questions: 123 | http://simpeg.discourse.group/ 124 | 125 | Chat: 126 | https://mattermost.softwareunderground.org/simpeg 127 | 128 | -------------------------------------------------------------------------------- /discretize/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ======================================================== 3 | Utility Classes and Functions (:mod:`discretize.utils`) 4 | ======================================================== 5 | .. currentmodule:: discretize.utils 6 | 7 | The ``utils`` package contains utilities for helping with common operations involving 8 | discrete meshes 9 | 10 | Utility Classes 11 | =============== 12 | .. autosummary:: 13 | :toctree: generated/ 14 | 15 | TensorType 16 | Zero 17 | Identity 18 | 19 | Utility Functions 20 | ================= 21 | 22 | Code Utilities 23 | -------------- 24 | .. autosummary:: 25 | :toctree: generated/ 26 | 27 | is_scalar 28 | as_array_n_by_dim 29 | requires 30 | 31 | Coordinate Transform Utilities 32 | ------------------------------ 33 | .. autosummary:: 34 | :toctree: generated/ 35 | 36 | rotate_points_from_normals 37 | rotation_matrix_from_normals 38 | cylindrical_to_cartesian 39 | cartesian_to_cylindrical 40 | 41 | Interpolation Utilities 42 | ----------------------- 43 | .. autosummary:: 44 | :toctree: generated/ 45 | 46 | interpolation_matrix 47 | volume_average 48 | 49 | IO utilities 50 | ------------ 51 | .. autosummary:: 52 | :toctree: generated/ 53 | 54 | load_mesh 55 | download 56 | 57 | Matrix Utilities 58 | ---------------- 59 | .. autosummary:: 60 | :toctree: generated/ 61 | 62 | mkvc 63 | sdiag 64 | sdinv 65 | speye 66 | kron3 67 | spzeros 68 | ddx 69 | av 70 | av_extrap 71 | ndgrid 72 | ind2sub 73 | sub2ind 74 | get_subarray 75 | inverse_3x3_block_diagonal 76 | inverse_2x2_block_diagonal 77 | invert_blocks 78 | make_property_tensor 79 | inverse_property_tensor 80 | cross2d 81 | 82 | Mesh Utilities 83 | -------------- 84 | .. autosummary:: 85 | :toctree: generated/ 86 | 87 | unpack_widths 88 | closest_points_index 89 | extract_core_mesh 90 | random_model 91 | refine_tree_xyz 92 | active_from_xyz 93 | mesh_builder_xyz 94 | 95 | Utilities for Curvilinear Meshes 96 | -------------------------------- 97 | .. autosummary:: 98 | :toctree: generated/ 99 | 100 | example_curvilinear_grid 101 | volume_tetrahedron 102 | face_info 103 | index_cube 104 | """ 105 | 106 | from discretize.utils.code_utils import is_scalar, as_array_n_by_dim, requires 107 | from discretize.utils.matrix_utils import ( 108 | mkvc, 109 | sdiag, 110 | sdinv, 111 | speye, 112 | kron3, 113 | spzeros, 114 | ddx, 115 | av, 116 | av_extrap, 117 | ndgrid, 118 | make_boundary_bool, 119 | ind2sub, 120 | sub2ind, 121 | get_subarray, 122 | inverse_3x3_block_diagonal, 123 | inverse_2x2_block_diagonal, 124 | invert_blocks, 125 | TensorType, 126 | make_property_tensor, 127 | inverse_property_tensor, 128 | Zero, 129 | Identity, 130 | ) 131 | from discretize.utils.mesh_utils import ( 132 | unpack_widths, 133 | closest_points_index, 134 | extract_core_mesh, 135 | random_model, 136 | refine_tree_xyz, 137 | active_from_xyz, 138 | mesh_builder_xyz, 139 | example_simplex_mesh, 140 | ) 141 | from discretize.utils.curvilinear_utils import ( 142 | example_curvilinear_grid, 143 | volume_tetrahedron, 144 | face_info, 145 | index_cube, 146 | ) 147 | from discretize.utils.interpolation_utils import interpolation_matrix, volume_average 148 | from discretize.utils.coordinate_utils import ( 149 | rotate_points_from_normals, 150 | rotation_matrix_from_normals, 151 | cyl2cart, 152 | cart2cyl, 153 | cylindrical_to_cartesian, 154 | cartesian_to_cylindrical, 155 | # rotate_vec_cyl2cart 156 | ) 157 | 158 | from discretize.utils.io_utils import download, load_mesh 159 | 160 | # DEPRECATIONS 161 | from discretize.utils.code_utils import isScalar, asArray_N_x_Dim 162 | from discretize.utils.matrix_utils import ( 163 | sdInv, 164 | getSubArray, 165 | inv3X3BlockDiagonal, 166 | inv2X2BlockDiagonal, 167 | makePropertyTensor, 168 | invPropertyTensor, 169 | cross2d, 170 | ) 171 | from discretize.utils.mesh_utils import ( 172 | meshTensor, 173 | closestPoints, 174 | ExtractCoreMesh, 175 | ) 176 | from discretize.utils.curvilinear_utils import ( 177 | exampleLrmGrid, 178 | volTetra, 179 | indexCube, 180 | faceInfo, 181 | ) 182 | from discretize.utils.interpolation_utils import interpmat 183 | from discretize.utils.coordinate_utils import ( 184 | rotationMatrixFromNormals, 185 | rotatePointsFromNormals, 186 | ) 187 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [build-system] 3 | build-backend = 'mesonpy' 4 | requires = [ 5 | "meson-python>=0.15.0", 6 | "Cython>=3.1.0", 7 | "setuptools_scm[toml]>=6.2", 8 | 9 | # numpy requirement for wheel builds for distribution on PyPI - building 10 | # against 2.x yields wheels that are also compatible with numpy 1.x at 11 | # runtime. 12 | "numpy>=2.0.0rc1", 13 | ] 14 | 15 | [project] 16 | name = 'discretize' 17 | dynamic = ["version"] 18 | description = 'Discretization tools for finite volume and inverse problems' 19 | readme = 'README.rst' 20 | requires-python = '>=3.11' 21 | authors = [ 22 | {name = 'SimPEG developers', email = 'rowanc1@gmail.com'}, 23 | ] 24 | keywords = [ 25 | 'finite volume', 'discretization', 'pde', 'ode' 26 | ] 27 | 28 | # Note: Python and NumPy upper version bounds should be set correctly in 29 | # release branches, see: 30 | # https://scipy.github.io/devdocs/dev/core-dev/index.html#version-ranges-for-numpy-and-other-dependencies 31 | dependencies = [ 32 | # TODO: update to "pin-compatible" once possible, see 33 | # https://github.com/mesonbuild/meson-python/issues/29 34 | "numpy>=1.22.4", 35 | "scipy>=1.12", 36 | ] 37 | classifiers = [ 38 | "Development Status :: 4 - Beta", 39 | "Intended Audience :: Developers", 40 | "Intended Audience :: Science/Research", 41 | "License :: OSI Approved :: MIT License", 42 | "Programming Language :: Python", 43 | "Programming Language :: Cython", 44 | "Programming Language :: C++", 45 | "Topic :: Scientific/Engineering", 46 | "Topic :: Scientific/Engineering :: Mathematics", 47 | "Topic :: Scientific/Engineering :: Physics", 48 | "Operating System :: Microsoft :: Windows", 49 | "Operating System :: POSIX", 50 | "Operating System :: Unix", 51 | "Operating System :: MacOS", 52 | "Natural Language :: English", 53 | ] 54 | 55 | [project.license] 56 | file = 'LICENSE' 57 | 58 | [project.optional-dependencies] 59 | plot = ["matplotlib"] 60 | viz = ["vtk>=6", "pyvista"] 61 | omf = ["omf"] 62 | all = ["discretize[plot,viz,omf]"] 63 | doc = [ 64 | "sphinx==8.1.3", 65 | "pydata-sphinx-theme==0.16.1", 66 | "sphinx-gallery==0.19.0", 67 | "numpydoc==1.9.0", 68 | "jupyter", 69 | "graphviz", 70 | "pillow", 71 | "pooch", 72 | "discretize[all]", 73 | ] 74 | test = [ 75 | "pytest", 76 | "pytest-cov", 77 | "sympy", 78 | "discretize[doc,all]", 79 | ] 80 | # when changing these, make sure to keep it consistent with .pre-commit-config. 81 | style = [ 82 | "black==24.3.0", 83 | "flake8==7.0.0", 84 | "flake8-bugbear==23.12.2", 85 | "flake8-builtins==2.2.0", 86 | "flake8-mutable==1.2.0", 87 | "flake8-rst-docstrings==0.3.0", 88 | "flake8-docstrings==1.7.0", 89 | "flake8-pyproject==1.2.3", 90 | ] 91 | build = [ 92 | "meson-python>=0.15.0", 93 | "meson", 94 | "ninja", 95 | "numpy>=2.0.0rc1", 96 | "cython>=3.1.0", 97 | "setuptools_scm", 98 | ] 99 | 100 | [project.urls] 101 | Homepage = 'https://simpeg.xyz' 102 | Documentation = 'https://discretize.simpeg.xyz' 103 | Repository = 'http://github.com/simpeg/discretize.git' 104 | 105 | [tool.setuptools_scm] 106 | 107 | [tool.cibuildwheel] 108 | # skip building wheels for python 3.6, 3.7, 3.8, 3.9, all pypy versions, and specialty linux 109 | # processors (still does arm builds though). 110 | # skip windows 32bit 111 | skip = "cp38-* cp39-* cp310-* *_ppc64le *_i686 *_s390x *-win32 cp310-win_arm64" 112 | build-verbosity = 3 113 | enable = ["cpython-freethreading"] 114 | 115 | # test importing discretize to make sure externals are loadable. 116 | test-command = 'python -c "import discretize; print(discretize.__version__)"' 117 | 118 | 119 | # use the visual studio compilers 120 | [tool.cibuildwheel.windows.config-settings] 121 | setup-args = [ 122 | '--vsenv' 123 | ] 124 | 125 | [tool.coverage.run] 126 | branch = true 127 | source = ["discretize", "tests", "examples", "tutorials"] 128 | # plugins = [ 129 | # "Cython.Coverage", 130 | # ] 131 | 132 | [tool.coverage.report] 133 | ignore_errors = false 134 | show_missing = true 135 | # Regexes for lines to exclude from consideration 136 | exclude_also = [ 137 | # Don't complain about missing debug-only code: 138 | "def __repr__", 139 | "if self\\.debug", 140 | 141 | # Don't complain if tests don't hit defensive assertion code: 142 | "raise AssertionError", 143 | "raise NotImplementedError", 144 | "AbstractMethodError", 145 | 146 | # Don't complain if non-runnable code isn't run: 147 | "if 0:", 148 | "if __name__ == .__main__.:", 149 | 150 | # Don't complain about abstract methods, they aren't run: 151 | "@(abc\\.)?abstractmethod", 152 | ] 153 | 154 | [tool.black] 155 | required-version = '24.3.0' 156 | target-version = ['py38', 'py39', 'py310', 'py311'] 157 | 158 | [tool.flake8] 159 | extend-ignore = [ 160 | # Too many leading '#' for block comment 161 | 'E266', 162 | # Line too long (82 > 79 characters) 163 | 'E501', 164 | # Do not use variables named 'I', 'O', or 'l' 165 | 'E741', 166 | # Line break before binary operator (conflicts with black) 167 | 'W503', 168 | # Ignore spaces before a colon (Black handles it) 169 | 'E203', 170 | # Ignore spaces around an operator (Black handles it) 171 | 'E225', 172 | # Ignore rst warnings for start and end, due to *args and **kwargs being invalid rst, but good for numpydoc 173 | 'RST210', 174 | 'RST213', 175 | # ignore undocced __init__ 176 | 'D107', 177 | ] 178 | exclude = [ 179 | '.git', 180 | '.eggs', 181 | '__pycache__', 182 | '.ipynb_checkpoints', 183 | 'docs/examples/*', 184 | 'docs/tutorials/*', 185 | 'docs/*', 186 | 'discretize/_extensions/*.py', 187 | '.ci/*' 188 | ] 189 | per-file-ignores = [ 190 | # disable unused-imports errors on __init__.py 191 | # Automodule used for __init__ scripts' description 192 | '__init__.py: F401, D204, D205, D400', 193 | # do not check for assigned lambdas in tests 194 | # do not check for missing docstrings in tests 195 | 'tests/*: E731, D', 196 | 'tutorials/*: D', 197 | 'examples/*: D', 198 | ] 199 | exclude-from-doctest = [ 200 | # Only check discretize for docstring style 201 | 'tests', 202 | 'tutorials', 203 | 'examples', 204 | ] 205 | 206 | rst-roles = [ 207 | 'class', 208 | 'func', 209 | 'mod', 210 | 'meth', 211 | 'attr', 212 | 'ref', 213 | 'data', 214 | # Python programming language: 215 | 'py:func','py:mod','py:attr','py:meth', 216 | ] 217 | 218 | rst-directives = [ 219 | # These are sorted alphabetically - but that does not matter 220 | 'autosummary', 221 | 'currentmodule', 222 | 'deprecated', 223 | ] 224 | -------------------------------------------------------------------------------- /discretize/_extensions/tree.h: -------------------------------------------------------------------------------- 1 | #ifndef __TREE_H 2 | #define __TREE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "geom.h" 10 | 11 | typedef std::size_t int_t; 12 | 13 | inline int_t key_func(int_t x, int_t y){ 14 | //Double Cantor pairing 15 | return ((x+y)*(x+y+1))/2+y; 16 | } 17 | inline int_t key_func(int_t x, int_t y, int_t z){ 18 | return key_func(key_func(x, y), z); 19 | } 20 | class Node; 21 | class Edge; 22 | class Face; 23 | class Cell; 24 | class Tree; 25 | class PyWrapper; 26 | typedef PyWrapper* function; 27 | 28 | typedef std::map node_map_t; 29 | typedef std::map edge_map_t; 30 | typedef std::map face_map_t; 31 | typedef node_map_t::iterator node_it_type; 32 | typedef edge_map_t::iterator edge_it_type; 33 | typedef face_map_t::iterator face_it_type; 34 | typedef std::vector cell_vec_t; 35 | typedef std::vector int_vec_t; 36 | 37 | class PyWrapper{ 38 | public: 39 | void *py_func; 40 | int (*eval)(void *, Cell*); 41 | 42 | PyWrapper(){ 43 | py_func = NULL; 44 | }; 45 | 46 | void set(void* func, int (*wrapper)(void*, Cell*)){ 47 | py_func = func; 48 | eval = wrapper; 49 | }; 50 | 51 | int operator()(Cell * cell){ 52 | return eval(py_func, cell); 53 | }; 54 | }; 55 | 56 | class Node{ 57 | public: 58 | int_t location_ind[3]; 59 | double location[3]; 60 | int_t key; 61 | int_t reference; 62 | int_t index; 63 | bool hanging; 64 | Node *parents[4]; 65 | Node(); 66 | Node(int_t, int_t, int_t, double*, double*, double*); 67 | double operator[](int_t index){ 68 | return location[index]; 69 | }; 70 | }; 71 | 72 | class Edge{ 73 | public: 74 | int_t location_ind[3]; 75 | double location[3]; 76 | int_t key; 77 | int_t reference; 78 | int_t index; 79 | double length; 80 | bool hanging; 81 | Node *points[2]; 82 | Edge *parents[2]; 83 | Edge(); 84 | Edge(Node& p1, Node&p2); 85 | double operator[](int_t index){ 86 | return location[index]; 87 | }; 88 | }; 89 | 90 | class Face{ 91 | public: 92 | int_t location_ind[3]; 93 | double location[3]; 94 | int_t key; 95 | int_t reference; 96 | int_t index; 97 | double area; 98 | bool hanging; 99 | Node *points[4]; 100 | Edge *edges[4]; 101 | Face *parent; 102 | Face(); 103 | Face(Node& p1, Node& p2, Node& p3, Node& p4); 104 | double operator[](int_t index){ 105 | return location[index]; 106 | }; 107 | }; 108 | 109 | class Cell{ 110 | public: 111 | int_t n_dim; 112 | Cell *parent, *children[8], *neighbors[6]; 113 | Node *points[8]; 114 | Edge *edges[12]; 115 | Face *faces[6]; 116 | 117 | int_t location_ind[3], key, level, max_level; 118 | long long int index; // non root parents will have a -1 value 119 | double location[3]; 120 | double operator[](int_t index){ 121 | return location[index]; 122 | }; 123 | double volume; 124 | 125 | Cell(); 126 | Cell(Node *pts[8], int_t ndim, int_t maxlevel);//, function func); 127 | Cell(Node *pts[8], Cell *parent); 128 | ~Cell(); 129 | 130 | inline Node* min_node(){ return points[0];}; 131 | inline Node* max_node(){ return points[(1< 150 | void refine_geom(node_map_t& nodes, const T& geom, int_t p_level, double *xs, double *ys, double* zs, bool diag_balance=false){ 151 | // early exit if my level is higher than or equal to target 152 | if (level >= p_level || level == max_level){ 153 | return; 154 | } 155 | double *a = min_node()->location; 156 | double *b = max_node()->location; 157 | // if I intersect cell, I will need to be divided (if I'm not already) 158 | if (geom.intersects_cell(a, b)){ 159 | if(is_leaf()){ 160 | divide(nodes, xs, ys, zs, true, diag_balance); 161 | } 162 | // recurse into children 163 | for(int_t i = 0; i < (1<refine_geom(nodes, geom, p_level, xs, ys, zs, diag_balance); 165 | } 166 | } 167 | } 168 | 169 | template 170 | void find_cells_geom(int_vec_t &cells, const T& geom){ 171 | double *a = min_node()->location; 172 | double *b = max_node()->location; 173 | if(geom.intersects_cell(a, b)){ 174 | if(this->is_leaf()){ 175 | cells.push_back(index); 176 | return; 177 | } 178 | for(int_t i = 0; i < (1<find_cells_geom(cells, geom); 180 | } 181 | } 182 | } 183 | }; 184 | 185 | class Tree{ 186 | public: 187 | int_t n_dim; 188 | std::vector > > roots; 189 | int_t max_level, nx, ny, nz; 190 | int_t *ixs, *iys, *izs; 191 | int_t nx_roots, ny_roots, nz_roots; 192 | double *xs; 193 | double *ys; 194 | double *zs; 195 | 196 | std::vector cells; 197 | node_map_t nodes; 198 | edge_map_t edges_x, edges_y, edges_z; 199 | face_map_t faces_x, faces_y, faces_z; 200 | std::vector hanging_nodes; 201 | std::vector hanging_edges_x, hanging_edges_y, hanging_edges_z; 202 | std::vector hanging_faces_x, hanging_faces_y, hanging_faces_z; 203 | 204 | Tree(); 205 | ~Tree(); 206 | 207 | void set_dimension(int_t dim); 208 | void set_levels(int_t l_x, int_t l_y, int_t l_z); 209 | void set_xs(double *x , double *y, double *z); 210 | void initialize_roots(); 211 | void number(); 212 | void finalize_lists(); 213 | 214 | void shift_cell_centers(double *shift); 215 | 216 | void insert_cell(double *new_center, int_t p_level, bool diagonal_balance=false); 217 | Cell* containing_cell(double, double, double); 218 | 219 | void refine_function(function test_func, bool diagonal_balance=false); 220 | 221 | void refine_image(double* image, bool diagonal_balance=false); 222 | 223 | template 224 | void refine_geom(const T& geom, int_t p_level, bool diagonal_balance=false){ 225 | for(int_t iz=0; izrefine_geom(nodes, geom, p_level, xs, ys, zs, diagonal_balance); 229 | }; 230 | 231 | template 232 | int_vec_t find_cells_geom(const T& geom){ 233 | int_vec_t intersections; 234 | for(int_t iz=0; izfind_cells_geom(intersections, geom); 238 | } 239 | } 240 | } 241 | return intersections; 242 | }; 243 | 244 | }; 245 | 246 | #endif 247 | -------------------------------------------------------------------------------- /discretize/utils/coordinate_utils.py: -------------------------------------------------------------------------------- 1 | """Simple utilities for coordinate transformations.""" 2 | 3 | import numpy as np 4 | from discretize.utils.matrix_utils import mkvc 5 | from discretize.utils.code_utils import as_array_n_by_dim, deprecate_function 6 | 7 | 8 | def cylindrical_to_cartesian(grid, vec=None): 9 | r"""Transform from cylindrical to cartesian coordinates. 10 | 11 | Transform a grid or a vector from cylindrical coordinates :math:`(r, \theta, z)` to 12 | Cartesian coordinates :math:`(x, y, z)`. :math:`\theta` is given in radians. 13 | 14 | Parameters 15 | ---------- 16 | grid : (n, 3) array_like 17 | Location points defined in cylindrical coordinates :math:`(r, \theta, z)`. 18 | vec : (n, 3) array_like, optional 19 | Vector defined in cylindrical coordinates :math:`(r, \theta, z)` at the 20 | locations grid. Will also except a flattend array in column major order with the 21 | same number of elements. 22 | 23 | Returns 24 | ------- 25 | (n, 3) numpy.ndarray 26 | If `vec` is ``None``, this returns the transformed `grid` array, otherwise 27 | this is the transformed `vec` array. 28 | 29 | Examples 30 | -------- 31 | Here, we convert a series of vectors in 3D space from cylindrical coordinates 32 | to Cartesian coordinates. 33 | 34 | >>> from discretize.utils import cylindrical_to_cartesian 35 | >>> import numpy as np 36 | 37 | Construct original set of vectors in cylindrical coordinates 38 | 39 | >>> r = np.ones(9) 40 | >>> phi = np.linspace(0, 2*np.pi, 9) 41 | >>> z = np.linspace(-4., 4., 9) 42 | >>> u = np.c_[r, phi, z] 43 | >>> u 44 | array([[ 1. , 0. , -4. ], 45 | [ 1. , 0.78539816, -3. ], 46 | [ 1. , 1.57079633, -2. ], 47 | [ 1. , 2.35619449, -1. ], 48 | [ 1. , 3.14159265, 0. ], 49 | [ 1. , 3.92699082, 1. ], 50 | [ 1. , 4.71238898, 2. ], 51 | [ 1. , 5.49778714, 3. ], 52 | [ 1. , 6.28318531, 4. ]]) 53 | 54 | Create equivalent set of vectors in Cartesian coordinates 55 | 56 | >>> v = cylindrical_to_cartesian(u) 57 | >>> v 58 | array([[ 1.00000000e+00, 0.00000000e+00, -4.00000000e+00], 59 | [ 7.07106781e-01, 7.07106781e-01, -3.00000000e+00], 60 | [ 6.12323400e-17, 1.00000000e+00, -2.00000000e+00], 61 | [-7.07106781e-01, 7.07106781e-01, -1.00000000e+00], 62 | [-1.00000000e+00, 1.22464680e-16, 0.00000000e+00], 63 | [-7.07106781e-01, -7.07106781e-01, 1.00000000e+00], 64 | [-1.83697020e-16, -1.00000000e+00, 2.00000000e+00], 65 | [ 7.07106781e-01, -7.07106781e-01, 3.00000000e+00], 66 | [ 1.00000000e+00, -2.44929360e-16, 4.00000000e+00]]) 67 | """ 68 | grid = np.atleast_2d(grid) 69 | 70 | if vec is None: 71 | return np.hstack( 72 | [ 73 | mkvc(grid[:, 0] * np.cos(grid[:, 1]), 2), 74 | mkvc(grid[:, 0] * np.sin(grid[:, 1]), 2), 75 | mkvc(grid[:, 2], 2), 76 | ] 77 | ) 78 | vec = np.asanyarray(vec) 79 | if len(vec.shape) == 1 or vec.shape[1] == 1: 80 | vec = vec.reshape(grid.shape, order="F") 81 | 82 | x = vec[:, 0] * np.cos(grid[:, 1]) - vec[:, 1] * np.sin(grid[:, 1]) 83 | y = vec[:, 0] * np.sin(grid[:, 1]) + vec[:, 1] * np.cos(grid[:, 1]) 84 | 85 | newvec = [x, y] 86 | if grid.shape[1] == 3: 87 | z = vec[:, 2] 88 | newvec += [z] 89 | 90 | return np.vstack(newvec).T 91 | 92 | 93 | def cyl2cart(grid, vec=None): 94 | """Transform from cylindrical to cartesian coordinates. 95 | 96 | An alias for `cylindrical_to_cartesian``. 97 | 98 | See Also 99 | -------- 100 | cylindrical_to_cartesian 101 | """ 102 | return cylindrical_to_cartesian(grid, vec) 103 | 104 | 105 | def cartesian_to_cylindrical(grid, vec=None): 106 | r"""Transform from cartesian to cylindrical coordinates. 107 | 108 | Transform a grid or a vector from Cartesian coordinates :math:`(x, y, z)` to 109 | cylindrical coordinates :math:`(r, \theta, z)`. 110 | 111 | Parameters 112 | ---------- 113 | grid : (n, 3) array_like 114 | Location points defined in Cartesian coordinates :math:`(x, y z)`. 115 | vec : (n, 3) array_like, optional 116 | Vector defined in Cartesian coordinates. This also accepts a flattened array 117 | with the same total elements in column major order. 118 | 119 | Returns 120 | ------- 121 | (n, 3) numpy.ndarray 122 | If `vec` is ``None``, this returns the transformed `grid` array, otherwise 123 | this is the transformed `vec` array. 124 | 125 | Examples 126 | -------- 127 | Here, we convert a series of vectors in 3D space from Cartesian coordinates 128 | to cylindrical coordinates. 129 | 130 | >>> from discretize.utils import cartesian_to_cylindrical 131 | >>> import numpy as np 132 | 133 | Create set of vectors in Cartesian coordinates 134 | 135 | >>> r = np.ones(9) 136 | >>> phi = np.linspace(0, 2*np.pi, 9) 137 | >>> z = np.linspace(-4., 4., 9) 138 | >>> x = r*np.cos(phi) 139 | >>> y = r*np.sin(phi) 140 | >>> u = np.c_[x, y, z] 141 | >>> u 142 | array([[ 1.00000000e+00, 0.00000000e+00, -4.00000000e+00], 143 | [ 7.07106781e-01, 7.07106781e-01, -3.00000000e+00], 144 | [ 6.12323400e-17, 1.00000000e+00, -2.00000000e+00], 145 | [-7.07106781e-01, 7.07106781e-01, -1.00000000e+00], 146 | [-1.00000000e+00, 1.22464680e-16, 0.00000000e+00], 147 | [-7.07106781e-01, -7.07106781e-01, 1.00000000e+00], 148 | [-1.83697020e-16, -1.00000000e+00, 2.00000000e+00], 149 | [ 7.07106781e-01, -7.07106781e-01, 3.00000000e+00], 150 | [ 1.00000000e+00, -2.44929360e-16, 4.00000000e+00]]) 151 | 152 | Compute equivalent set of vectors in cylindrical coordinates 153 | 154 | >>> v = cartesian_to_cylindrical(u) 155 | >>> v 156 | array([[ 1.00000000e+00, 0.00000000e+00, -4.00000000e+00], 157 | [ 1.00000000e+00, 7.85398163e-01, -3.00000000e+00], 158 | [ 1.00000000e+00, 1.57079633e+00, -2.00000000e+00], 159 | [ 1.00000000e+00, 2.35619449e+00, -1.00000000e+00], 160 | [ 1.00000000e+00, 3.14159265e+00, 0.00000000e+00], 161 | [ 1.00000000e+00, -2.35619449e+00, 1.00000000e+00], 162 | [ 1.00000000e+00, -1.57079633e+00, 2.00000000e+00], 163 | [ 1.00000000e+00, -7.85398163e-01, 3.00000000e+00], 164 | [ 1.00000000e+00, -2.44929360e-16, 4.00000000e+00]]) 165 | """ 166 | grid = as_array_n_by_dim(grid, 3) 167 | theta = np.arctan2(grid[:, 1], grid[:, 0]) 168 | if vec is None: 169 | return np.c_[np.linalg.norm(grid[:, :2], axis=-1), theta, grid[:, 2]] 170 | vec = as_array_n_by_dim(vec, 3) 171 | 172 | return np.hstack( 173 | [ 174 | mkvc(np.cos(theta) * vec[:, 0] + np.sin(theta) * vec[:, 1], 2), 175 | mkvc(-np.sin(theta) * vec[:, 0] + np.cos(theta) * vec[:, 1], 2), 176 | mkvc(vec[:, 2], 2), 177 | ] 178 | ) 179 | 180 | 181 | def cart2cyl(grid, vec=None): 182 | """Transform from cartesian to cylindrical coordinates. 183 | 184 | An alias for cartesian_to_cylindrical 185 | 186 | See Also 187 | -------- 188 | cartesian_to_cylindrical 189 | """ 190 | return cartesian_to_cylindrical(grid, vec) 191 | 192 | 193 | def rotation_matrix_from_normals(v0, v1, tol=1e-20): 194 | r"""Generate a 3x3 rotation matrix defining the rotation from vector v0 to v1. 195 | 196 | This function uses Rodrigues' rotation formula to generate the rotation 197 | matrix :math:`\mathbf{A}` going from vector :math:`\mathbf{v_0}` to 198 | vector :math:`\mathbf{v_1}`. Thus: 199 | 200 | .. math:: 201 | \mathbf{Av_0} = \mathbf{v_1} 202 | 203 | For detailed desciption of the algorithm, see 204 | https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula 205 | 206 | Parameters 207 | ---------- 208 | v0 : (3) numpy.ndarray 209 | Starting orientation direction 210 | v1 : (3) numpy.ndarray 211 | Finishing orientation direction 212 | tol : float, optional 213 | Numerical tolerance. If the length of the rotation axis is below this value, 214 | it is assumed to be no rotation, and an identity matrix is returned. 215 | 216 | Returns 217 | ------- 218 | (3, 3) numpy.ndarray 219 | The rotation matrix from v0 to v1. 220 | """ 221 | # ensure both v0, v1 are vectors of length 1 222 | if len(v0) != 3: 223 | raise ValueError("Length of n0 should be 3") 224 | if len(v1) != 3: 225 | raise ValueError("Length of n1 should be 3") 226 | 227 | # ensure both are true normals 228 | n0 = v0 * 1.0 / np.linalg.norm(v0) 229 | n1 = v1 * 1.0 / np.linalg.norm(v1) 230 | 231 | n0dotn1 = n0.dot(n1) 232 | 233 | # define the rotation axis, which is the cross product of the two vectors 234 | rotAx = np.cross(n0, n1) 235 | 236 | if np.linalg.norm(rotAx) < tol: 237 | return np.eye(3, dtype=float) 238 | 239 | rotAx *= 1.0 / np.linalg.norm(rotAx) 240 | 241 | cosT = n0dotn1 / (np.linalg.norm(n0) * np.linalg.norm(n1)) 242 | sinT = np.sqrt(1.0 - n0dotn1**2) 243 | 244 | ux = np.array( 245 | [ 246 | [0.0, -rotAx[2], rotAx[1]], 247 | [rotAx[2], 0.0, -rotAx[0]], 248 | [-rotAx[1], rotAx[0], 0.0], 249 | ], 250 | dtype=float, 251 | ) 252 | 253 | return np.eye(3, dtype=float) + sinT * ux + (1.0 - cosT) * (ux.dot(ux)) 254 | 255 | 256 | def rotate_points_from_normals(xyz, v0, v1, x0=np.r_[0.0, 0.0, 0.0]): 257 | r"""Rotate a set of xyz locations about a specified point. 258 | 259 | Rotate a grid of Cartesian points about a location x0 according to the 260 | rotation defined from vector v0 to v1. 261 | 262 | Let :math:`\mathbf{x}` represent an input xyz location, let :math:`\mathbf{x_0}` be 263 | the origin of rotation, and let :math:`\mathbf{R}` denote the rotation matrix from 264 | vector v0 to v1. Where :math:`\mathbf{x'}` is the new xyz location, this function 265 | outputs the following operation for all input locations: 266 | 267 | .. math:: 268 | \mathbf{x'} = \mathbf{R (x - x_0)} + \mathbf{x_0} 269 | 270 | Parameters 271 | ---------- 272 | xyz : (n, 3) numpy.ndarray 273 | locations to rotate 274 | v0 : (3) numpy.ndarray 275 | Starting orientation direction 276 | v1 : (3) numpy.ndarray 277 | Finishing orientation direction 278 | x0 : (3) numpy.ndarray, optional 279 | The origin of rotation. 280 | 281 | Returns 282 | ------- 283 | (n, 3) numpy.ndarray 284 | The rotated xyz locations. 285 | """ 286 | # Compute rotation matrix between v0 and v1 287 | R = rotation_matrix_from_normals(v0, v1) 288 | 289 | if xyz.shape[1] != 3: 290 | raise ValueError("Grid of xyz points should be n x 3") 291 | if len(x0) != 3: 292 | raise ValueError("x0 should have length 3") 293 | 294 | # Define origin 295 | X0 = np.ones([xyz.shape[0], 1]) * mkvc(x0) 296 | 297 | return (xyz - X0).dot(R.T) + X0 # equivalent to (R*(xyz - X0)).T + X0 298 | 299 | 300 | rotationMatrixFromNormals = deprecate_function( 301 | rotation_matrix_from_normals, 302 | "rotationMatrixFromNormals", 303 | removal_version="1.0.0", 304 | error=True, 305 | ) 306 | rotatePointsFromNormals = deprecate_function( 307 | rotate_points_from_normals, 308 | "rotatePointsFromNormals", 309 | removal_version="1.0.0", 310 | error=True, 311 | ) 312 | -------------------------------------------------------------------------------- /discretize/utils/interpolation_utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for creating averaging operators.""" 2 | 3 | import numpy as np 4 | import scipy.sparse as sp 5 | from discretize.utils.matrix_utils import mkvc, sub2ind 6 | from discretize.utils.code_utils import deprecate_function 7 | 8 | try: 9 | from discretize._extensions import interputils_cython as pyx 10 | 11 | _interp_point_1D = pyx._interp_point_1D 12 | _interpmat1D = pyx._interpmat1D 13 | _interpmat2D = pyx._interpmat2D 14 | _interpmat3D = pyx._interpmat3D 15 | _vol_interp = pyx._tensor_volume_averaging 16 | _interpCython = True 17 | except ImportError as err: 18 | print(err) 19 | import os 20 | 21 | # Check if being called from non-standard location (i.e. a git repository) 22 | # is tree_ext.cpp here? will not be in the folder if installed to site-packages... 23 | file_test = ( 24 | os.path.dirname(os.path.abspath(__file__)) 25 | + "/_extensions/interputils_cython.pyx" 26 | ) 27 | if os.path.isfile(file_test): 28 | # Then we are being run from a repository 29 | print( 30 | """ 31 | Unable to import interputils_cython. 32 | 33 | It would appear that discretize is being imported from its repository. 34 | If this is intentional, you need to run: 35 | 36 | python setup.py build_ext --inplace 37 | 38 | to build the cython code. 39 | """ 40 | ) 41 | _interpCython = False 42 | 43 | 44 | def interpolation_matrix(locs, x, y=None, z=None): 45 | """ 46 | Generate interpolation matrix which maps a tensor quantity to a set of locations. 47 | 48 | This function generates a sparse matrix for interpolating tensor quantities to a set 49 | of specified locations. It uses nD linear interpolation. The user may generate the 50 | interpolation matrix for tensor quantities that live on 1D, 2D or 3D tensors. This 51 | functionality is frequently used to interpolate quantites from cell centers or nodes 52 | to specified locations. 53 | 54 | In higher dimensions the ordering of the output has the 1st dimension changing the 55 | quickest. 56 | 57 | Parameters 58 | ---------- 59 | locs : (n, dim) numpy.ndarray 60 | The locations for the interpolated values. Here *n* is 61 | the number of locations and *dim* is the dimension (1, 2 or 3) 62 | x : (nx) numpy.ndarray 63 | Vector defining the locations of the tensor along the x-axis 64 | y : (ny) numpy.ndarray, optional 65 | Vector defining the locations of the tensor along the y-axis. Required if 66 | ``dim`` is 2. 67 | z : (nz) numpy.ndarray, optional 68 | Vector defining the locations of the tensor along the z-axis. Required if 69 | ``dim`` is 3. 70 | 71 | Returns 72 | ------- 73 | (n, nx * ny * nz) scipy.sparse.csr_matrix 74 | A sparse matrix which interpolates the tensor quantity on cell centers or nodes 75 | to the set of specified locations. 76 | 77 | Examples 78 | -------- 79 | Here is a 1D example where a function evaluated on a regularly spaced grid 80 | is interpolated to a set of random locations. To compare the accuracy, the 81 | function is evaluated at the set of random locations. 82 | 83 | >>> from discretize.utils import interpolation_matrix 84 | >>> from discretize import TensorMesh 85 | >>> import numpy as np 86 | >>> import matplotlib.pyplot as plt 87 | >>> rng = np.random.default_rng(14) 88 | 89 | Create an interpolation matrix 90 | 91 | >>> locs = rng.random(50)*0.8+0.1 92 | >>> x = np.linspace(0, 1, 7) 93 | >>> dense = np.linspace(0, 1, 200) 94 | >>> fun = lambda x: np.cos(2*np.pi*x) 95 | >>> Q = interpolation_matrix(locs, x) 96 | 97 | Plot original function and interpolation 98 | 99 | >>> fig1 = plt.figure(figsize=(5, 3)) 100 | >>> ax = fig1.add_axes([0.1, 0.1, 0.8, 0.8]) 101 | >>> ax.plot(dense, fun(dense), 'k:', lw=3) 102 | >>> ax.plot(x, fun(x), 'ks', markersize=8) 103 | >>> ax.plot(locs, Q*fun(x), 'go', markersize=4) 104 | >>> ax.plot(locs, fun(locs), 'rs', markersize=4) 105 | >>> ax.legend( 106 | ... [ 107 | ... 'True Function', 108 | ... 'True (discrete loc.)', 109 | ... 'Interpolated (computed)', 110 | ... 'True (interp. loc.)' 111 | ... ], 112 | ... loc='upper center' 113 | ... ) 114 | >>> plt.show() 115 | 116 | Here, demonstrate a similar example on a 2D mesh using a 2D Gaussian distribution. 117 | We interpolate the Gaussian from the nodes to cell centers and examine the relative 118 | error. 119 | 120 | >>> hx = np.ones(10) 121 | >>> hy = np.ones(10) 122 | >>> mesh = TensorMesh([hx, hy], x0='CC') 123 | >>> def fun(x, y): 124 | ... return np.exp(-(x**2 + y**2)/2**2) 125 | 126 | Define the the value at the mesh nodes, 127 | 128 | >>> nodes = mesh.nodes 129 | >>> val_nodes = fun(nodes[:, 0], nodes[:, 1]) 130 | 131 | >>> centers = mesh.cell_centers 132 | >>> A = interpolation_matrix( 133 | ... centers, mesh.nodes_x, mesh.nodes_y 134 | ... ) 135 | >>> val_interp = A.dot(val_nodes) 136 | 137 | Plot the interpolated values, along with the true values at cell centers, 138 | 139 | >>> val_centers = fun(centers[:, 0], centers[:, 1]) 140 | >>> fig = plt.figure(figsize=(11,3.3)) 141 | >>> clim = (0., 1.) 142 | >>> ax1 = fig.add_subplot(131) 143 | >>> ax2 = fig.add_subplot(132) 144 | >>> ax3 = fig.add_subplot(133) 145 | >>> mesh.plot_image(val_centers, ax=ax1, clim=clim) 146 | >>> mesh.plot_image(val_interp, ax=ax2, clim=clim) 147 | >>> mesh.plot_image(val_centers-val_interp, ax=ax3, clim=clim) 148 | >>> ax1.set_title('Analytic at Centers') 149 | >>> ax2.set_title('Interpolated from Nodes') 150 | >>> ax3.set_title('Relative Error') 151 | >>> plt.show() 152 | """ 153 | npts = locs.shape[0] 154 | locs = locs.astype(float) 155 | x = x.astype(float) 156 | if y is None and z is None: 157 | shape = [x.size] 158 | inds, vals = _interpmat1D(mkvc(locs), x) 159 | elif z is None: 160 | y = y.astype(float) 161 | shape = [x.size, y.size] 162 | inds, vals = _interpmat2D(locs, x, y) 163 | else: 164 | y = y.astype(float) 165 | z = z.astype(float) 166 | shape = [x.size, y.size, z.size] 167 | inds, vals = _interpmat3D(locs, x, y, z) 168 | 169 | I = np.repeat(range(npts), 2 ** len(shape)) 170 | J = sub2ind(shape, inds) 171 | Q = sp.csr_matrix((vals, (I, J)), shape=(npts, np.prod(shape))) 172 | return Q 173 | 174 | 175 | def volume_average(mesh_in, mesh_out, values=None, output=None): 176 | """Volume averaging interpolation between meshes. 177 | 178 | This volume averaging function looks for overlapping cells in each mesh, 179 | and weights the output values by the partial volume ratio of the overlapping 180 | input cells. The volume average operation should result in an output such that 181 | ``np.sum(mesh_in.cell_volumes*values)`` = ``np.sum(mesh_out.cell_volumes*output)``, 182 | when the input and output meshes have the exact same extent. When the output mesh 183 | extent goes beyond the input mesh, it is assumed to have constant values in that 184 | direction. When the output mesh extent is smaller than the input mesh, only the 185 | overlapping extent of the input mesh contributes to the output. 186 | 187 | This function operates in three different modes. If only *mesh_in* and 188 | *mesh_out* are given, the returned value is a ``scipy.sparse.csr_matrix`` 189 | that represents this operation (so it could potentially be applied repeatedly). 190 | If *values* is given, the volume averaging is performed right away (without 191 | internally forming the matrix) and the returned value is the result of this. 192 | If *output* is given as well, it will be filled with the values of the 193 | operation and then returned (assuming it has the correct ``dtype``). 194 | 195 | Parameters 196 | ---------- 197 | mesh_in : ~discretize.TensorMesh or ~discretize.TreeMesh 198 | Input mesh (the mesh you are interpolating from) 199 | mesh_out : ~discretize.TensorMesh or ~discretize.TreeMesh 200 | Output mesh (the mesh you are interpolating to) 201 | values : (mesh_in.n_cells) numpy.ndarray, optional 202 | Array with values defined at the cells of ``mesh_in`` 203 | output : (mesh_out.n_cells) numpy.ndarray of float, optional 204 | Output array to be overwritten 205 | 206 | Returns 207 | ------- 208 | (mesh_out.n_cells, mesh_in.n_cells) scipy.sparse.csr_matrix or (mesh_out.n_cells) numpy.ndarray 209 | If *values* = *None* , the returned value is a matrix representing this 210 | operation, otherwise it is a :class:`numpy.ndarray` of the result of the 211 | operation. 212 | 213 | Examples 214 | -------- 215 | Create two meshes with the same extent, but different divisions (the meshes 216 | do not have to be the same extent). 217 | 218 | >>> import numpy as np 219 | >>> from discretize import TensorMesh 220 | >>> rng = np.random.default_rng(853) 221 | >>> h1 = np.ones(32) 222 | >>> h2 = np.ones(16)*2 223 | >>> mesh_in = TensorMesh([h1, h1]) 224 | >>> mesh_out = TensorMesh([h2, h2]) 225 | 226 | Create a random model defined on the input mesh, and use volume averaging to 227 | interpolate it to the output mesh. 228 | 229 | >>> from discretize.utils import volume_average 230 | >>> model1 = rng.random(mesh_in.nC) 231 | >>> model2 = volume_average(mesh_in, mesh_out, model1) 232 | 233 | Because these two meshes' cells are perfectly aligned, but the output mesh 234 | has 1 cell for each 4 of the input cells, this operation should effectively 235 | look like averaging each of those cells values 236 | 237 | >>> import matplotlib.pyplot as plt 238 | >>> plt.figure(figsize=(6, 3)) 239 | >>> ax1 = plt.subplot(121) 240 | >>> mesh_in.plot_image(model1, ax=ax1) 241 | >>> ax2 = plt.subplot(122) 242 | >>> mesh_out.plot_image(model2, ax=ax2) 243 | >>> plt.show() 244 | 245 | """ 246 | try: 247 | in_type = mesh_in._meshType 248 | out_type = mesh_out._meshType 249 | except AttributeError: 250 | raise TypeError("Both input and output mesh must be valid discetize meshes") 251 | 252 | valid_meshs = ["TENSOR", "TREE"] 253 | if in_type not in valid_meshs or out_type not in valid_meshs: 254 | raise NotImplementedError( 255 | f"Volume averaging is only implemented for TensorMesh and TreeMesh, " 256 | f"not {type(mesh_in).__name__} and/or {type(mesh_out).__name__}" 257 | ) 258 | 259 | if mesh_in.dim != mesh_out.dim: 260 | raise ValueError("Both meshes must have the same dimension") 261 | 262 | if values is not None and len(values) != mesh_in.nC: 263 | raise ValueError( 264 | "Input array does not have the same length as the number of cells in input mesh" 265 | ) 266 | if output is not None and len(output) != mesh_out.nC: 267 | raise ValueError( 268 | "Output array does not have the same length as the number of cells in output mesh" 269 | ) 270 | 271 | if values is not None: 272 | values = np.asarray(values, dtype=np.float64) 273 | if output is not None: 274 | output = np.asarray(output, dtype=np.float64) 275 | 276 | if in_type == "TENSOR": 277 | if out_type == "TENSOR": 278 | return _vol_interp(mesh_in, mesh_out, values, output) 279 | elif out_type == "TREE": 280 | return mesh_out._vol_avg_from_tens(mesh_in, values, output) 281 | elif in_type == "TREE": 282 | if out_type == "TENSOR": 283 | return mesh_in._vol_avg_to_tens(mesh_out, values, output) 284 | elif out_type == "TREE": 285 | return mesh_out._vol_avg_from_tree(mesh_in, values, output) 286 | else: 287 | raise TypeError("Unsupported mesh types") 288 | 289 | 290 | interpmat = deprecate_function( 291 | interpolation_matrix, "interpmat", removal_version="1.0.0", error=True 292 | ) 293 | -------------------------------------------------------------------------------- /discretize/_extensions/interputils_cython.pyx: -------------------------------------------------------------------------------- 1 | # cython: embedsignature=True, language_level=3 2 | # cython: linetrace=True 3 | # cython: freethreading_compatible = True 4 | import numpy as np 5 | import cython 6 | cimport numpy as np 7 | import scipy.sparse as sp 8 | 9 | def _interp_point_1D(np.ndarray[np.float64_t, ndim=1] x, float xr_i): 10 | """ 11 | given a point, xr_i, this will find which two integers it lies between. 12 | 13 | :param numpy.ndarray x: Tensor vector of 1st dimension of grid. 14 | :param float xr_i: Location of a point 15 | :rtype: int,int,float,float 16 | :return: index1, index2, portion1, portion2 17 | """ 18 | cdef IIFF xs 19 | _get_inds_ws(x,xr_i,&xs) 20 | return xs.i1,xs.i2,xs.w1,xs.w2 21 | 22 | cdef struct IIFF: 23 | np.int64_t i1,i2 24 | np.float64_t w1,w2 25 | 26 | @cython.boundscheck(False) 27 | @cython.wraparound(False) 28 | @cython.nonecheck(False) 29 | cdef np.int64_t _bisect_left(np.float64_t[:] a, np.float64_t x) nogil: 30 | cdef np.int64_t lo, hi, mid 31 | lo = 0 32 | hi = a.shape[0] 33 | while lo < hi: 34 | mid = (lo+hi)//2 35 | if a[mid] < x: lo = mid+1 36 | else: hi = mid 37 | return lo 38 | 39 | @cython.boundscheck(False) 40 | @cython.wraparound(False) 41 | @cython.nonecheck(False) 42 | cdef np.int64_t _bisect_right(np.float64_t[:] a, np.float64_t x) nogil: 43 | cdef np.int64_t lo, hi, mid 44 | lo = 0 45 | hi = a.shape[0] 46 | while lo < hi: 47 | mid = (lo+hi)//2 48 | if x < a[mid]: hi = mid 49 | else: lo = mid+1 50 | return lo 51 | 52 | @cython.boundscheck(False) 53 | @cython.wraparound(False) 54 | @cython.nonecheck(False) 55 | @cython.cdivision(True) 56 | cdef void _get_inds_ws(np.float64_t[:] x, np.float64_t xp, IIFF* out) nogil: 57 | cdef np.int64_t ind = _bisect_right(x,xp) 58 | cdef np.int64_t nx = x.shape[0] 59 | out.i2 = ind 60 | out.i1 = ind-1 61 | out.i2 = max(min(out.i2,nx-1),0) 62 | out.i1 = max(min(out.i1,nx-1),0) 63 | if(out.i1==out.i2): 64 | out.w1 = 0.5 65 | else: 66 | out.w1 = (x[out.i2]-xp)/(x[out.i2]-x[out.i1]) 67 | out.w2 = 1-out.w1 68 | 69 | @cython.boundscheck(False) 70 | @cython.wraparound(False) 71 | @cython.nonecheck(False) 72 | def _interpmat1D(np.ndarray[np.float64_t, ndim=1] locs, 73 | np.ndarray[np.float64_t, ndim=1] x): 74 | cdef int nx = x.size 75 | cdef IIFF xs 76 | cdef int npts = locs.shape[0] 77 | cdef int i 78 | 79 | cdef np.ndarray[np.int64_t,ndim=1] inds = np.empty(npts*2,dtype=np.int64) 80 | cdef np.ndarray[np.float64_t,ndim=1] vals = np.empty(npts*2,dtype=np.float64) 81 | for i in range(npts): 82 | _get_inds_ws(x,locs[i],&xs) 83 | 84 | inds[2*i ] = xs.i1 85 | inds[2*i+1] = xs.i2 86 | vals[2*i ] = xs.w1 87 | vals[2*i+1] = xs.w2 88 | 89 | return inds,vals 90 | 91 | @cython.boundscheck(False) 92 | @cython.wraparound(False) 93 | @cython.nonecheck(False) 94 | def _interpmat2D(np.ndarray[np.float64_t, ndim=2] locs, 95 | np.ndarray[np.float64_t, ndim=1] x, 96 | np.ndarray[np.float64_t, ndim=1] y): 97 | cdef int nx,ny 98 | nx,ny = len(x),len(y) 99 | cdef int npts = locs.shape[0] 100 | cdef int i 101 | cdef IIFF xs,ys 102 | 103 | cdef np.ndarray[np.int64_t,ndim=2] inds = np.empty((npts*4,2),dtype=np.int64) 104 | cdef np.ndarray[np.float64_t,ndim=1] vals = np.empty(npts*4,dtype=np.float64) 105 | for i in range(npts): 106 | _get_inds_ws(x,locs[i,0],&xs) 107 | _get_inds_ws(y,locs[i,1],&ys) 108 | 109 | inds[4*i ,0] = xs.i1 110 | inds[4*i+1,0] = xs.i1 111 | inds[4*i+2,0] = xs.i2 112 | inds[4*i+3,0] = xs.i2 113 | inds[4*i ,1] = ys.i1 114 | inds[4*i+1,1] = ys.i2 115 | inds[4*i+2,1] = ys.i1 116 | inds[4*i+3,1] = ys.i2 117 | 118 | vals[4*i ] = xs.w1*ys.w1 119 | vals[4*i+1] = xs.w1*ys.w2 120 | vals[4*i+2] = xs.w2*ys.w1 121 | vals[4*i+3] = xs.w2*ys.w2 122 | 123 | return inds,vals 124 | 125 | @cython.boundscheck(False) 126 | @cython.wraparound(False) 127 | @cython.nonecheck(False) 128 | def _interpmat3D(np.ndarray[np.float64_t, ndim=2] locs, 129 | np.ndarray[np.float64_t, ndim=1] x, 130 | np.ndarray[np.float64_t, ndim=1] y, 131 | np.ndarray[np.float64_t, ndim=1] z): 132 | 133 | cdef int nx,ny,nz 134 | nx,ny,nz = len(x),len(y),len(z) 135 | cdef IIFF xs,ys,zs 136 | cdef int npts = locs.shape[0] 137 | cdef int i 138 | 139 | cdef np.ndarray[np.int64_t,ndim=2] inds = np.empty((npts*8,3),dtype=np.int64) 140 | cdef np.ndarray[np.float64_t,ndim=1] vals = np.empty(npts*8,dtype=np.float64) 141 | for i in range(npts): 142 | _get_inds_ws(x,locs[i,0],&xs) 143 | _get_inds_ws(y,locs[i,1],&ys) 144 | _get_inds_ws(z,locs[i,2],&zs) 145 | 146 | inds[8*i ,0] = xs.i1 147 | inds[8*i+1,0] = xs.i1 148 | inds[8*i+2,0] = xs.i2 149 | inds[8*i+3,0] = xs.i2 150 | inds[8*i+4,0] = xs.i1 151 | inds[8*i+5,0] = xs.i1 152 | inds[8*i+6,0] = xs.i2 153 | inds[8*i+7,0] = xs.i2 154 | 155 | inds[8*i ,1] = ys.i1 156 | inds[8*i+1,1] = ys.i2 157 | inds[8*i+2,1] = ys.i1 158 | inds[8*i+3,1] = ys.i2 159 | inds[8*i+4,1] = ys.i1 160 | inds[8*i+5,1] = ys.i2 161 | inds[8*i+6,1] = ys.i1 162 | inds[8*i+7,1] = ys.i2 163 | 164 | inds[8*i ,2] = zs.i1 165 | inds[8*i+1,2] = zs.i1 166 | inds[8*i+2,2] = zs.i1 167 | inds[8*i+3,2] = zs.i1 168 | inds[8*i+4,2] = zs.i2 169 | inds[8*i+5,2] = zs.i2 170 | inds[8*i+6,2] = zs.i2 171 | inds[8*i+7,2] = zs.i2 172 | 173 | vals[8*i ] = xs.w1*ys.w1*zs.w1 174 | vals[8*i+1] = xs.w1*ys.w2*zs.w1 175 | vals[8*i+2] = xs.w2*ys.w1*zs.w1 176 | vals[8*i+3] = xs.w2*ys.w2*zs.w1 177 | vals[8*i+4] = xs.w1*ys.w1*zs.w2 178 | vals[8*i+5] = xs.w1*ys.w2*zs.w2 179 | vals[8*i+6] = xs.w2*ys.w1*zs.w2 180 | vals[8*i+7] = xs.w2*ys.w2*zs.w2 181 | 182 | return inds,vals 183 | 184 | @cython.boundscheck(False) 185 | @cython.cdivision(True) 186 | def _tensor_volume_averaging(mesh_in, mesh_out, values=None, output=None): 187 | 188 | cdef np.int32_t[:] i1_in, i1_out, i2_in, i2_out, i3_in, i3_out 189 | cdef np.float64_t[:] w1, w2, w3 190 | w1 = np.array([1.0], dtype=np.float64) 191 | w2 = np.array([1.0], dtype=np.float64) 192 | w3 = np.array([1.0], dtype=np.float64) 193 | i1_in = np.array([0], dtype=np.int32) 194 | i1_out = np.array([0], dtype=np.int32) 195 | i2_in = np.array([0], dtype=np.int32) 196 | i2_out = np.array([0], dtype=np.int32) 197 | i3_in = np.array([0], dtype=np.int32) 198 | i3_out = np.array([0], dtype=np.int32) 199 | cdef int dim = mesh_in.dim 200 | w1, i1_in, i1_out = _volume_avg_weights(mesh_in.nodes_x, mesh_out.nodes_x) 201 | if dim > 1: 202 | w2, i2_in, i2_out = _volume_avg_weights(mesh_in.nodes_y, mesh_out.nodes_y) 203 | if dim > 2: 204 | w3, i3_in, i3_out = _volume_avg_weights(mesh_in.nodes_z, mesh_out.nodes_z) 205 | 206 | cdef (np.int32_t, np.int32_t, np.int32_t) w_shape = (w1.shape[0], w2.shape[0], w3.shape[0]) 207 | cdef (np.int32_t, np.int32_t, np.int32_t) mesh_in_shape 208 | cdef (np.int32_t, np.int32_t, np.int32_t) mesh_out_shape 209 | 210 | nCv_in = [len(h) for h in mesh_in.h] 211 | nCv_out = [len(h) for h in mesh_out.h] 212 | if dim == 1: 213 | mesh_in_shape = (nCv_in[0], 1, 1) 214 | mesh_out_shape = (nCv_out[0], 1, 1) 215 | elif dim == 2: 216 | mesh_in_shape = (nCv_in[0], nCv_in[1], 1) 217 | mesh_out_shape = (nCv_out[0], nCv_out[1], 1) 218 | elif dim == 3: 219 | mesh_in_shape = (*nCv_in, ) 220 | mesh_out_shape = (*nCv_out, ) 221 | 222 | cdef np.float64_t[::1, :, :] val_in 223 | cdef np.float64_t[::1, :, :] val_out 224 | cdef int i1, i2, i3, i1i, i2i, i3i, i1o, i2o, i3o 225 | cdef np.float64_t w_3, w_32 226 | cdef np.float64_t[::1, :, :] vol = mesh_out.cell_volumes.reshape(mesh_out_shape, order='F').astype(np.float64) 227 | 228 | if values is not None: 229 | # If given a values array, do the operation 230 | val_in = values.reshape(mesh_in_shape, order='F').astype(np.float64) 231 | if output is None: 232 | output = np.zeros(mesh_out.n_cells, dtype=np.float64) 233 | else: 234 | output = np.require(output, dtype=np.float64, requirements=['A', 'W']) 235 | v_o = output.reshape(mesh_out_shape, order='F') 236 | v_o.fill(0) 237 | val_out = v_o 238 | for i3 in range(w_shape[2]): 239 | i3i = i3_in[i3] 240 | i3o = i3_out[i3] 241 | w_3 = w3[i3] 242 | for i2 in range(w_shape[1]): 243 | i2i = i2_in[i2] 244 | i2o = i2_out[i2] 245 | w_32 = w_3*w2[i2] 246 | for i1 in range(w_shape[0]): 247 | i1i = i1_in[i1] 248 | i1o = i1_out[i1] 249 | val_out[i1o, i2o, i3o] += w_32*w1[i1]*val_in[i1i, i2i, i3i]/vol[i1o, i2o, i3o] 250 | return output 251 | 252 | # Else, build and return a sparse matrix representing the operation 253 | i_i = np.empty(w_shape, dtype=np.int32, order='F') 254 | i_o = np.empty(w_shape, dtype=np.int32, order='F') 255 | ws = np.empty(w_shape, dtype=np.float64, order='F') 256 | cdef np.int32_t[::1,:,:] i_in = i_i 257 | cdef np.int32_t[::1,:,:] i_out = i_o 258 | cdef np.float64_t[::1, :, :] w = ws 259 | for i3 in range(w.shape[2]): 260 | i3i = i3_in[i3] 261 | i3o = i3_out[i3] 262 | w_3 = w3[i3] 263 | for i2 in range(w.shape[1]): 264 | i2i = i2_in[i2] 265 | i2o = i2_out[i2] 266 | w_32 = w_3*w2[i2] 267 | for i1 in range(w.shape[0]): 268 | i1i = i1_in[i1] 269 | i1o = i1_out[i1] 270 | w[i1, i2, i3] = w_32*w1[i1]/vol[i1o, i2o, i3o] 271 | i_in[i1, i2, i3] = (i3i*mesh_in_shape[1] + i2i)*mesh_in_shape[0] + i1i 272 | i_out[i1, i2, i3] = (i3o*mesh_out_shape[1] + i2o)*mesh_out_shape[0] + i1o 273 | ws = ws.reshape(-1, order='F') 274 | i_i = i_i.reshape(-1, order='F') 275 | i_o = i_o.reshape(-1, order='F') 276 | A = sp.csr_matrix((ws, (i_o, i_i)), shape=(mesh_out.nC, mesh_in.nC)) 277 | return A 278 | 279 | @cython.boundscheck(False) 280 | def _volume_avg_weights(np.float64_t[:] x1, np.float64_t[:] x2): 281 | cdef int n1 = x1.shape[0] 282 | cdef int n2 = x2.shape[0] 283 | cdef np.float64_t[:] xs = np.empty(n1 + n2) 284 | # Fill xs with uniques and truncate 285 | cdef int i1, i2, i, ii 286 | i1 = i2 = i = 0 287 | while i1x2[i2]: 293 | xs[i] = x2[i2] 294 | i2 += 1 295 | else: 296 | xs[i] = x1[i1] 297 | i1 += 1 298 | i2 += 1 299 | elif i1=x1[i1]: 322 | i1 += 1 323 | while i2=x2[i2]: 324 | i2 += 1 325 | _ix1[ii] = min(max(i1-1, 0), n1-1) 326 | _ix2[ii] = min(max(i2-1, 0), n2-1) 327 | ii += 1 328 | 329 | hs = hs[:ii] 330 | ix1 = ix1[:ii] 331 | ix2 = ix2[:ii] 332 | return hs, ix1, ix2 333 | -------------------------------------------------------------------------------- /discretize/mixins/omf_mod.py: -------------------------------------------------------------------------------- 1 | """Module for ``omf`` interaction with ``discretize``.""" 2 | 3 | import numpy as np 4 | import discretize 5 | 6 | 7 | def omf(): 8 | """Lazy loading omf.""" 9 | import omf 10 | 11 | return omf 12 | 13 | 14 | def _ravel_data_array(arr, nx, ny, nz): 15 | """Ravel an array from discretize ordering to omf ordering. 16 | 17 | Converts a 1D numpy array from ``discretize`` ordering (x, y, z) 18 | to a flattened 1D numpy array with ``OMF`` ordering (z, y, x) 19 | 20 | In ``discretize``, three-dimensional data are frequently organized within a 21 | 1D numpy array whose elements are ordered along the x-axis, then the y-axis, 22 | then the z-axis. **_ravel_data_array** converts the input array 23 | (discretize format) to a 1D numpy array ordered according to the open 24 | mining format; which is ordered along 25 | the z-axis, then the y-axis, then the x-axis. 26 | 27 | Parameters 28 | ---------- 29 | arr : numpy.ndarray 30 | A 1D vector or nD array ordered along the x, then y, then z axes 31 | nx : int 32 | Number of cells along the x-axis 33 | ny : int 34 | Number of cells along the y-axis 35 | nz : int 36 | Number of cells along the z-axis 37 | 38 | Returns 39 | ------- 40 | numpy.ndarray (n_cells) 41 | A flattened 1D array ordered according to the open mining format 42 | 43 | Examples 44 | -------- 45 | To demonstrate the reordering, we design a small 3D tensor mesh. 46 | We print a numpy array with the xyz locations of cell the centers using the 47 | original ordering (discretize). We then re-order the cell locations according to OMF. 48 | 49 | >>> from discretize import TensorMesh 50 | >>> import numpy as np 51 | 52 | >>> hx = np.ones(4) 53 | >>> hy = 2*np.ones(3) 54 | >>> hz = 3*np.ones(2) 55 | >>> mesh = TensorMesh([hx, hy, hz]) 56 | 57 | >>> dim = mesh.shape_cells[::-1] # OMF orderting 58 | >>> xc = np.reshape(mesh.cell_centers[:, 0], dim, order="C").ravel(order="F") 59 | >>> yc = np.reshape(mesh.cell_centers[:, 1], dim, order="C").ravel(order="F") 60 | >>> zc = np.reshape(mesh.cell_centers[:, 2], dim, order="C").ravel(order="F") 61 | 62 | >>> mesh.cell_centers 63 | array([[0.5, 1. , 1.5], 64 | [1.5, 1. , 1.5], 65 | [2.5, 1. , 1.5], 66 | [3.5, 1. , 1.5], 67 | [0.5, 3. , 1.5], 68 | [1.5, 3. , 1.5], 69 | [2.5, 3. , 1.5], 70 | [3.5, 3. , 1.5], 71 | [0.5, 5. , 1.5], 72 | [1.5, 5. , 1.5], 73 | [2.5, 5. , 1.5], 74 | [3.5, 5. , 1.5], 75 | [0.5, 1. , 4.5], 76 | [1.5, 1. , 4.5], 77 | [2.5, 1. , 4.5], 78 | [3.5, 1. , 4.5], 79 | [0.5, 3. , 4.5], 80 | [1.5, 3. , 4.5], 81 | [2.5, 3. , 4.5], 82 | [3.5, 3. , 4.5], 83 | [0.5, 5. , 4.5], 84 | [1.5, 5. , 4.5], 85 | [2.5, 5. , 4.5], 86 | [3.5, 5. , 4.5]]) 87 | 88 | >>> np.c_[xc, yc, zc] 89 | array([[0.5, 1. , 1.5], 90 | [0.5, 1. , 4.5], 91 | [0.5, 3. , 1.5], 92 | [0.5, 3. , 4.5], 93 | [0.5, 5. , 1.5], 94 | [0.5, 5. , 4.5], 95 | [1.5, 1. , 1.5], 96 | [1.5, 1. , 4.5], 97 | [1.5, 3. , 1.5], 98 | [1.5, 3. , 4.5], 99 | [1.5, 5. , 1.5], 100 | [1.5, 5. , 4.5], 101 | [2.5, 1. , 1.5], 102 | [2.5, 1. , 4.5], 103 | [2.5, 3. , 1.5], 104 | [2.5, 3. , 4.5], 105 | [2.5, 5. , 1.5], 106 | [2.5, 5. , 4.5], 107 | [3.5, 1. , 1.5], 108 | [3.5, 1. , 4.5], 109 | [3.5, 3. , 1.5], 110 | [3.5, 3. , 4.5], 111 | [3.5, 5. , 1.5], 112 | [3.5, 5. , 4.5]]) 113 | """ 114 | dim = (nz, ny, nx) 115 | return np.reshape(arr, dim, order="C").ravel(order="F") 116 | 117 | 118 | def _unravel_data_array(arr, nx, ny, nz): 119 | """Unravel an array from omf ordering to discretize ordering. 120 | 121 | Converts a 1D numpy array from ``OMF`` ordering (z, y, x) 122 | to a flattened 1D numpy array with ``discretize`` ordering (x, y, z) 123 | 124 | In ``OMF``, three-dimensional data are organized within a 125 | 1D numpy array whose elements are ordered along the z-axis, then the y-axis, 126 | then the x-axis. **_unravel_data_array** converts the input array 127 | (OMF format) to a 1D numpy array ordered according to ``discretize``; 128 | which is ordered along the x-axis, then the y-axis, then the y-axis. 129 | 130 | Parameters 131 | ---------- 132 | arr : numpy.ndarray 133 | A 1D vector or nD array ordered along the z, then y, then x axes 134 | nx : int 135 | Number of cells along the x-axis 136 | ny : int 137 | Number of cells along the y-axis 138 | nz : int 139 | Number of cells along the z-axis 140 | 141 | Returns 142 | ------- 143 | (n_cells) numpy.ndarray 144 | A flattened 1D array ordered according to the discretize format 145 | 146 | """ 147 | dim = (nz, ny, nx) 148 | return np.reshape(arr, dim, order="F").ravel(order="C") 149 | 150 | 151 | class InterfaceOMF(object): 152 | """Convert between ``omf`` and ``discretize`` objects. 153 | 154 | The ``InterfaceOMF`` class was designed for easy conversion between 155 | ``discretize`` objects and `open mining format `__ (OMF) objects. 156 | Examples include: meshes, models and data arrays. 157 | """ 158 | 159 | def _tensor_mesh_to_omf(mesh, models=None): 160 | """Convert a TensorMesh to an omf object. 161 | 162 | Constructs an :class:`omf.VolumeElement` object of this tensor mesh and 163 | the given models as cell data of that grid. 164 | 165 | Parameters 166 | ---------- 167 | mesh : discretize.TensorMesh 168 | The tensor mesh to convert to a :class:`omf.VolumeElement` 169 | models : dict(numpy.ndarray) 170 | Name('s) and array('s). Match number of cells 171 | """ 172 | if models is None: 173 | models = {} 174 | # Make the geometry 175 | geometry = omf().VolumeGridGeometry() 176 | # Set tensors 177 | tensors = mesh.h 178 | if len(tensors) < 1: 179 | raise RuntimeError( 180 | "Your mesh is empty... fill it out before converting to OMF" 181 | ) 182 | elif len(tensors) == 1: 183 | geometry.tensor_u = tensors[0] 184 | geometry.tensor_v = np.array( 185 | [ 186 | 0.0, 187 | ] 188 | ) 189 | geometry.tensor_w = np.array( 190 | [ 191 | 0.0, 192 | ] 193 | ) 194 | elif len(tensors) == 2: 195 | geometry.tensor_u = tensors[0] 196 | geometry.tensor_v = tensors[1] 197 | geometry.tensor_w = np.array( 198 | [ 199 | 0.0, 200 | ] 201 | ) 202 | elif len(tensors) == 3: 203 | geometry.tensor_u = tensors[0] 204 | geometry.tensor_v = tensors[1] 205 | geometry.tensor_w = tensors[2] 206 | else: 207 | raise RuntimeError("This mesh is too high-dimensional for OMF") 208 | # Set rotation axes 209 | orientation = mesh.orientation 210 | geometry.axis_u = orientation[0] 211 | geometry.axis_v = orientation[1] 212 | geometry.axis_w = orientation[2] 213 | # Set the origin 214 | geometry.origin = mesh.origin 215 | # Make sure the geometry is built correctly 216 | geometry.validate() 217 | # Make the volume element (the OMF object) 218 | omfmesh = omf().VolumeElement( 219 | geometry=geometry, 220 | ) 221 | # Add model data arrays onto the cells of the mesh 222 | omfmesh.data = [] 223 | for name, arr in models.items(): 224 | data = omf().ScalarData( 225 | name=name, 226 | array=_ravel_data_array(arr, *mesh.shape_cells), 227 | location="cells", 228 | ) 229 | omfmesh.data.append(data) 230 | # Validate to make sure a proper OMF object is returned to the user 231 | omfmesh.validate() 232 | return omfmesh 233 | 234 | def _tree_mesh_to_omf(mesh, models=None): 235 | raise NotImplementedError("Not possible until OMF v2 is released.") 236 | 237 | def _curvilinear_mesh_to_omf(mesh, models=None): 238 | raise NotImplementedError("Not currently possible.") 239 | 240 | def _cyl_mesh_to_omf(mesh, models=None): 241 | raise NotImplementedError("Not currently possible.") 242 | 243 | def to_omf(mesh, models=None): 244 | """Convert to an ``omf`` data object. 245 | 246 | Convert this mesh object to its proper ``omf`` data object with 247 | the given model dictionary as the cell data of that dataset. 248 | 249 | Parameters 250 | ---------- 251 | models : dict of [str, (n_cells) numpy.ndarray], optional 252 | Name('s) and array('s). 253 | 254 | Returns 255 | ------- 256 | omf.volume.VolumeElement 257 | """ 258 | # TODO: mesh.validate() 259 | converters = { 260 | # TODO: 'tree' : InterfaceOMF._tree_mesh_to_omf, 261 | "tensor": InterfaceOMF._tensor_mesh_to_omf, 262 | # TODO: 'curv' : InterfaceOMF._curvilinear_mesh_to_omf, 263 | # TODO: 'CylindricalMesh' : InterfaceOMF._cyl_mesh_to_omf, 264 | } 265 | key = mesh._meshType.lower() 266 | try: 267 | convert = converters[key] 268 | except KeyError: 269 | raise RuntimeError( 270 | "Mesh type `{}` is not currently supported for OMF conversion.".format( 271 | key 272 | ) 273 | ) 274 | # Convert the data object 275 | return convert(mesh, models=models) 276 | 277 | @staticmethod 278 | def _omf_volume_to_tensor(element): 279 | """Convert an :class:`omf.VolumeElement` to :class:`discretize.TensorMesh`.""" 280 | geometry = element.geometry 281 | h = [geometry.tensor_u, geometry.tensor_v, geometry.tensor_w] 282 | orientation = np.array( 283 | [ 284 | geometry.axis_u, 285 | geometry.axis_v, 286 | geometry.axis_w, 287 | ] 288 | ) 289 | mesh = discretize.TensorMesh(h, origin=geometry.origin, orientation=orientation) 290 | 291 | data_dict = {} 292 | for data in element.data: 293 | # NOTE: this is agnostic about data location - i.e. nodes vs cells 294 | data_dict[data.name] = _unravel_data_array( 295 | np.array(data.array), *mesh.shape_cells 296 | ) 297 | 298 | # Return TensorMesh and data dictionary 299 | return mesh, data_dict 300 | 301 | @staticmethod 302 | def from_omf(element): 303 | """Convert an ``omf`` object to a ``discretize`` mesh. 304 | 305 | Convert an OMF element to it's proper ``discretize`` type. 306 | Automatically determines the output type. Returns both the mesh and a 307 | dictionary of model arrays. 308 | 309 | Parameters 310 | ---------- 311 | element : omf.volume.VolumeElement 312 | The open mining format volume element object 313 | 314 | Returns 315 | ------- 316 | mesh : discretize.TensorMesh 317 | The returned mesh type will be appropriately based on the input `element`. 318 | models : dict of [str, (n_cells) numpy.ndarray] 319 | The models contained in `element` 320 | 321 | Notes 322 | ----- 323 | Currently only :class:discretize.TensorMesh is supported. 324 | """ 325 | element.validate() 326 | converters = { 327 | omf().VolumeElement.__name__: InterfaceOMF._omf_volume_to_tensor, 328 | } 329 | key = element.__class__.__name__ 330 | try: 331 | convert = converters[key] 332 | except KeyError: 333 | raise RuntimeError( 334 | "OMF type `{}` is not currently supported for conversion.".format(key) 335 | ) 336 | # Convert the data object 337 | return convert(element) 338 | -------------------------------------------------------------------------------- /discretize/utils/code_utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for common operations within code.""" 2 | 3 | import numpy as np 4 | import warnings 5 | 6 | SCALARTYPES = (complex, float, int, np.number) 7 | 8 | 9 | def is_scalar(f): 10 | """Determine if the input argument is a scalar. 11 | 12 | The function **is_scalar** returns *True* if the input is an integer, 13 | float or complex number. The function returns *False* otherwise. 14 | 15 | Parameters 16 | ---------- 17 | f : object 18 | Any input quantity 19 | 20 | Returns 21 | ------- 22 | bool 23 | - *True* if the input argument is an integer, float or complex number 24 | - *False* otherwise 25 | """ 26 | if isinstance(f, SCALARTYPES): 27 | return True 28 | elif ( 29 | isinstance(f, np.ndarray) 30 | and f.size == 1 31 | and isinstance(f.reshape(-1)[0], SCALARTYPES) 32 | ): 33 | return True 34 | return False 35 | 36 | 37 | def as_array_n_by_dim(pts, dim): 38 | """Coerce the given array to have *dim* columns. 39 | 40 | The function **as_array_n_by_dim** will examine the *pts* array, 41 | and coerce it to be at least if the number of columns is equal to *dim*. 42 | 43 | This is similar to the :func:`numpy.atleast_2d`, except that it ensures that then 44 | input has *dim* columns, and it appends a :data:`numpy.newaxis` to 1D arrays 45 | instead of prepending. 46 | 47 | Parameters 48 | ---------- 49 | pts : array_like 50 | array to check. 51 | dim : int 52 | The number of columns which *pts* should have 53 | 54 | Returns 55 | ------- 56 | (n_pts, dim) numpy.ndarray 57 | verified array 58 | """ 59 | pts = np.asarray(pts) 60 | 61 | if dim > 1: 62 | pts = np.atleast_2d(pts) 63 | elif len(pts.shape) == 1: 64 | pts = pts[:, np.newaxis] 65 | 66 | if pts.shape[1] != dim: 67 | raise ValueError( 68 | "pts must be a column vector of shape (nPts, {0:d}) not ({1:d}, {2:d})".format( 69 | *((dim,) + pts.shape) 70 | ) 71 | ) 72 | 73 | return pts 74 | 75 | 76 | def requires(modules): 77 | """Decorate a function with soft dependencies. 78 | 79 | This function was inspired by the `requires` function of pysal, 80 | which is released under the 'BSD 3-Clause "New" or "Revised" License'. 81 | 82 | https://github.com/pysal/pysal/blob/master/pysal/lib/common.py 83 | 84 | Parameters 85 | ---------- 86 | modules : dict 87 | Dictionary containing soft dependencies, e.g., 88 | {'matplotlib': matplotlib}. 89 | 90 | Returns 91 | ------- 92 | decorated_function : function 93 | Original function if all soft dependencies are met, otherwise 94 | it returns an empty function which prints why it is not running. 95 | 96 | """ 97 | # Check the required modules, add missing ones in the list `missing`. 98 | missing = [] 99 | for key, item in modules.items(): 100 | if item is False: 101 | missing.append(key) 102 | 103 | def decorated_function(function): 104 | """Wrap function.""" 105 | if not missing: 106 | return function 107 | else: 108 | 109 | def passer(*args, **kwargs): 110 | print(("Missing dependencies: {d}.".format(d=missing))) 111 | print(("Not running `{}`.".format(function.__name__))) 112 | 113 | return passer 114 | 115 | return decorated_function 116 | 117 | 118 | def deprecate_class( 119 | removal_version=None, new_location=None, future_warn=False, error=False 120 | ): 121 | """Decorate a class as deprecated. 122 | 123 | Parameters 124 | ---------- 125 | removal_version : str, optional 126 | Which version the class will be removed in. 127 | new_location : str, optional 128 | A new package location for the class. 129 | future_warn : bool, optional 130 | Whether to issue a FutureWarning, or a DeprecationWarning. 131 | error : bool, optional 132 | Throw error if deprecated class no longer implemented 133 | """ 134 | if future_warn: 135 | warn = FutureWarning 136 | else: 137 | warn = DeprecationWarning 138 | 139 | def decorator(cls): 140 | my_name = cls.__name__ 141 | parent_name = cls.__bases__[0].__name__ 142 | message = f"{my_name} has been deprecated, please use {parent_name}." 143 | if error: 144 | message = f"{my_name} has been removed, please use {parent_name}." 145 | elif removal_version is not None: 146 | message += ( 147 | f" It will be removed in version {removal_version} of discretize." 148 | ) 149 | else: 150 | message += " It will be removed in a future version of discretize." 151 | 152 | # stash the original initialization of the class 153 | cls._old__init__ = cls.__init__ 154 | 155 | def __init__(self, *args, **kwargs): 156 | if error: 157 | raise NotImplementedError(message) 158 | else: 159 | warnings.warn(message, warn, stacklevel=2) 160 | self._old__init__(*args, **kwargs) 161 | 162 | cls.__init__ = __init__ 163 | if new_location is not None: 164 | parent_name = f"{new_location}.{parent_name}" 165 | cls.__doc__ = f""" This class has been deprecated, see `{parent_name}` for documentation""" 166 | return cls 167 | 168 | return decorator 169 | 170 | 171 | def deprecate_module( 172 | old_name, new_name, removal_version=None, future_warn=False, error=False 173 | ): 174 | """Deprecate a module. 175 | 176 | Parameters 177 | ---------- 178 | old_name : str 179 | The old name of the module. 180 | new_name : str 181 | The new name of the module. 182 | removal_version : str, optional 183 | Which version the class will be removed in. 184 | future_warn : bool, optional 185 | Whether to issue a FutureWarning, or a DeprecationWarning. 186 | error : bool, default: ``False`` 187 | Throw error if deprecated module no longer implemented 188 | """ 189 | if future_warn: 190 | warn = FutureWarning 191 | else: 192 | warn = DeprecationWarning 193 | message = f"The {old_name} module has been deprecated, please use {new_name}." 194 | if error: 195 | message = f"{old_name} has been removed, please use {new_name}." 196 | elif removal_version is not None: 197 | message += f" It will be removed in version {removal_version} of discretize" 198 | else: 199 | message += " It will be removed in a future version of discretize." 200 | message += " Please update your code accordingly." 201 | if error: 202 | raise NotImplementedError(message) 203 | else: 204 | warnings.warn(message, warn, stacklevel=2) 205 | 206 | 207 | def deprecate_property( 208 | new_name, old_name, removal_version=None, future_warn=False, error=False 209 | ): 210 | """Deprecate a class property. 211 | 212 | Parameters 213 | ---------- 214 | new_name : str 215 | The new name of the property. 216 | old_name : str 217 | The old name of the property. 218 | removal_version : str, optional 219 | Which version the class will be removed in. 220 | future_warn : bool, optional 221 | Whether to issue a FutureWarning, or a DeprecationWarning. 222 | error : bool, default: ``False`` 223 | Throw error if deprecated property no longer implemented 224 | 225 | """ 226 | if future_warn: 227 | warn = FutureWarning 228 | else: 229 | warn = DeprecationWarning 230 | if error: 231 | tag = "" 232 | elif removal_version is not None: 233 | tag = f" It will be removed in version {removal_version} of discretize." 234 | else: 235 | tag = " It will be removed in a future version of discretize." 236 | 237 | def get_dep(self): 238 | class_name = type(self).__name__ 239 | message = ( 240 | f"{class_name}.{old_name} has been deprecated, please use {class_name}.{new_name}." 241 | + tag 242 | ) 243 | if error: 244 | raise NotImplementedError(message.replace("deprecated", "removed")) 245 | else: 246 | warnings.warn(message, warn, stacklevel=2) 247 | return getattr(self, new_name) 248 | 249 | def set_dep(self, other): 250 | class_name = type(self).__name__ 251 | message = ( 252 | f"{class_name}.{old_name} has been deprecated, please use {class_name}.{new_name}." 253 | + tag 254 | ) 255 | if error: 256 | raise NotImplementedError(message.replace("deprecated", "removed")) 257 | else: 258 | warnings.warn(message, warn, stacklevel=2) 259 | setattr(self, new_name, other) 260 | 261 | doc = f""" 262 | `{old_name}` has been deprecated. See `{new_name}` for documentation. 263 | 264 | See Also 265 | -------- 266 | {new_name} 267 | """ 268 | 269 | return property(get_dep, set_dep, None, doc) 270 | 271 | 272 | def deprecate_method( 273 | new_name, old_name, removal_version=None, future_warn=False, error=False 274 | ): 275 | """Deprecate a class method. 276 | 277 | Parameters 278 | ---------- 279 | new_name : str 280 | The new name of the method. 281 | old_name : str 282 | The old name of the method. 283 | removal_version : str, optional 284 | Which version the class will be removed in. 285 | future_warn : bool, optional 286 | Whether to issue a FutureWarning, or a DeprecationWarning. 287 | error : bool, default: ``False`` 288 | Throw error if deprecated method no longer implemented 289 | """ 290 | if future_warn: 291 | warn = FutureWarning 292 | else: 293 | warn = DeprecationWarning 294 | if error: 295 | tag = "" 296 | elif removal_version is not None: 297 | tag = f" It will be removed in version {removal_version} of discretize." 298 | else: 299 | tag = " It will be removed in a future version of discretize." 300 | 301 | def new_method(self, *args, **kwargs): 302 | class_name = type(self).__name__ 303 | message = ( 304 | f"{class_name}.{old_name} has been deprecated, please use " 305 | f"{class_name}.{new_name}.{tag}" 306 | ) 307 | if error: 308 | raise NotImplementedError(message.replace("deprecated", "removed")) 309 | else: 310 | warnings.warn( 311 | message, 312 | warn, 313 | stacklevel=2, 314 | ) 315 | return getattr(self, new_name)(*args, **kwargs) 316 | 317 | doc = f""" 318 | `{old_name}` has been deprecated. See `{new_name}` for documentation 319 | 320 | See Also 321 | -------- 322 | {new_name} 323 | """ 324 | if error: 325 | doc = doc.replace("deprecated", "removed") 326 | new_method.__doc__ = doc 327 | return new_method 328 | 329 | 330 | def deprecate_function( 331 | new_function, old_name, removal_version=None, future_warn=False, error=False 332 | ): 333 | """Deprecate a function. 334 | 335 | Parameters 336 | ---------- 337 | new_function : callable 338 | The new function. 339 | old_name : str 340 | The old name of the function. 341 | removal_version : str, optional 342 | Which version the class will be removed in. 343 | future_warn : bool, optional 344 | Whether to issue a FutureWarning, or a DeprecationWarning. 345 | error : bool, default: ``False`` 346 | Throw error if deprecated function no longer implemented 347 | """ 348 | if future_warn: 349 | warn = FutureWarning 350 | else: 351 | warn = DeprecationWarning 352 | new_name = new_function.__name__ 353 | if error: 354 | tag = "" 355 | elif removal_version is not None: 356 | tag = f" It will be removed in version {removal_version} of discretize." 357 | else: 358 | tag = " It will be removed in a future version of discretize." 359 | 360 | message = f"{old_name} has been deprecated, please use {new_name}." + tag 361 | 362 | def dep_function(*args, **kwargs): 363 | if error: 364 | raise NotImplementedError(message.replace("deprecated", "removed")) 365 | else: 366 | warnings.warn( 367 | message, 368 | warn, 369 | stacklevel=2, 370 | ) 371 | return new_function(*args, **kwargs) 372 | 373 | doc = f""" 374 | `{old_name}` has been deprecated. See `{new_name}` for documentation 375 | 376 | See Also 377 | -------- 378 | {new_name} 379 | """ 380 | if error: 381 | doc = doc.replace("deprecated", "removed") 382 | dep_function.__doc__ = doc 383 | return dep_function 384 | 385 | 386 | # DEPRECATIONS 387 | isScalar = deprecate_function( 388 | is_scalar, "isScalar", removal_version="1.0.0", error=True 389 | ) 390 | asArray_N_x_Dim = deprecate_function( 391 | as_array_n_by_dim, "asArray_N_x_Dim", removal_version="1.0.0", error=True 392 | ) 393 | -------------------------------------------------------------------------------- /discretize/_extensions/simplex_helpers.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language=c++ 2 | # cython: embedsignature=True, language_level=3 3 | # cython: linetrace=True 4 | # cython: freethreading_compatible = True 5 | 6 | from libcpp.pair cimport pair 7 | from libcpp.unordered_map cimport unordered_map 8 | from cython.operator cimport dereference 9 | cimport cython 10 | cimport numpy as np 11 | from cython cimport view 12 | from libc.math cimport sqrt 13 | 14 | cdef extern from "triplet.h": 15 | cdef cppclass triplet[T, U, V]: 16 | T v1 17 | U v2 18 | V v3 19 | triplet() 20 | triplet(T, U, V) 21 | import numpy as np 22 | 23 | ctypedef fused ints: 24 | size_t 25 | np.int32_t 26 | np.int64_t 27 | 28 | ctypedef fused pointers: 29 | size_t 30 | np.intp_t 31 | np.int32_t 32 | np.int64_t 33 | 34 | @cython.boundscheck(False) 35 | def _build_faces_edges(ints[:, :] simplices): 36 | # the node index in each simplex must be in increasing order 37 | cdef: 38 | int dim = simplices.shape[1] - 1 39 | ints n_simplex = simplices.shape[0] 40 | ints[:] simplex 41 | ints v1, v2, v3 42 | 43 | unordered_map[pair[ints, ints], ints] edges 44 | pair[ints, ints] edge 45 | ints n_edges = 0 46 | ints edges_per_simplex = 3 if dim==2 else 6 47 | ints[:, :] simplex_edges 48 | 49 | unordered_map[triplet[ints, ints, ints], ints] faces 50 | triplet[ints, ints, ints] face 51 | ints n_faces = 0 52 | ints faces_per_simplex = dim + 1 53 | ints[:, :] simplex_faces 54 | 55 | if ints is size_t: 56 | int_type = np.uintp 57 | elif ints is np.int32_t: 58 | int_type = np.int32 59 | elif ints is np.int64_t: 60 | int_type = np.int64 61 | 62 | simplex_edges = np.empty((n_simplex, edges_per_simplex), dtype=int_type) 63 | 64 | if dim == 3: 65 | simplex_faces = np.empty((n_simplex, faces_per_simplex), dtype=int_type) 66 | else: 67 | simplex_faces = simplex_edges 68 | 69 | cdef ints[:,:] edge_pairs = np.array( 70 | [[1, 2], [0, 2], [0, 1], [0, 3], [1, 3], [2, 3]], 71 | dtype=int_type 72 | ) 73 | 74 | for i_simp in range(n_simplex): 75 | simplex = simplices[i_simp] 76 | 77 | # build edges 78 | for i_edge in range(edges_per_simplex): 79 | 80 | v1 = simplex[edge_pairs[i_edge, 0]] 81 | v2 = simplex[edge_pairs[i_edge, 1]] 82 | edge = pair[ints, ints](v1, v2) 83 | 84 | edge_search = edges.find(edge) 85 | if edge_search != edges.end(): 86 | ind = dereference(edge_search).second 87 | else: 88 | ind = n_edges 89 | edges[edge] = ind 90 | n_edges += 1 91 | simplex_edges[i_simp, i_edge] = ind 92 | 93 | # build faces in 3D 94 | if dim == 3: 95 | for i_face in range(4): 96 | if i_face == 0: 97 | v1 = simplex[1] 98 | v2 = simplex[2] 99 | v3 = simplex[3] 100 | elif i_face == 1: 101 | v1 = simplex[0] 102 | v2 = simplex[2] 103 | v3 = simplex[3] 104 | elif i_face == 2: 105 | v1 = simplex[0] 106 | v2 = simplex[1] 107 | v3 = simplex[3] 108 | else: 109 | v1 = simplex[0] 110 | v2 = simplex[1] 111 | v3 = simplex[2] 112 | face = triplet[ints, ints, ints](v1, v2, v3) 113 | 114 | face_search = faces.find(face) 115 | if face_search != faces.end(): 116 | ind = dereference(face_search).second 117 | else: 118 | ind = n_faces 119 | faces[face] = ind 120 | n_faces += 1 121 | simplex_faces[i_simp, i_face] = ind 122 | 123 | cdef ints[:, :] _edges = np.empty((n_edges, 2), dtype=int_type) 124 | for edge_it in edges: 125 | _edges[edge_it.second, 0] = edge_it.first.first 126 | _edges[edge_it.second, 1] = edge_it.first.second 127 | 128 | cdef ints[:, :] _faces 129 | if dim == 3: 130 | _faces = np.empty((n_faces, 3), dtype=int_type) 131 | for face_it in faces: 132 | _faces[face_it.second, 0] = face_it.first.v1 133 | _faces[face_it.second, 1] = face_it.first.v2 134 | _faces[face_it.second, 2] = face_it.first.v3 135 | else: 136 | _faces = _edges 137 | 138 | cdef ints[:, :] face_edges 139 | cdef ints[:] _face 140 | 141 | if dim == 3: 142 | face_edges = np.empty((n_faces, 3), dtype=int_type) 143 | for i_face in range(n_faces): 144 | _face = _faces[i_face] 145 | # get indices of each edge in the face 146 | # 3 edges per face 147 | for i_edge in range(3): 148 | if i_edge == 0: 149 | v1 = _face[1] 150 | v2 = _face[2] 151 | elif i_edge == 1: 152 | v1 = _face[0] 153 | v2 = _face[2] 154 | elif i_edge == 2: 155 | v1 = _face[0] 156 | v2 = _face[1] 157 | # because of how faces were constructed, v1 < v2 always 158 | edge = pair[ints, ints](v1, v2) 159 | ind = edges[edge] 160 | face_edges[i_face, i_edge] = ind 161 | else: 162 | face_edges = np.empty((1, 1), dtype=int_type) 163 | 164 | return simplex_faces, _faces, simplex_edges, _edges, face_edges 165 | 166 | @cython.boundscheck(False) 167 | def _build_adjacency(ints[:, :] simplex_faces, n_faces): 168 | cdef: 169 | size_t n_cells = simplex_faces.shape[0] 170 | int dim = simplex_faces.shape[1] - 1 171 | np.int64_t[:, :] neighbors 172 | np.int64_t[:] visited 173 | ints[:] simplex 174 | 175 | ints i_cell, j, k, i_face, i_other 176 | 177 | if ints is size_t: 178 | int_type = np.uintp 179 | elif ints is np.int32_t: 180 | int_type = np.int32 181 | elif ints is np.int64_t: 182 | int_type = np.int64 183 | 184 | neighbors = np.full((n_cells, dim + 1), -1, dtype=np.int64) 185 | visited = np.full((n_faces), -1, dtype=np.int64) 186 | 187 | for i_cell in range(n_cells): 188 | simplex = simplex_faces[i_cell] 189 | for j in range(dim + 1): 190 | i_face = simplex[j] 191 | i_other = visited[i_face] 192 | if i_other == -1: 193 | visited[i_face] = i_cell 194 | else: 195 | neighbors[i_cell, j] = i_other 196 | k = 0 197 | while (k < dim + 1) and (simplex_faces[i_other, k] != i_face): 198 | k += 1 199 | neighbors[i_other, k] = i_cell 200 | return neighbors 201 | 202 | @cython.boundscheck(False) 203 | @cython.linetrace(False) 204 | cdef void _compute_bary_coords( 205 | np.float64_t[:] point, 206 | np.float64_t[:, :] Tinv, 207 | np.float64_t[:] shift, 208 | np.float64_t * bary 209 | ) nogil: 210 | cdef: 211 | int dim = point.shape[0] 212 | int i, j 213 | 214 | bary[dim] = 1.0 215 | for i in range(dim): 216 | bary[i] = 0.0 217 | for j in range(dim): 218 | bary[i] += Tinv[i, j] * (point[j] - shift[j]) 219 | bary[dim] -= bary[i] 220 | 221 | @cython.boundscheck(False) 222 | def _directed_search( 223 | np.float64_t[:, :] locs, 224 | pointers[:] nearest_cc, 225 | np.float64_t[:, :] nodes, 226 | ints[:, :] simplex_nodes, 227 | np.int64_t[:, :] neighbors, 228 | np.float64_t[:, :, :] transform, 229 | np.float64_t[:, :] shift, 230 | np.float64_t eps=1E-15, 231 | bint zeros_outside=False, 232 | bint return_bary=True 233 | ): 234 | cdef: 235 | int i, j 236 | pointers i_simp 237 | int n_locs = locs.shape[0], dim = locs.shape[1] 238 | int max_directed = 1 + simplex_nodes.shape[0] // 4 239 | int i_directed 240 | bint is_inside 241 | np.int64_t[:] inds = np.full(len(locs), -1, dtype=np.int64) 242 | np.float64_t[:, :] all_barys = np.empty((1, 1), dtype=np.float64) 243 | np.float64_t barys[4] 244 | np.float64_t[:] loc 245 | np.float64_t[:, :] Tinv 246 | np.float64_t[:] rD 247 | if return_bary: 248 | all_barys = np.empty((len(locs), dim+1), dtype=np.float64) 249 | 250 | for i in range(n_locs): 251 | loc = locs[i] 252 | 253 | i_simp = nearest_cc[i] # start at the nearest cell center 254 | i_directed = 0 255 | while i_directed < max_directed: 256 | Tinv = transform[i_simp] 257 | rD = shift[i_simp] 258 | _compute_bary_coords(loc, Tinv, rD, barys) 259 | j = 0 260 | is_inside = True 261 | while j <= dim: 262 | if barys[j] < -eps: 263 | is_inside = False 264 | # if not -1, move towards neighbor 265 | if neighbors[i_simp, j] != -1: 266 | i_simp = neighbors[i_simp, j] 267 | break 268 | j += 1 269 | # If inside, I found my container 270 | if is_inside: 271 | break 272 | # Else, if I cycled through every bary 273 | # without breaking out of the above loop, that means I'm completely outside 274 | elif j == dim + 1: 275 | if zeros_outside: 276 | i_simp = -1 277 | break 278 | i_directed += 1 279 | 280 | if i_directed == max_directed: 281 | # made it through the whole loop without breaking out 282 | # Mark as failed 283 | i_simp = -2 284 | inds[i] = i_simp 285 | if return_bary: 286 | for j in range(dim+1): 287 | all_barys[i, j] = barys[j] 288 | 289 | if return_bary: 290 | return np.array(inds), np.array(all_barys) 291 | return np.array(inds) 292 | 293 | @cython.boundscheck(False) 294 | @cython.cdivision(True) 295 | def _interp_cc( 296 | np.float64_t[:, :] locs, 297 | np.float64_t[:, :] cell_centers, 298 | np.float64_t[:] mat_data, 299 | ints[:] mat_indices, 300 | ints[:] mat_indptr, 301 | ): 302 | cdef: 303 | ints i, j, diff, start, stop, i_d 304 | ints n_max_per_row = 0 305 | ints[:] close_cells 306 | int dim = locs.shape[1] 307 | np.float64_t[:, :] drs 308 | np.float64_t[:] rs 309 | np.float64_t[:] rhs 310 | np.float64_t[:] lambs 311 | np.float64_t[:] weights 312 | np.float64_t[:] point 313 | np.float64_t[:] close_cell 314 | np.float64_t det, weight_sum 315 | np.float64_t xx, xy, xz, yy, yz, zz 316 | bint too_close 317 | np.float64_t eps = 1E-15 318 | # Find maximum number per row to pre-allocate a storage 319 | for i in range(locs.shape[0]): 320 | diff = mat_indptr[i+1] - mat_indptr[i] 321 | if diff > n_max_per_row: 322 | n_max_per_row = diff 323 | # 324 | drs = np.empty((n_max_per_row, dim), dtype=np.float64) 325 | rs = np.empty((n_max_per_row,), dtype=np.float64) 326 | rhs = np.empty((dim,),dtype=np.float64) 327 | lambs = np.empty((dim,),dtype=np.float64) 328 | 329 | for i in range(locs.shape[0]): 330 | point = locs[i] 331 | start = mat_indptr[i] 332 | stop = mat_indptr[i+1] 333 | diff = stop-start 334 | close_cells = mat_indices[start:stop] 335 | for j in range(diff): 336 | rs[j] = 0.0 337 | close_cell = cell_centers[close_cells[j]] 338 | for i_d in range(dim): 339 | drs[j, i_d] = close_cell[i_d] - point[i_d] 340 | rs[j] += drs[j, i_d]*drs[j, i_d] 341 | rs[j] = sqrt(rs[j]) 342 | weights = mat_data[start:stop] 343 | weights[:] = 0.0 344 | too_close = False 345 | i_d = 0 346 | for j in range(diff): 347 | if rs[j] < eps: 348 | too_close = True 349 | i_d = j 350 | if too_close: 351 | weights[i_d] = 1.0 352 | else: 353 | for j in range(diff): 354 | for i_d in range(dim): 355 | drs[j, i_d] /= rs[j] 356 | xx = xy = yy = 0.0 357 | rhs[:] = 0.0 358 | if dim == 2: 359 | for j in range(diff): 360 | xx += drs[j, 0] * drs[j, 0] 361 | xy += drs[j, 0] * drs[j, 1] 362 | yy += drs[j, 1] * drs[j, 1] 363 | rhs[0] -= drs[j, 0] 364 | rhs[1] -= drs[j, 1] 365 | det = xx * yy - xy * xy 366 | lambs[0] = (yy * rhs[0] - xy * rhs[1])/det 367 | lambs[1] = (-xy * rhs[0] + xx * rhs[1])/det 368 | 369 | if dim == 3: 370 | zz = xz = yz = 0.0 371 | for j in range(diff): 372 | xx += drs[j, 0] * drs[j, 0] 373 | xy += drs[j, 0] * drs[j, 1] 374 | yy += drs[j, 1] * drs[j, 1] 375 | xz += drs[j, 0] * drs[j, 2] 376 | yz += drs[j, 1] * drs[j, 2] 377 | zz += drs[j, 2] * drs[j, 2] 378 | rhs[0] -= drs[j, 0] 379 | rhs[1] -= drs[j, 1] 380 | rhs[2] -= drs[j, 2] 381 | 382 | det = ( 383 | xx * (yy * zz - yz * yz) 384 | + xy * (xz * yz - xy * zz) 385 | + xz * (xy * yz - xz * yy) 386 | ) 387 | lambs[0] = ( 388 | (yy * zz - yz * yz) * rhs[0] 389 | + (xz * yz - xy * zz) * rhs[1] 390 | + (xy * yz - xz * yy) * rhs[2] 391 | )/det 392 | lambs[1] = ( 393 | (xz * yz - xy * zz) * rhs[0] 394 | + (xx * zz - xz * xz) * rhs[1] 395 | + (xy * xz - xx * yz) * rhs[2] 396 | )/det 397 | lambs[2] = ( 398 | (xy * yz - xz * yy) * rhs[0] 399 | + (xz * xy - xx * yz) * rhs[1] 400 | + (xx * yy - xy * xy) * rhs[2] 401 | )/det 402 | 403 | weight_sum = 0.0 404 | for j in range(diff): 405 | weights[j] = 1.0 406 | for i_d in range(dim): 407 | weights[j] += lambs[i_d] * drs[j, i_d] 408 | weights[j] /= rs[j] 409 | weight_sum += weights[j] 410 | for j in range(diff): 411 | weights[j] /= weight_sum 412 | -------------------------------------------------------------------------------- /discretize/tensor_cell.py: -------------------------------------------------------------------------------- 1 | """Cell class for TensorMesh.""" 2 | 3 | import itertools 4 | import numpy as np 5 | 6 | 7 | class TensorCell: 8 | """ 9 | Representation of a cell in a TensorMesh. 10 | 11 | Parameters 12 | ---------- 13 | h : (dim) numpy.ndarray 14 | Array with the cell widths along each direction. For a 2D mesh, it 15 | must have two elements (``hx``, ``hy``). For a 3D mesh it must have 16 | three elements (``hx``, ``hy``, ``hz``). 17 | origin : (dim) numpy.ndarray 18 | Array with the coordinates of the origin of the cell, i.e. the 19 | bottom-left-frontmost corner. 20 | index_unraveled : (dim) tuple 21 | Array with the unraveled indices of the cell in its parent mesh. 22 | mesh_shape : (dim) tuple 23 | Shape of the parent mesh. 24 | 25 | Examples 26 | -------- 27 | Define a simple :class:`discretize.TensorMesh`. 28 | 29 | >>> from discretize import TensorMesh 30 | >>> mesh = TensorMesh([5, 8, 10]) 31 | 32 | We can obtain a particular cell in the mesh by its index: 33 | 34 | >>> cell = mesh[3] 35 | >>> cell 36 | TensorCell(h=[0.2 0.125 0.1 ], origin=[0.6 0. 0. ], index=3, mesh_shape=(5, 8, 10)) 37 | 38 | And then obtain information about it, like its 39 | :attr:`discretize.tensor_cell.TensorCell.origin`: 40 | 41 | >>> cell.origin 42 | array([0.6, 0. , 0. ]) 43 | 44 | Or its 45 | :attr:`discretize.tensor_cell.TensorCell.bounds`: 46 | 47 | >>> cell.bounds 48 | array([0.6 , 0.8 , 0. , 0.125, 0. , 0.1 ]) 49 | 50 | We can also get its neighboring cells: 51 | 52 | >>> neighbours = cell.get_neighbors(mesh) 53 | >>> for neighbor in neighbours: 54 | ... print(neighbor.center) 55 | [0.5 0.0625 0.05 ] 56 | [0.9 0.0625 0.05 ] 57 | [0.7 0.1875 0.05 ] 58 | [0.7 0.0625 0.15 ] 59 | 60 | 61 | Alternatively, we can iterate over all cells in the mesh with a simple 62 | *for loop* or list comprehension: 63 | 64 | >>> cells = [cell for cell in mesh] 65 | >>> len(cells) 66 | 400 67 | 68 | """ 69 | 70 | def __init__(self, h, origin, index_unraveled, mesh_shape): 71 | self._h = h 72 | self._origin = origin 73 | self._index_unraveled = index_unraveled 74 | self._mesh_shape = mesh_shape 75 | 76 | def __repr__(self): 77 | """Represent a TensorCell.""" 78 | attributes = ", ".join( 79 | [ 80 | f"{attr}={getattr(self, attr)}" 81 | for attr in ("h", "origin", "index", "mesh_shape") 82 | ] 83 | ) 84 | return f"TensorCell({attributes})" 85 | 86 | def __eq__(self, other): 87 | """Check if this cell is the same as other one.""" 88 | if not isinstance(other, TensorCell): 89 | raise TypeError( 90 | f"Cannot compare an object of type '{other.__class__.__name__}' " 91 | "with a TensorCell" 92 | ) 93 | are_equal = ( 94 | np.all(self.h == other.h) 95 | and np.all(self.origin == other.origin) 96 | and self.index == other.index 97 | and self.mesh_shape == other.mesh_shape 98 | ) 99 | return are_equal 100 | 101 | @property 102 | def h(self): 103 | """Cell widths.""" 104 | return self._h 105 | 106 | @property 107 | def origin(self): 108 | """Coordinates of the origin of the cell.""" 109 | return self._origin 110 | 111 | @property 112 | def index(self): 113 | """Index of the cell in a TensorMesh.""" 114 | return np.ravel_multi_index( 115 | self.index_unraveled, dims=self.mesh_shape, order="F" 116 | ) 117 | 118 | @property 119 | def index_unraveled(self): 120 | """Unraveled index of the cell in a TensorMesh.""" 121 | return self._index_unraveled 122 | 123 | @property 124 | def mesh_shape(self): 125 | """Shape of the parent mesh.""" 126 | return self._mesh_shape 127 | 128 | @property 129 | def dim(self): 130 | """Dimensions of the cell (1, 2 or 3).""" 131 | return len(self.h) 132 | 133 | @property 134 | def center(self): 135 | """ 136 | Coordinates of the cell center. 137 | 138 | Returns 139 | ------- 140 | center : (dim) array 141 | Array with the coordinates of the cell center. 142 | """ 143 | center = np.array(self.origin) + np.array(self.h) / 2 144 | return center 145 | 146 | @property 147 | def bounds(self): 148 | """ 149 | Bounds of the cell. 150 | 151 | Coordinates that define the bounds of the cell. Bounds are returned in 152 | the following order: ``x1``, ``x2``, ``y1``, ``y2``, ``z1``, ``z2``. 153 | 154 | Returns 155 | ------- 156 | bounds : (2 * dim) array 157 | Array with the cell bounds. 158 | """ 159 | bounds = np.array( 160 | [ 161 | origin_i + factor * h_i 162 | for origin_i, h_i in zip(self.origin, self.h) 163 | for factor in (0, 1) 164 | ] 165 | ) 166 | return bounds 167 | 168 | @property 169 | def neighbors(self): 170 | """ 171 | Indices for this cell's neighbors within its parent mesh. 172 | 173 | Returns 174 | ------- 175 | list of list of int 176 | """ 177 | neighbor_indices = [] 178 | for dim in range(self.dim): 179 | for delta in (-1, 1): 180 | index = list(self.index_unraveled) 181 | index[dim] += delta 182 | if 0 <= index[dim] < self._mesh_shape[dim]: 183 | neighbor_indices.append( 184 | np.ravel_multi_index(index, dims=self.mesh_shape, order="F") 185 | ) 186 | return neighbor_indices 187 | 188 | @property 189 | def nodes(self): 190 | """ 191 | Indices for this cell's nodes within its parent mesh. 192 | 193 | Returns 194 | ------- 195 | list of int 196 | """ 197 | # Define shape of nodes in parent mesh 198 | nodes_shape = [s + 1 for s in self.mesh_shape] 199 | # Get indices of nodes per dimension 200 | nodes_index_per_dim = [[index, index + 1] for index in self.index_unraveled] 201 | # Combine the nodes_index_per_dim using itertools.product. 202 | # Because we want to follow a FORTRAN order, we need to reverse the 203 | # order of the nodes_index_per_dim and the indices. 204 | nodes_indices = [i[::-1] for i in itertools.product(*nodes_index_per_dim[::-1])] 205 | # Ravel indices 206 | nodes_indices = [ 207 | np.ravel_multi_index(index, dims=nodes_shape, order="F") 208 | for index in nodes_indices 209 | ] 210 | return nodes_indices 211 | 212 | @property 213 | def edges(self): 214 | """ 215 | Indices for this cell's edges within its parent mesh. 216 | 217 | Returns 218 | ------- 219 | list of int 220 | """ 221 | if self.dim == 1: 222 | edges_indices = [self.index] 223 | elif self.dim == 2: 224 | # Get shape of edges grids (for edges_x and edges_y) 225 | edges_x_shape = [self.mesh_shape[0], self.mesh_shape[1] + 1] 226 | edges_y_shape = [self.mesh_shape[0] + 1, self.mesh_shape[1]] 227 | # Calculate total amount of edges_x 228 | n_edges_x = edges_x_shape[0] * edges_x_shape[1] 229 | # Get indices of edges_x 230 | edges_x_indices = [ 231 | [self.index_unraveled[0], self.index_unraveled[1] + delta] 232 | for delta in (0, 1) 233 | ] 234 | edges_x_indices = [ 235 | np.ravel_multi_index(index, dims=edges_x_shape, order="F") 236 | for index in edges_x_indices 237 | ] 238 | # Get indices of edges_y 239 | edges_y_indices = [ 240 | [self.index_unraveled[0] + delta, self.index_unraveled[1]] 241 | for delta in (0, 1) 242 | ] 243 | edges_y_indices = [ 244 | n_edges_x + np.ravel_multi_index(index, dims=edges_y_shape, order="F") 245 | for index in edges_y_indices 246 | ] 247 | edges_indices = edges_x_indices + edges_y_indices 248 | elif self.dim == 3: 249 | edges_x_shape = [ 250 | n if i == 0 else n + 1 for i, n in enumerate(self.mesh_shape) 251 | ] 252 | edges_y_shape = [ 253 | n if i == 1 else n + 1 for i, n in enumerate(self.mesh_shape) 254 | ] 255 | edges_z_shape = [ 256 | n if i == 2 else n + 1 for i, n in enumerate(self.mesh_shape) 257 | ] 258 | # Calculate total amount of edges_x and edges_y 259 | n_edges_x = edges_x_shape[0] * edges_x_shape[1] * edges_x_shape[2] 260 | n_edges_y = edges_y_shape[0] * edges_y_shape[1] * edges_y_shape[2] 261 | # Get indices of edges_x 262 | edges_x_indices = [ 263 | [ 264 | self.index_unraveled[0], 265 | self.index_unraveled[1] + delta_y, 266 | self.index_unraveled[2] + delta_z, 267 | ] 268 | for delta_z in (0, 1) 269 | for delta_y in (0, 1) 270 | ] 271 | edges_x_indices = [ 272 | np.ravel_multi_index(index, dims=edges_x_shape, order="F") 273 | for index in edges_x_indices 274 | ] 275 | # Get indices of edges_y 276 | edges_y_indices = [ 277 | [ 278 | self.index_unraveled[0] + delta_x, 279 | self.index_unraveled[1], 280 | self.index_unraveled[2] + delta_z, 281 | ] 282 | for delta_z in (0, 1) 283 | for delta_x in (0, 1) 284 | ] 285 | edges_y_indices = [ 286 | n_edges_x + np.ravel_multi_index(index, dims=edges_y_shape, order="F") 287 | for index in edges_y_indices 288 | ] 289 | # Get indices of edges_z 290 | edges_z_indices = [ 291 | [ 292 | self.index_unraveled[0] + delta_x, 293 | self.index_unraveled[1] + delta_y, 294 | self.index_unraveled[2], 295 | ] 296 | for delta_y in (0, 1) 297 | for delta_x in (0, 1) 298 | ] 299 | edges_z_indices = [ 300 | n_edges_x 301 | + n_edges_y 302 | + np.ravel_multi_index(index, dims=edges_z_shape, order="F") 303 | for index in edges_z_indices 304 | ] 305 | edges_indices = edges_x_indices + edges_y_indices + edges_z_indices 306 | return edges_indices 307 | 308 | @property 309 | def faces(self): 310 | """ 311 | Indices for cell's faces within its parent mesh. 312 | 313 | Returns 314 | ------- 315 | list of int 316 | """ 317 | if self.dim == 1: 318 | faces_indices = [self.index, self.index + 1] 319 | elif self.dim == 2: 320 | # Get shape of faces grids 321 | # (faces_x are normal to x and faces_y are normal to y) 322 | faces_x_shape = [self.mesh_shape[0] + 1, self.mesh_shape[1]] 323 | faces_y_shape = [self.mesh_shape[0], self.mesh_shape[1] + 1] 324 | # Calculate total amount of faces_x 325 | n_faces_x = faces_x_shape[0] * faces_x_shape[1] 326 | # Get indices of faces_x 327 | faces_x_indices = [ 328 | [self.index_unraveled[0] + delta, self.index_unraveled[1]] 329 | for delta in (0, 1) 330 | ] 331 | faces_x_indices = [ 332 | np.ravel_multi_index(index, dims=faces_x_shape, order="F") 333 | for index in faces_x_indices 334 | ] 335 | # Get indices of faces_y 336 | faces_y_indices = [ 337 | [self.index_unraveled[0], self.index_unraveled[1] + delta] 338 | for delta in (0, 1) 339 | ] 340 | faces_y_indices = [ 341 | n_faces_x + np.ravel_multi_index(index, dims=faces_y_shape, order="F") 342 | for index in faces_y_indices 343 | ] 344 | faces_indices = faces_x_indices + faces_y_indices 345 | elif self.dim == 3: 346 | # Get shape of faces grids 347 | faces_x_shape = [ 348 | n + 1 if i == 0 else n for i, n in enumerate(self.mesh_shape) 349 | ] 350 | faces_y_shape = [ 351 | n + 1 if i == 1 else n for i, n in enumerate(self.mesh_shape) 352 | ] 353 | faces_z_shape = [ 354 | n + 1 if i == 2 else n for i, n in enumerate(self.mesh_shape) 355 | ] 356 | # Calculate total amount of faces_x and faces_y 357 | n_faces_x = faces_x_shape[0] * faces_x_shape[1] * faces_x_shape[2] 358 | n_faces_y = faces_y_shape[0] * faces_y_shape[1] * faces_y_shape[2] 359 | # Get indices of faces_x 360 | faces_x_indices = [ 361 | [ 362 | self.index_unraveled[0] + delta, 363 | self.index_unraveled[1], 364 | self.index_unraveled[2], 365 | ] 366 | for delta in (0, 1) 367 | ] 368 | faces_x_indices = [ 369 | np.ravel_multi_index(index, dims=faces_x_shape, order="F") 370 | for index in faces_x_indices 371 | ] 372 | # Get indices of faces_y 373 | faces_y_indices = [ 374 | [ 375 | self.index_unraveled[0], 376 | self.index_unraveled[1] + delta, 377 | self.index_unraveled[2], 378 | ] 379 | for delta in (0, 1) 380 | ] 381 | faces_y_indices = [ 382 | n_faces_x + np.ravel_multi_index(index, dims=faces_y_shape, order="F") 383 | for index in faces_y_indices 384 | ] 385 | # Get indices of faces_z 386 | faces_z_indices = [ 387 | [ 388 | self.index_unraveled[0], 389 | self.index_unraveled[1], 390 | self.index_unraveled[2] + delta, 391 | ] 392 | for delta in (0, 1) 393 | ] 394 | faces_z_indices = [ 395 | n_faces_x 396 | + n_faces_y 397 | + np.ravel_multi_index(index, dims=faces_z_shape, order="F") 398 | for index in faces_z_indices 399 | ] 400 | faces_indices = faces_x_indices + faces_y_indices + faces_z_indices 401 | return faces_indices 402 | 403 | def get_neighbors(self, mesh): 404 | """ 405 | Return the neighboring cells in the mesh. 406 | 407 | Parameters 408 | ---------- 409 | mesh : TensorMesh 410 | TensorMesh where the current cell lives. 411 | 412 | Returns 413 | ------- 414 | list of TensorCell 415 | """ 416 | return [mesh[index] for index in self.neighbors] 417 | -------------------------------------------------------------------------------- /discretize/utils/curvilinear_utils.py: -------------------------------------------------------------------------------- 1 | """Functions for working with curvilinear meshes.""" 2 | 3 | import numpy as np 4 | from discretize.utils.matrix_utils import mkvc, ndgrid, sub2ind 5 | from discretize.utils.code_utils import deprecate_function 6 | 7 | 8 | def example_curvilinear_grid(nC, exType): 9 | """Create the gridded node locations for a curvilinear mesh. 10 | 11 | Parameters 12 | ---------- 13 | nC : list of int 14 | list of number of cells in each dimension. Must be length 2 or 3 15 | exType : {"rect", "rotate", "sphere"} 16 | String specifying the style of example curvilinear mesh. 17 | 18 | Returns 19 | ------- 20 | list of numpy.ndarray 21 | List containing the gridded x, y (and z) node locations for the 22 | curvilinear mesh. 23 | """ 24 | if not isinstance(nC, list): 25 | raise TypeError("nC must be a list containing the number of nodes") 26 | if len(nC) != 2 and len(nC) != 3: 27 | raise ValueError("nC must either two or three dimensions") 28 | exType = exType.lower() 29 | 30 | possibleTypes = ["rect", "rotate", "sphere"] 31 | if exType not in possibleTypes: 32 | raise TypeError("Not a possible example type.") 33 | 34 | if exType == "rect": 35 | return list( 36 | ndgrid([np.cumsum(np.r_[0, np.ones(nx) / nx]) for nx in nC], vector=False) 37 | ) 38 | elif exType == "sphere": 39 | nodes = list( 40 | ndgrid( 41 | [np.cumsum(np.r_[0, np.ones(nx) / nx]) - 0.5 for nx in nC], vector=False 42 | ) 43 | ) 44 | nodes = np.stack(nodes, axis=-1) 45 | nodes = 2 * nodes 46 | # L_inf distance to center 47 | r0 = np.linalg.norm(nodes, ord=np.inf, axis=-1) 48 | # L2 distance to center 49 | r2 = np.linalg.norm(nodes, axis=-1) 50 | r0[r0 == 0.0] = 1.0 51 | r2[r2 == 0.0] = 1.0 52 | scale = r0 / r2 53 | nodes = nodes * scale[..., None] 54 | nodes = np.transpose(nodes, (-1, *np.arange(len(nC)))) 55 | nodes = [node for node in nodes] # turn it into a list 56 | return nodes 57 | elif exType == "rotate": 58 | if len(nC) == 2: 59 | X, Y = ndgrid( 60 | [np.cumsum(np.r_[0, np.ones(nx) / nx]) for nx in nC], vector=False 61 | ) 62 | amt = 0.5 - np.sqrt((X - 0.5) ** 2 + (Y - 0.5) ** 2) 63 | amt[amt < 0] = 0 64 | return [X + (-(Y - 0.5)) * amt, Y + (+(X - 0.5)) * amt] 65 | elif len(nC) == 3: 66 | X, Y, Z = ndgrid( 67 | [np.cumsum(np.r_[0, np.ones(nx) / nx]) for nx in nC], vector=False 68 | ) 69 | amt = 0.5 - np.sqrt((X - 0.5) ** 2 + (Y - 0.5) ** 2 + (Z - 0.5) ** 2) 70 | amt[amt < 0] = 0 71 | return [ 72 | X + (-(Y - 0.5)) * amt, 73 | Y + (-(Z - 0.5)) * amt, 74 | Z + (-(X - 0.5)) * amt, 75 | ] 76 | 77 | 78 | def volume_tetrahedron(xyz, A, B, C, D): 79 | r"""Return the tetrahedron volumes for a specified set of verticies. 80 | 81 | Let *xyz* be an (n, 3) array denoting a set of vertex locations. 82 | Any 4 vertex locations *a, b, c* and *d* can be used to define a tetrahedron. 83 | For the set of tetrahedra whose verticies are indexed in vectors 84 | *A, B, C* and *D*, this function returns the corresponding volumes. 85 | See algorithm: https://en.wikipedia.org/wiki/Tetrahedron#Volume 86 | 87 | .. math:: 88 | vol = {1 \over 6} \big | ( \mathbf{a - d} ) \cdot 89 | ( ( \mathbf{b - d} ) \times ( \mathbf{c - d} ) ) \big | 90 | 91 | Parameters 92 | ---------- 93 | xyz : (n_pts, 3) numpy.ndarray 94 | x,y, and z locations for all verticies 95 | A : (n_tetra) numpy.ndarray of int 96 | Vector containing the indicies for the **a** vertex locations 97 | B : (n_tetra) numpy.ndarray of int 98 | Vector containing the indicies for the **b** vertex locations 99 | C : (n_tetra) numpy.ndarray of int 100 | Vector containing the indicies for the **c** vertex locations 101 | D : (n_tetra) numpy.ndarray of int 102 | Vector containing the indicies for the **d** vertex locations 103 | 104 | Returns 105 | ------- 106 | (n_tetra) numpy.ndarray 107 | Volumes of the tetrahedra whose vertices are indexed by 108 | *A, B, C* and *D*. 109 | 110 | Examples 111 | -------- 112 | Here we define a small 3D tensor mesh. 4 nodes are chosen to 113 | be the verticies of a tetrahedron. We compute the volume of this 114 | tetrahedron. Note that xyz locations for the verticies can be 115 | scattered and do not require regular spacing. 116 | 117 | >>> from discretize.utils import volume_tetrahedron 118 | >>> from discretize import TensorMesh 119 | >>> import numpy as np 120 | >>> import matplotlib.pyplot as plt 121 | >>> import matplotlib as mpl 122 | 123 | Define corners of a uniform cube 124 | 125 | >>> h = [1, 1] 126 | >>> mesh = TensorMesh([h, h, h]) 127 | >>> xyz = mesh.nodes 128 | 129 | Specify the indicies of the corner points 130 | 131 | >>> A = np.array([0]) 132 | >>> B = np.array([6]) 133 | >>> C = np.array([8]) 134 | >>> D = np.array([24]) 135 | 136 | Compute volume for all tetrahedra and the extract first one 137 | 138 | >>> vol = volume_tetrahedron(xyz, A, B, C, D) 139 | >>> vol = vol[0] 140 | >>> vol 141 | array([1.33333333]) 142 | 143 | Plotting small mesh and tetrahedron 144 | 145 | >>> fig = plt.figure(figsize=(7, 7)) 146 | >>> ax = plt.subplot(projection='3d') 147 | >>> mesh.plot_grid(ax=ax) 148 | >>> k = [0, 6, 8, 0, 24, 6, 24, 8] 149 | >>> xyz_tetra = xyz[k, :] 150 | >>> ax.plot(xyz_tetra[:, 0], xyz_tetra[:, 1], xyz_tetra[:, 2], 'r') 151 | >>> ax.text(-0.25, 0., 3., 'Volume of the tetrahedron: {:g} $m^3$'.format(vol)) 152 | >>> plt.show() 153 | """ 154 | AD = xyz[A, :] - xyz[D, :] 155 | BD = xyz[B, :] - xyz[D, :] 156 | CD = xyz[C, :] - xyz[D, :] 157 | 158 | V = ( 159 | (BD[:, 0] * CD[:, 1] - BD[:, 1] * CD[:, 0]) * AD[:, 2] 160 | - (BD[:, 0] * CD[:, 2] - BD[:, 2] * CD[:, 0]) * AD[:, 1] 161 | + (BD[:, 1] * CD[:, 2] - BD[:, 2] * CD[:, 1]) * AD[:, 0] 162 | ) 163 | return np.abs(V / 6) 164 | 165 | 166 | def index_cube(nodes, grid_shape, n=None): 167 | """Return the index of nodes on a tensor (or curvilinear) mesh. 168 | 169 | For 2D tensor meshes, each cell is defined by nodes 170 | *A, B, C* and *D*. And for 3D tensor meshes, each cell 171 | is defined by nodes *A* through *H* (see below). *index_cube* 172 | outputs the indices for the specified node(s) for all 173 | cells in the mesh. 174 | 175 | TWO DIMENSIONS:: 176 | 177 | node(i,j+1) node(i+i,j+1) 178 | B -------------- C 179 | | | 180 | | cell(i,j) | 181 | | I | 182 | | | 183 | A -------------- D 184 | node(i,j) node(i+1,j) 185 | 186 | THREE DIMENSIONS:: 187 | 188 | node(i,j+1,k+1) node(i+1,j+1,k+1) 189 | F ---------------- G 190 | /| / | 191 | / | / | 192 | / | / | 193 | node(i,j,k+1) node(i+1,j,k+1) 194 | E --------------- H | 195 | | B -----------|---- C 196 | | / cell(i,j,k) | / 197 | | / I | / 198 | | / | / 199 | A --------------- D 200 | node(i,j,k) node(i+1,j,k) 201 | 202 | Parameters 203 | ---------- 204 | nodes : str 205 | String specifying which nodes to return. For 2D meshes, 206 | *nodes* must be a string containing combinations of the characters 'A', 'B', 207 | 'C', or 'D'. For 3D meshes, *nodes* can also be 'E', 'F', 'G', or 'H'. Note that 208 | order is preserved. E.g. if we want to return the C, D and A node indices in 209 | that particular order, we input *nodes* = 'CDA'. 210 | grid_shape : list of int 211 | Number of nodes along the i,j,k directions; e.g. [ni,nj,nk] 212 | nc : list of int 213 | Number of cells along the i,j,k directions; e.g. [nci,ncj,nck] 214 | 215 | Returns 216 | ------- 217 | index : tuple of numpy.ndarray 218 | Each entry of the tuple is a 1D :class:`numpy.ndarray` containing the indices of 219 | the nodes specified in the input *nodes* in the order asked; 220 | e.g. if *nodes* = 'DCBA', the tuple returned is ordered (D,C,B,A). 221 | 222 | Examples 223 | -------- 224 | Here, we construct a small 2D tensor mesh 225 | (works for a curvilinear mesh as well) and use *index_cube* 226 | to find the indices of the 'A' and 'C' nodes. We then 227 | plot the mesh, as well as the 'A' and 'C' node locations. 228 | 229 | >>> from discretize import TensorMesh 230 | >>> from discretize.utils import index_cube 231 | >>> from matplotlib import pyplot as plt 232 | >>> import numpy as np 233 | 234 | Create a simple tensor mesh. 235 | 236 | >>> n_cells = 5 237 | >>> h = 2*np.ones(n_cells) 238 | >>> mesh = TensorMesh([h, h], x0='00') 239 | 240 | Get indices of 'A' and 'C' nodes for all cells. 241 | 242 | >>> A, C = index_cube('AC', [n_cells+1, n_cells+1]) 243 | 244 | Plot mesh and the locations of the A and C nodes 245 | 246 | >>> fig1 = plt.figure(figsize=(5, 5)) 247 | >>> ax1 = fig1.add_axes([0.1, 0.1, 0.8, 0.8]) 248 | >>> mesh.plot_grid(ax=ax1) 249 | >>> ax1.scatter(mesh.nodes[A, 0], mesh.nodes[A, 1], 100, 'r', marker='^') 250 | >>> ax1.scatter(mesh.nodes[C, 0], mesh.nodes[C, 1], 100, 'g', marker='v') 251 | >>> ax1.set_title('A nodes (red) and C nodes (green)') 252 | >>> plt.show() 253 | """ 254 | if not isinstance(nodes, str): 255 | raise TypeError("Nodes must be a str variable: e.g. 'ABCD'") 256 | nodes = nodes.upper() 257 | try: 258 | dim = len(grid_shape) 259 | if n is None: 260 | n = tuple(x - 1 for x in grid_shape) 261 | except TypeError: 262 | return TypeError("grid_shape must be iterable") 263 | # Make sure that we choose from the possible nodes. 264 | possibleNodes = "ABCD" if dim == 2 else "ABCDEFGH" 265 | for node in nodes: 266 | if node not in possibleNodes: 267 | raise ValueError("Nodes must be chosen from: '{0!s}'".format(possibleNodes)) 268 | 269 | if dim == 2: 270 | ij = ndgrid(np.arange(n[0]), np.arange(n[1])) 271 | i, j = ij[:, 0], ij[:, 1] 272 | elif dim == 3: 273 | ijk = ndgrid(np.arange(n[0]), np.arange(n[1]), np.arange(n[2])) 274 | i, j, k = ijk[:, 0], ijk[:, 1], ijk[:, 2] 275 | else: 276 | raise Exception("Only 2 and 3 dimensions supported.") 277 | 278 | nodeMap = { 279 | "A": [0, 0, 0], 280 | "B": [0, 1, 0], 281 | "C": [1, 1, 0], 282 | "D": [1, 0, 0], 283 | "E": [0, 0, 1], 284 | "F": [0, 1, 1], 285 | "G": [1, 1, 1], 286 | "H": [1, 0, 1], 287 | } 288 | out = () 289 | for node in nodes: 290 | shift = nodeMap[node] 291 | if dim == 2: 292 | out += (sub2ind(grid_shape, np.c_[i + shift[0], j + shift[1]]).flatten(),) 293 | elif dim == 3: 294 | out += ( 295 | sub2ind( 296 | grid_shape, np.c_[i + shift[0], j + shift[1], k + shift[2]] 297 | ).flatten(), 298 | ) 299 | 300 | return out 301 | 302 | 303 | def face_info(xyz, A, B, C, D, average=True, normalize_normals=True, **kwargs): 304 | r"""Return normal surface vectors and areas for a given set of faces. 305 | 306 | Let *xyz* be an (n, 3) array denoting a set of vertex locations. 307 | Now let vertex locations *a, b, c* and *d* define a quadrilateral 308 | (regular or irregular) in 2D or 3D space. For this quadrilateral, 309 | we organize the vertices as follows: 310 | 311 | CELL VERTICES:: 312 | 313 | a -------Vab------- b 314 | / / 315 | / / 316 | Vda (X) Vbc 317 | / / 318 | / / 319 | d -------Vcd------- c 320 | 321 | where the normal vector *(X)* is pointing into the page. For a set 322 | of quadrilaterals whose vertices are indexed in arrays *A, B, C* and *D* , 323 | this function returns the normal surface vector(s) and the area 324 | for each quadrilateral. 325 | 326 | At each vertex, there are 4 cross-products that can be used to compute the 327 | vector normal the surface defined by the quadrilateral. In 3D space however, 328 | the vertices indexed may not define a quadrilateral exactly and thus the normal vectors 329 | computed at each vertex might not be identical. In this case, you may choose output 330 | the normal vector at *a, b, c* and *d* or compute 331 | the average normal surface vector as follows: 332 | 333 | .. math:: 334 | \bf{n} = \frac{1}{4} \big ( 335 | \bf{v_{ab} \times v_{da}} + 336 | \bf{v_{bc} \times v_{ab}} + 337 | \bf{v_{cd} \times v_{bc}} + 338 | \bf{v_{da} \times v_{cd}} \big ) 339 | 340 | 341 | For computing the surface area, we assume the vertices define a quadrilateral. 342 | 343 | Parameters 344 | ---------- 345 | xyz : (n, 3) numpy.ndarray 346 | The x,y, and z locations for all verticies 347 | A : (n_face) numpy.ndarray 348 | Vector containing the indicies for the **a** vertex locations 349 | B : (n_face) numpy.ndarray 350 | Vector containing the indicies for the **b** vertex locations 351 | C : (n_face) numpy.ndarray 352 | Vector containing the indicies for the **c** vertex locations 353 | D : (n_face) numpy.ndarray 354 | Vector containing the indicies for the **d** vertex locations 355 | average : bool, optional 356 | If *True*, the function returns the average surface 357 | normal vector for each surface. If *False* , the function will 358 | return the normal vectors computed at the *A, B, C* and *D* 359 | vertices in a cell array {nA,nB,nC,nD}. 360 | normalize_normal : bool, optional 361 | If *True*, the function will normalize the surface normal 362 | vectors. This is applied regardless of whether the *average* parameter 363 | is set to *True* or *False*. If *False*, the vectors are not normalized. 364 | 365 | Returns 366 | ------- 367 | N : (n_face) numpy.ndarray or (4) list of (n_face) numpy.ndarray 368 | Normal vector(s) for each surface. If *average* = *True*, the function 369 | returns an ndarray with the average surface normal vectos. If *average* = *False* , 370 | the function returns a cell array {nA,nB,nC,nD} containing the normal vectors 371 | computed using each vertex of the surface. 372 | area : (n_face) numpy.ndarray 373 | The surface areas. 374 | 375 | Examples 376 | -------- 377 | Here we define a set of vertices for a tensor mesh. We then 378 | index 4 vertices for an irregular quadrilateral. The 379 | function *face_info* is used to compute the normal vector 380 | and the surface area. 381 | 382 | >>> from discretize.utils import face_info 383 | >>> from discretize import TensorMesh 384 | >>> import numpy as np 385 | >>> import matplotlib.pyplot as plt 386 | >>> import matplotlib as mpl 387 | 388 | Define Corners of a uniform cube. 389 | 390 | >>> h = [1, 1] 391 | >>> mesh = TensorMesh([h, h, h]) 392 | >>> xyz = mesh.nodes 393 | 394 | Choose the face indices, 395 | 396 | >>> A = np.array([0]) 397 | >>> B = np.array([4]) 398 | >>> C = np.array([26]) 399 | >>> D = np.array([18]) 400 | 401 | Compute average surface normal vector (normalized), 402 | 403 | >>> nvec, area = face_info(xyz, A, B, C, D) 404 | >>> nvec, area 405 | (array([[-0.70710678, 0.70710678, 0. ]]), array([4.24264069])) 406 | 407 | Plot surface for example 1 on mesh 408 | 409 | >>> fig = plt.figure(figsize=(7, 7)) 410 | >>> ax = plt.subplot(projection='3d') 411 | >>> mesh.plot_grid(ax=ax) 412 | >>> k = [0, 4, 26, 18, 0] 413 | >>> xyz_quad = xyz[k, :] 414 | >>> ax.plot(xyz_quad[:, 0], xyz_quad[:, 1], xyz_quad[:, 2], 'r') 415 | >>> ax.text(-0.25, 0., 3., 'Area of the surface: {:g} $m^2$'.format(area[0])) 416 | >>> ax.text(-0.25, 0., 2.8, 'Normal vector: ({:.2f}, {:.2f}, {:.2f})'.format( 417 | ... nvec[0, 0], nvec[0, 1], nvec[0, 2]) 418 | ... ) 419 | >>> plt.show() 420 | 421 | In our second example, the vertices are unable to define a flat 422 | surface in 3D space. However, we will demonstrate the *face_info* 423 | returns the average normal vector and an approximate surface area. 424 | 425 | Define the face indicies 426 | >>> A = np.array([0]) 427 | >>> B = np.array([5]) 428 | >>> C = np.array([26]) 429 | >>> D = np.array([18]) 430 | 431 | Compute average surface normal vector 432 | 433 | >>> nvec, area = face_info(xyz, A, B, C, D) 434 | >>> nvec, area 435 | (array([[-0.4472136 , 0.89442719, 0. ]]), array([2.23606798])) 436 | 437 | Plot surface for example 2 on mesh 438 | 439 | >>> fig = plt.figure(figsize=(7, 7)) 440 | >>> ax = plt.subplot(projection='3d') 441 | >>> mesh.plot_grid(ax=ax) 442 | >>> k = [0, 5, 26, 18, 0] 443 | >>> xyz_quad = xyz[k, :] 444 | >>> ax.plot(xyz_quad[:, 0], xyz_quad[:, 1], xyz_quad[:, 2], 'g') 445 | >>> ax.text(-0.25, 0., 3., 'Area of the surface: {:g} $m^2$'.format(area[0])) 446 | >>> ax.text(-0.25, 0., 2.8, 'Average normal vector: ({:.2f}, {:.2f}, {:.2f})'.format( 447 | ... nvec[0, 0], nvec[0, 1], nvec[0, 2]) 448 | ... ) 449 | >>> plt.show() 450 | """ 451 | if "normalizeNormals" in kwargs: 452 | raise TypeError( 453 | "The normalizeNormals keyword argument has been removed, please use normalize_normals. " 454 | "This will be removed in discretize 1.0.0", 455 | ) 456 | if not isinstance(average, bool): 457 | raise TypeError("average must be a boolean") 458 | if not isinstance(normalize_normals, bool): 459 | raise TypeError("normalize_normals must be a boolean") 460 | 461 | AB = xyz[B, :] - xyz[A, :] 462 | BC = xyz[C, :] - xyz[B, :] 463 | CD = xyz[D, :] - xyz[C, :] 464 | DA = xyz[A, :] - xyz[D, :] 465 | 466 | def cross(X, Y): 467 | return np.c_[ 468 | X[:, 1] * Y[:, 2] - X[:, 2] * Y[:, 1], 469 | X[:, 2] * Y[:, 0] - X[:, 0] * Y[:, 2], 470 | X[:, 0] * Y[:, 1] - X[:, 1] * Y[:, 0], 471 | ] 472 | 473 | nA = cross(AB, DA) 474 | nB = cross(BC, AB) 475 | nC = cross(CD, BC) 476 | nD = cross(DA, CD) 477 | 478 | def length(x): 479 | return np.sqrt(x[:, 0] ** 2 + x[:, 1] ** 2 + x[:, 2] ** 2) 480 | 481 | def normalize(x): 482 | return x / np.kron(np.ones((1, x.shape[1])), mkvc(length(x), 2)) 483 | 484 | if average: 485 | # average the normals at each vertex. 486 | N = (nA + nB + nC + nD) / 4 # this is intrinsically weighted by area 487 | # normalize 488 | N = normalize(N) 489 | else: 490 | if normalize_normals: 491 | N = [normalize(nA), normalize(nB), normalize(nC), normalize(nD)] 492 | else: 493 | N = [nA, nB, nC, nD] 494 | 495 | # Area calculation 496 | # 497 | # Approximate by 4 different triangles, and divide by 2. 498 | # Each triangle is one half of the length of the cross product 499 | # 500 | # So also could be viewed as the average parallelogram. 501 | # 502 | # TODO: This does not compute correctly for concave quadrilaterals 503 | area = (length(nA) + length(nB) + length(nC) + length(nD)) / 4 504 | 505 | return N, area 506 | 507 | 508 | exampleLrmGrid = deprecate_function( 509 | example_curvilinear_grid, 510 | "exampleLrmGrid", 511 | removal_version="1.0.0", 512 | error=True, 513 | ) 514 | volTetra = deprecate_function( 515 | volume_tetrahedron, "volTetra", removal_version="1.0.0", error=True 516 | ) 517 | indexCube = deprecate_function( 518 | index_cube, "indexCube", removal_version="1.0.0", error=True 519 | ) 520 | faceInfo = deprecate_function( 521 | face_info, "faceInfo", removal_version="1.0.0", error=True 522 | ) 523 | -------------------------------------------------------------------------------- /discretize/_extensions/geom.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "geom.h" 4 | #include 5 | #include 6 | 7 | // Define the 3D cross product as a pre-processor macro 8 | #define CROSS3D(e0, e1, out) \ 9 | out[0] = e0[1] * e1[2] - e0[2] * e1[1]; \ 10 | out[1] = e0[2] * e1[0] - e0[0] * e1[2]; \ 11 | out[2] = e0[0] * e1[1] - e0[1] * e1[0]; 12 | 13 | // simple geometric objects for intersection tests with an aabb 14 | 15 | Geometric::Geometric(){ 16 | dim = 0; 17 | } 18 | 19 | Geometric::Geometric(int_t dim){ 20 | this->dim = dim; 21 | } 22 | 23 | Ball::Ball() : Geometric(){ 24 | x0 = NULL; 25 | r = 0; 26 | rsq = 0; 27 | } 28 | 29 | Ball::Ball(int_t dim, double* x0, double r) : Geometric(dim){ 30 | this->x0 = x0; 31 | this->r = r; 32 | this->rsq = r * r; 33 | } 34 | 35 | bool Ball::intersects_cell(double *a, double *b) const{ 36 | // check if I intersect the ball 37 | double dx; 38 | double r2_test = 0.0; 39 | for(int_t i=0; ix0 = x0; 54 | this->x1 = x1; 55 | for(int_t i=0; i::infinity(); 62 | double t_far = std::numeric_limits::infinity(); 63 | double t0, t1; 64 | 65 | for(int_t i=0; i b[i])){ 68 | return false; 69 | } 70 | if(std::max(x0[i], x1[i]) < a[i]){ 71 | return false; 72 | } 73 | if(std::min(x0[i], x1[i]) > b[i]){ 74 | return false; 75 | } 76 | if (x0[i] != x1[i]){ 77 | t0 = (a[i] - x0[i]) * inv_dx[i]; 78 | t1 = (b[i] - x0[i]) * inv_dx[i]; 79 | if (t0 > t1){ 80 | std::swap(t0, t1); 81 | } 82 | t_near = std::max(t_near, t0); 83 | t_far = std::min(t_far, t1); 84 | if (t_near > t_far || t_far < 0 || t_near > 1){ 85 | return false; 86 | } 87 | } 88 | } 89 | return true; 90 | } 91 | 92 | Box::Box() : Geometric(){ 93 | x0 = NULL; 94 | x1 = NULL; 95 | } 96 | 97 | Box::Box(int_t dim, double* x0, double *x1) : Geometric(dim){ 98 | this->x0 = x0; 99 | this->x1 = x1; 100 | } 101 | 102 | bool Box::intersects_cell(double *a, double *b) const{ 103 | for(int_t i=0; i b[i]){ 108 | return false; 109 | } 110 | } 111 | return true; 112 | } 113 | 114 | Plane::Plane() : Geometric(){ 115 | origin = NULL; 116 | normal = NULL; 117 | } 118 | 119 | Plane::Plane(int_t dim, double* origin, double *normal) : Geometric(dim){ 120 | this->origin = origin; 121 | this->normal = normal; 122 | } 123 | 124 | bool Plane::intersects_cell(double *a, double *b) const{ 125 | double center; 126 | double half_width; 127 | double s = 0.0; 128 | double r = 0.0; 129 | for(int_t i=0;ix0 = x0; 152 | this->x1 = x1; 153 | this->x2 = x2; 154 | 155 | for(int_t i=0; i 2){ 161 | normal[0] = e0[1] * e1[2] - e0[2] * e1[1]; 162 | normal[1] = e0[2] * e1[0] - e0[0] * e1[2]; 163 | normal[2] = e0[0] * e1[1] - e0[1] * e1[0]; 164 | } 165 | } 166 | 167 | bool Triangle::intersects_cell(double *a, double *b) const{ 168 | double center; 169 | double v0[3], v1[3], v2[3], half[3]; 170 | double vmin, vmax; 171 | double p0, p1, p2, pmin, pmax, rad; 172 | for(int_t i=0; i < dim; ++i){ 173 | center = 0.5 * (b[i] + a[i]); 174 | v0[i] = x0[i] - center; 175 | v1[i] = x1[i] - center; 176 | vmin = std::min(v0[i], v1[i]); 177 | vmax = std::max(v0[i], v1[i]); 178 | v2[i] = x2[i] - center; 179 | vmin = std::min(vmin, v2[i]); 180 | vmax = std::max(vmax, v2[i]); 181 | half[i] = center - a[i]; 182 | 183 | // Bounding box check 184 | if (vmin > half[i] || vmax < -half[i]){ 185 | return false; 186 | } 187 | } 188 | // first do the 3 edge cross tests that apply in 2D and 3D 189 | 190 | // edge 0 cross z_hat 191 | //p0 = e0[1] * v0[0] - e0[0] * v0[1]; 192 | p1 = e0[1] * v1[0] - e0[0] * v1[1]; 193 | p2 = e0[1] * v2[0] - e0[0] * v2[1]; 194 | pmin = std::min(p1, p2); 195 | pmax = std::max(p1, p2); 196 | rad = std::abs(e0[1]) * half[0] + std::abs(e0[0]) * half[1]; 197 | if (pmin > rad || pmax < -rad){ 198 | return false; 199 | } 200 | 201 | // edge 1 cross z_hat 202 | p0 = e1[1] * v0[0] - e1[0] * v0[1]; 203 | p1 = e1[1] * v1[0] - e1[0] * v1[1]; 204 | //p2 = e1[1] * v2[0] - e1[0] * v2[1]; 205 | pmin = std::min(p0, p1); 206 | pmax = std::max(p0, p1); 207 | rad = std::abs(e1[1]) * half[0] + std::abs(e1[0]) * half[1]; 208 | if (pmin > rad || pmax < -rad){ 209 | return false; 210 | } 211 | 212 | // edge 2 cross z_hat 213 | //p0 = e2[1] * v0[0] - e2[0] * v0[1]; 214 | p1 = e2[1] * v1[0] - e2[0] * v1[1]; 215 | p2 = e2[1] * v2[0] - e2[0] * v2[1]; 216 | pmin = std::min(p1, p2); 217 | pmax = std::max(p1, p2); 218 | rad = std::abs(e2[1]) * half[0] + std::abs(e2[0]) * half[1]; 219 | if (pmin > rad || pmax < -rad){ 220 | return false; 221 | } 222 | 223 | if(dim > 2){ 224 | // edge 0 cross x_hat 225 | p0 = e0[2] * v0[1] - e0[1] * v0[2]; 226 | //p1 = e0[2] * v1[1] - e0[1] * v1[2]; 227 | p2 = e0[2] * v2[1] - e0[1] * v2[2]; 228 | pmin = std::min(p0, p2); 229 | pmax = std::max(p0, p2); 230 | rad = std::abs(e0[2]) * half[1] + std::abs(e0[1]) * half[2]; 231 | if (pmin > rad || pmax < -rad){ 232 | return false; 233 | } 234 | // edge 0 cross y_hat 235 | p0 = -e0[2] * v0[0] + e0[0] * v0[2]; 236 | //p1 = -e0[2] * v1[0] + e0[0] * v1[2]; 237 | p2 = -e0[2] * v2[0] + e0[0] * v2[2]; 238 | pmin = std::min(p0, p2); 239 | pmax = std::max(p0, p2); 240 | rad = std::abs(e0[2]) * half[0] + std::abs(e0[0]) * half[2]; 241 | if (pmin > rad || pmax < -rad){ 242 | return false; 243 | } 244 | // edge 1 cross x_hat 245 | p0 = e1[2] * v0[1] - e1[1] * v0[2]; 246 | //p1 = e1[2] * v1[1] - e1[1] * v1[2]; 247 | p2 = e1[2] * v2[1] - e1[1] * v2[2]; 248 | pmin = std::min(p0, p2); 249 | pmax = std::max(p0, p2); 250 | rad = std::abs(e1[2]) * half[1] + std::abs(e1[1]) * half[2]; 251 | if (pmin > rad || pmax < -rad){ 252 | return false; 253 | } 254 | // edge 1 cross y_hat 255 | p0 = -e1[2] * v0[0] + e1[0] * v0[2]; 256 | //p1 = -e1[2] * v1[0] + e1[0] * v1[2]; 257 | p2 = -e1[2] * v2[0] + e1[0] * v2[2]; 258 | pmin = std::min(p0, p2); 259 | pmax = std::max(p0, p2); 260 | rad = std::abs(e1[2]) * half[0] + std::abs(e1[0]) * half[2]; 261 | if (pmin > rad || pmax < -rad){ 262 | return false; 263 | } 264 | // edge 2 cross x_hat 265 | p0 = e2[2] * v0[1] - e2[1] * v0[2]; 266 | p1 = e2[2] * v1[1] - e2[1] * v1[2]; 267 | //p2 = e2[2] * v2[1] - e2[1] * v2[2]; 268 | pmin = std::min(p0, p1); 269 | pmax = std::max(p0, p1); 270 | rad = std::abs(e2[2]) * half[1] + std::abs(e2[1]) * half[2]; 271 | if (pmin > rad || pmax < -rad){ 272 | return false; 273 | } 274 | // edge 2 cross y_hat 275 | p0 = -e2[2] * v0[0] + e2[0] * v0[2]; 276 | p1 = -e2[2] * v1[0] + e2[0] * v1[2]; 277 | //p2 = -e2[2] * v2[0] + e2[0] * v2[2]; 278 | pmin = std::min(p0, p1); 279 | pmax = std::max(p0, p1); 280 | rad = std::abs(e2[2]) * half[0] + std::abs(e2[0]) * half[2]; 281 | if (pmin > rad || pmax < -rad){ 282 | return false; 283 | } 284 | 285 | // triangle normal axis 286 | pmin = 0.0; 287 | pmax = 0.0; 288 | for(int_t i=0; i 0){ 290 | pmin += normal[i] * (-half[i] - v0[i]); 291 | pmax += normal[i] * (half[i] - v0[i]); 292 | }else{ 293 | pmin += normal[i] * (half[i] - v0[i]); 294 | pmax += normal[i] * (-half[i] - v0[i]); 295 | } 296 | } 297 | if (pmin > 0 || pmax < 0){ 298 | return false; 299 | } 300 | } 301 | return true; 302 | } 303 | 304 | VerticalTriangularPrism::VerticalTriangularPrism() : Triangle(){ 305 | h = 0; 306 | } 307 | 308 | VerticalTriangularPrism::VerticalTriangularPrism(int_t dim, double* x0, double *x1, double *x2, double h) : Triangle(dim, x0, x1, x2){ 309 | this->h = h; 310 | } 311 | 312 | bool VerticalTriangularPrism::intersects_cell(double *a, double *b) const{ 313 | double center; 314 | double v0[3], v1[3], v2[3], half[3]; 315 | double vmin, vmax; 316 | double p0, p1, p2, p3, pmin, pmax, rad; 317 | for(int_t i=0; i < dim; ++i){ 318 | center = 0.5 * (a[i] + b[i]); 319 | v0[i] = x0[i] - center; 320 | v1[i] = x1[i] - center; 321 | vmin = std::min(v0[i], v1[i]); 322 | vmax = std::max(v0[i], v1[i]); 323 | v2[i] = x2[i] - center; 324 | vmin = std::min(vmin, v2[i]); 325 | vmax = std::max(vmax, v2[i]); 326 | if(i == 2){ 327 | vmax += h; 328 | } 329 | half[i] = center - a[i]; 330 | 331 | // Bounding box check 332 | if (vmin > half[i] || vmax < -half[i]){ 333 | return false; 334 | } 335 | } 336 | // first do the 3 edge cross tests that apply in 2D and 3D 337 | 338 | // edge 0 cross z_hat 339 | //p0 = e0[1] * v0[0] - e0[0] * v0[1]; 340 | p1 = e0[1] * v1[0] - e0[0] * v1[1]; 341 | p2 = e0[1] * v2[0] - e0[0] * v2[1]; 342 | pmin = std::min(p1, p2); 343 | pmax = std::max(p1, p2); 344 | rad = std::abs(e0[1]) * half[0] + std::abs(e0[0]) * half[1]; 345 | if (pmin > rad || pmax < -rad){ 346 | return false; 347 | } 348 | 349 | // edge 1 cross z_hat 350 | p0 = e1[1] * v0[0] - e1[0] * v0[1]; 351 | p1 = e1[1] * v1[0] - e1[0] * v1[1]; 352 | //p2 = e1[1] * v2[0] - e1[0] * v2[1]; 353 | pmin = std::min(p0, p1); 354 | pmax = std::max(p0, p1); 355 | rad = std::abs(e1[1]) * half[0] + std::abs(e1[0]) * half[1]; 356 | if (pmin > rad || pmax < -rad){ 357 | return false; 358 | } 359 | 360 | // edge 2 cross z_hat 361 | //p0 = e2[1] * v0[0] - e2[0] * v0[1]; 362 | p1 = e2[1] * v1[0] - e2[0] * v1[1]; 363 | p2 = e2[1] * v2[0] - e2[0] * v2[1]; 364 | pmin = std::min(p1, p2); 365 | pmax = std::max(p1, p2); 366 | rad = std::abs(e2[1]) * half[0] + std::abs(e2[0]) * half[1]; 367 | if (pmin > rad || pmax < -rad){ 368 | return false; 369 | } 370 | 371 | // edge 0 cross x_hat 372 | p0 = e0[2] * v0[1] - e0[1] * v0[2]; 373 | p1 = e0[2] * v0[1] - e0[1] * (v0[2] + h); 374 | p2 = e0[2] * v2[1] - e0[1] * v2[2]; 375 | p3 = e0[2] * v2[1] - e0[1] * (v2[2] + h); 376 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 377 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 378 | rad = std::abs(e0[2]) * half[1] + std::abs(e0[1]) * half[2]; 379 | if (pmin > rad || pmax < -rad){ 380 | return false; 381 | } 382 | // edge 0 cross y_hat 383 | p0 = -e0[2] * v0[0] + e0[0] * v0[2]; 384 | p1 = -e0[2] * v0[0] + e0[0] * (v0[2] + h); 385 | p2 = -e0[2] * v2[0] + e0[0] * v2[2]; 386 | p3 = -e0[2] * v2[0] + e0[0] * (v2[2] + h); 387 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 388 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 389 | rad = std::abs(e0[2]) * half[0] + std::abs(e0[0]) * half[2]; 390 | if (pmin > rad || pmax < -rad){ 391 | return false; 392 | } 393 | // edge 1 cross x_hat 394 | p0 = e1[2] * v0[1] - e1[1] * v0[2]; 395 | p1 = e1[2] * v0[1] - e1[1] * (v0[2] + h); 396 | p2 = e1[2] * v2[1] - e1[1] * v2[2]; 397 | p3 = e1[2] * v2[1] - e1[1] * (v2[2] + h); 398 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 399 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 400 | rad = std::abs(e1[2]) * half[1] + std::abs(e1[1]) * half[2]; 401 | if (pmin > rad || pmax < -rad){ 402 | return false; 403 | } 404 | // edge 1 cross y_hat 405 | p0 = -e1[2] * v0[0] + e1[0] * v0[2]; 406 | p1 = -e1[2] * v0[0] + e1[0] * (v0[2] + h); 407 | p2 = -e1[2] * v2[0] + e1[0] * v2[2]; 408 | p3 = -e1[2] * v2[0] + e1[0] * (v2[2] + h); 409 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 410 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 411 | rad = std::abs(e1[2]) * half[0] + std::abs(e1[0]) * half[2]; 412 | if (pmin > rad || pmax < -rad){ 413 | return false; 414 | } 415 | // edge 2 cross x_hat 416 | p0 = e2[2] * v0[1] - e2[1] * v0[2]; 417 | p1 = e2[2] * v0[1] - e2[1] * (v0[2] + h); 418 | p2 = e2[2] * v1[1] - e2[1] * v1[2]; 419 | p3 = e2[2] * v1[1] - e2[1] * (v1[2] + h); 420 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 421 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 422 | rad = std::abs(e2[2]) * half[1] + std::abs(e2[1]) * half[2]; 423 | if (pmin > rad || pmax < -rad){ 424 | return false; 425 | } 426 | // edge 2 cross y_hat 427 | p0 = -e2[2] * v0[0] + e2[0] * v0[2]; 428 | p1 = -e2[2] * v0[0] + e2[0] * (v0[2] + h); 429 | p2 = -e2[2] * v1[0] + e2[0] * v1[2]; 430 | p3 = -e2[2] * v1[0] + e2[0] * (v1[2] + h); 431 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 432 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 433 | rad = std::abs(e2[2]) * half[0] + std::abs(e2[0]) * half[2]; 434 | if (pmin > rad || pmax < -rad){ 435 | return false; 436 | } 437 | 438 | // triangle normal axis 439 | p0 = normal[0] * v0[0] + normal[1] * v0[1] + normal[2] * v0[2]; 440 | p1 = normal[0] * v0[0] + normal[1] * v0[1] + normal[2] * (v0[2] + h); 441 | pmin = std::min(p0, p1); 442 | pmax = std::max(p0, p1); 443 | rad = std::abs(normal[0]) * half[0] + std::abs(normal[1]) * half[1] + std::abs(normal[2]) * half[2]; 444 | if (pmin > rad || pmax < -rad){ 445 | return false; 446 | } 447 | // the axes defined by the three vertical prism faces 448 | // should already be tested by the e0, e1, e2 cross z_hat tests 449 | return true; 450 | } 451 | 452 | Tetrahedron::Tetrahedron() : Geometric(){ 453 | x0 = NULL; 454 | x1 = NULL; 455 | x2 = NULL; 456 | x3 = NULL; 457 | for(int_t i=0; i<6; ++i){ 458 | for(int_t j=0; j<3; ++j){ 459 | edge_tans[i][j] = 0.0; 460 | } 461 | } 462 | for(int_t i=0; i<4; ++i){ 463 | for(int_t j=0; j<3; ++j){ 464 | face_normals[i][j] = 0.0; 465 | } 466 | } 467 | } 468 | 469 | Tetrahedron::Tetrahedron(int_t dim, double* x0, double *x1, double *x2, double *x3) : Geometric(dim){ 470 | this->x0 = x0; 471 | this->x1 = x1; 472 | this->x2 = x2; 473 | this->x3 = x3; 474 | for(int_t i=0; i half[i] || pmax < -half[i]){ 510 | return false; 511 | } 512 | } 513 | // first do the 3 edge cross tests that apply in 2D and 3D 514 | const double *axis; 515 | 516 | for(int_t i=0; i<6; ++i){ 517 | // edge cross [1, 0, 0] 518 | p0 = edge_tans[i][2] * v0[1] - edge_tans[i][1] * v0[2]; 519 | p1 = edge_tans[i][2] * v1[1] - edge_tans[i][1] * v1[2]; 520 | p2 = edge_tans[i][2] * v2[1] - edge_tans[i][1] * v2[2]; 521 | p3 = edge_tans[i][2] * v3[1] - edge_tans[i][1] * v3[2]; 522 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 523 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 524 | rad = std::abs(edge_tans[i][2]) * half[1] + std::abs(edge_tans[i][1]) * half[2]; 525 | if (pmin > rad || pmax < -rad){ 526 | return false; 527 | } 528 | 529 | p0 = -edge_tans[i][2] * v0[0] + edge_tans[i][0] * v0[2]; 530 | p1 = -edge_tans[i][2] * v1[0] + edge_tans[i][0] * v1[2]; 531 | p2 = -edge_tans[i][2] * v2[0] + edge_tans[i][0] * v2[2]; 532 | p3 = -edge_tans[i][2] * v3[0] + edge_tans[i][0] * v3[2]; 533 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 534 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 535 | rad = std::abs(edge_tans[i][2]) * half[0] + std::abs(edge_tans[i][0]) * half[2]; 536 | if (pmin > rad || pmax < -rad){ 537 | return false; 538 | } 539 | 540 | p0 = edge_tans[i][1] * v0[0] - edge_tans[i][0] * v0[1]; 541 | p1 = edge_tans[i][1] * v1[0] - edge_tans[i][0] * v1[1]; 542 | p2 = edge_tans[i][1] * v2[0] - edge_tans[i][0] * v2[1]; 543 | p3 = edge_tans[i][1] * v3[0] - edge_tans[i][0] * v3[1]; 544 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 545 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 546 | rad = std::abs(edge_tans[i][1]) * half[0] + std::abs(edge_tans[i][0]) * half[1]; 547 | if (pmin > rad || pmax < -rad){ 548 | return false; 549 | } 550 | } 551 | // triangle face normals 552 | for(int_t i=0; i<4; ++i){ 553 | axis = face_normals[i]; 554 | p0 = axis[0] * v0[0] + axis[1] * v0[1] + axis[2] * v0[2]; 555 | p1 = axis[0] * v1[0] + axis[1] * v1[1] + axis[2] * v1[2]; 556 | p2 = axis[0] * v2[0] + axis[1] * v2[1] + axis[2] * v2[2]; 557 | p3 = axis[0] * v3[0] + axis[1] * v3[1] + axis[2] * v3[2]; 558 | pmin = std::min(std::min(std::min(p0, p1), p2), p3); 559 | pmax = std::max(std::max(std::max(p0, p1), p2), p3); 560 | rad = std::abs(axis[0]) * half[0] + std::abs(axis[1]) * half[1] + std::abs(axis[2]) * half[2]; 561 | if (pmin > rad || pmax < -rad){ 562 | return false; 563 | } 564 | } 565 | return true; 566 | } -------------------------------------------------------------------------------- /discretize/mixins/mesh_io.py: -------------------------------------------------------------------------------- 1 | """Module for reading and writing meshes to text files. 2 | 3 | The text files representing meshes are often in the `UBC` format. 4 | """ 5 | 6 | import os 7 | import numpy as np 8 | 9 | from discretize.utils import mkvc 10 | from discretize.utils.code_utils import deprecate_method 11 | 12 | try: 13 | from discretize.mixins.vtk_mod import ( 14 | InterfaceTensorread_vtk, 15 | InterfaceSimplexReadVTK, 16 | ) 17 | except ImportError: 18 | InterfaceSimplexReadVTK = InterfaceTensorread_vtk = object 19 | 20 | 21 | class TensorMeshIO(InterfaceTensorread_vtk): 22 | """Class for managing the input/output of tensor meshes and models. 23 | 24 | The ``TensorMeshIO`` class contains a set of class methods specifically 25 | for the :class:`~discretize.TensorMesh` class. These include: 26 | 27 | - Read/write tensor meshes to file 28 | - Read/write models defined on tensor meshes 29 | 30 | """ 31 | 32 | @classmethod 33 | def _readUBC_3DMesh(cls, file_name): 34 | """Read 3D tensor mesh from UBC-GIF formatted file. 35 | 36 | Parameters 37 | ---------- 38 | file_name : str or file name 39 | full path to the UBC-GIF formatted mesh file 40 | 41 | Returns 42 | ------- 43 | discretize.TensorMesh 44 | The tensor mesh 45 | """ 46 | # Read the file as line strings, remove lines with comment = ! 47 | msh = np.genfromtxt(file_name, delimiter="\n", dtype=str, comments="!") 48 | 49 | # Interal function to read cell size lines for the UBC mesh files. 50 | def readCellLine(line): 51 | line_list = [] 52 | for seg in line.split(): 53 | if "*" in seg: 54 | sp = seg.split("*") 55 | seg_arr = np.ones((int(sp[0]),)) * float(sp[1]) 56 | else: 57 | seg_arr = np.array([float(seg)], float) 58 | line_list.append(seg_arr) 59 | return np.concatenate(line_list) 60 | 61 | # Fist line is the size of the model 62 | # sizeM = np.array(msh[0].split(), dtype=float) 63 | # Second line is the South-West-Top corner coordinates. 64 | origin = np.array(msh[1].split(), dtype=float) 65 | # Read the cell sizes 66 | h1 = readCellLine(msh[2]) 67 | h2 = readCellLine(msh[3]) 68 | h3temp = readCellLine(msh[4]) 69 | # Invert the indexing of the vector to start from the bottom. 70 | h3 = h3temp[::-1] 71 | # Adjust the reference point to the bottom south west corner 72 | origin[2] = origin[2] - np.sum(h3) 73 | # Make the mesh 74 | tensMsh = cls([h1, h2, h3], origin=origin) 75 | return tensMsh 76 | 77 | @classmethod 78 | def _readUBC_2DMesh(cls, file_name): 79 | """Read 2D tensor mesh from UBC-GIF formatted file. 80 | 81 | Parameters 82 | ---------- 83 | file_name : str or file name 84 | full path to the UBC-GIF formatted mesh file 85 | 86 | Returns 87 | ------- 88 | discretize.TensorMesh 89 | The tensor mesh 90 | """ 91 | fopen = open(file_name, "r") 92 | 93 | # Read down the file and unpack dx vector 94 | def unpackdx(fid, nrows): 95 | for ii in range(nrows): 96 | line = fid.readline() 97 | var = np.array(line.split(), dtype=float) 98 | if ii == 0: 99 | x0 = var[0] 100 | xvec = np.ones(int(var[2])) * (var[1] - var[0]) / int(var[2]) 101 | xend = var[1] 102 | else: 103 | xvec = np.hstack( 104 | (xvec, np.ones(int(var[1])) * (var[0] - xend) / int(var[1])) 105 | ) 106 | xend = var[0] 107 | return x0, xvec 108 | 109 | # Start with dx block 110 | # First line specifies the number of rows for x-cells 111 | line = fopen.readline() 112 | # Strip comments lines 113 | while line.startswith("!"): 114 | line = fopen.readline() 115 | nl = np.array(line.split(), dtype=int) 116 | [x0, dx] = unpackdx(fopen, nl[0]) 117 | # Move down the file until reaching the z-block 118 | line = fopen.readline() 119 | if not line: 120 | line = fopen.readline() 121 | # End with dz block 122 | # First line specifies the number of rows for z-cells 123 | line = fopen.readline() 124 | nl = np.array(line.split(), dtype=int) 125 | [z0, dz] = unpackdx(fopen, nl[0]) 126 | # Flip z0 to be the bottom of the mesh for SimPEG 127 | z0 = -(z0 + sum(dz)) 128 | dz = dz[::-1] 129 | # Make the mesh 130 | tensMsh = cls([dx, dz], origin=(x0, z0)) 131 | 132 | fopen.close() 133 | 134 | return tensMsh 135 | 136 | @classmethod 137 | def read_UBC(cls, file_name, directory=None): 138 | """Read 2D or 3D tensor mesh from UBC-GIF formatted file. 139 | 140 | Parameters 141 | ---------- 142 | file_name : str or file name 143 | full path to the UBC-GIF formatted mesh file or just its name if directory is specified 144 | directory : str, optional 145 | directory where the UBC-GIF file lives 146 | 147 | Returns 148 | ------- 149 | discretize.TensorMesh 150 | The tensor mesh 151 | """ 152 | # Check the expected mesh dimensions 153 | if directory is None: 154 | directory = "" 155 | fname = os.path.join(directory, file_name) 156 | # Read the file as line strings, remove lines with comment = ! 157 | msh = np.genfromtxt(fname, delimiter="\n", dtype=str, comments="!", max_rows=1) 158 | # Fist line is the size of the model 159 | sizeM = np.array(msh.ravel()[0].split(), dtype=float) 160 | # Check if the mesh is a UBC 2D mesh 161 | if sizeM.shape[0] == 1: 162 | Tnsmsh = cls._readUBC_2DMesh(fname) 163 | # Check if the mesh is a UBC 3D mesh 164 | elif sizeM.shape[0] == 3: 165 | Tnsmsh = cls._readUBC_3DMesh(fname) 166 | else: 167 | raise Exception("File format not recognized") 168 | return Tnsmsh 169 | 170 | def _readModelUBC_2D(mesh, file_name): 171 | """Read UBC-GIF formatted model file for 2D tensor mesh. 172 | 173 | Parameters 174 | ---------- 175 | file_name : str or file name 176 | full path to the UBC-GIF formatted model file 177 | 178 | Returns 179 | ------- 180 | (n_cells) numpy.ndarray 181 | The model defined on the 2D tensor mesh 182 | """ 183 | # Open file and skip header... assume that we know the mesh already 184 | obsfile = np.genfromtxt(file_name, delimiter=" \n", dtype=str, comments="!") 185 | 186 | dim = tuple(np.array(obsfile[0].split(), dtype=int)) 187 | if mesh.shape_cells != dim: 188 | raise Exception("Dimension of the model and mesh mismatch") 189 | 190 | model = [] 191 | for line in obsfile[1:]: 192 | model.extend([float(val) for val in line.split()]) 193 | model = np.asarray(model) 194 | if not len(model) == mesh.nC: 195 | raise Exception( 196 | """Something is not right, expected size is {:d} 197 | but unwrap vector is size {:d}""".format( 198 | mesh.nC, len(model) 199 | ) 200 | ) 201 | 202 | return model.reshape(mesh.vnC, order="F")[:, ::-1].reshape(-1, order="F") 203 | 204 | def _readModelUBC_3D(mesh, file_name): 205 | """Read UBC-GIF formatted model file for 3D tensor mesh. 206 | 207 | Parameters 208 | ---------- 209 | file_name : str or file name 210 | full path to the UBC-GIF formatted model file 211 | 212 | Returns 213 | ------- 214 | (n_cells) numpy.ndarray 215 | The model defined on the 3D tensor mesh 216 | """ 217 | f = open(file_name, "r") 218 | model = np.array(list(map(float, f.readlines()))) 219 | f.close() 220 | nCx, nCy, nCz = mesh.shape_cells 221 | model = np.reshape(model, (nCz, nCx, nCy), order="F") 222 | model = model[::-1, :, :] 223 | model = np.transpose(model, (1, 2, 0)) 224 | model = mkvc(model) 225 | return model 226 | 227 | def read_model_UBC(mesh, file_name, directory=None): 228 | """Read UBC-GIF formatted model file for 2D or 3D tensor mesh. 229 | 230 | Parameters 231 | ---------- 232 | file_name : str or file name 233 | full path to the UBC-GIF formatted model file or just its name if directory is specified 234 | directory : str, optional 235 | directory where the UBC-GIF file lives 236 | 237 | Returns 238 | ------- 239 | (n_cells) numpy.ndarray 240 | The model defined on the mesh 241 | """ 242 | if directory is None: 243 | directory = "" 244 | fname = os.path.join(directory, file_name) 245 | if mesh.dim == 3: 246 | model = mesh._readModelUBC_3D(fname) 247 | elif mesh.dim == 2: 248 | model = mesh._readModelUBC_2D(fname) 249 | else: 250 | raise Exception("mesh must be a Tensor Mesh 2D or 3D") 251 | return model 252 | 253 | def write_model_UBC(mesh, file_name, model, directory=None): 254 | """Write 2D or 3D tensor model to UBC-GIF formatted file. 255 | 256 | Parameters 257 | ---------- 258 | file_name : str or file name 259 | full path for the output mesh file or just its name if directory is specified 260 | model : (n_cells) numpy.ndarray 261 | The model to write out. 262 | directory : str, optional 263 | output directory 264 | """ 265 | if directory is None: 266 | directory = "" 267 | fname = os.path.join(directory, file_name) 268 | if mesh.dim == 3: 269 | # Reshape model to a matrix 270 | modelMat = mesh.reshape(model, "CC", "CC", "M") 271 | # Transpose the axes 272 | modelMatT = modelMat.transpose((2, 0, 1)) 273 | # Flip z to positive down 274 | modelMatTR = mkvc(modelMatT[::-1, :, :]) 275 | np.savetxt(fname, modelMatTR.ravel()) 276 | 277 | elif mesh.dim == 2: 278 | modelMat = mesh.reshape(model, "CC", "CC", "M").T[::-1] 279 | f = open(fname, "w") 280 | f.write("{:d} {:d}\n".format(*mesh.shape_cells)) 281 | f.close() 282 | f = open(fname, "ab") 283 | np.savetxt(f, modelMat) 284 | f.close() 285 | 286 | else: 287 | raise Exception("mesh must be a Tensor Mesh 2D or 3D") 288 | 289 | def _writeUBC_3DMesh(mesh, file_name, comment_lines=""): 290 | """Write 3D tensor mesh to UBC-GIF formatted file. 291 | 292 | Parameters 293 | ---------- 294 | file_name : str or file name 295 | full path for the output mesh file 296 | comment_lines : str, optional 297 | comment lines preceded are preceeded with '!' 298 | """ 299 | if not mesh.dim == 3: 300 | raise Exception("Mesh must be 3D") 301 | 302 | s = comment_lines 303 | s += "{0:d} {1:d} {2:d}\n".format(*tuple(mesh.vnC)) 304 | # Have to it in the same operation or use mesh.origin.copy(), 305 | # otherwise the mesh.origin is updated. 306 | origin = mesh.origin + np.array([0, 0, mesh.h[2].sum()]) 307 | 308 | nCx, nCy, nCz = mesh.shape_cells 309 | s += "{0:.6f} {1:.6f} {2:.6f}\n".format(*tuple(origin)) 310 | s += ("%.6f " * nCx + "\n") % tuple(mesh.h[0]) 311 | s += ("%.6f " * nCy + "\n") % tuple(mesh.h[1]) 312 | s += ("%.6f " * nCz + "\n") % tuple(mesh.h[2][::-1]) 313 | f = open(file_name, "w") 314 | f.write(s) 315 | f.close() 316 | 317 | def _writeUBC_2DMesh(mesh, file_name, comment_lines=""): 318 | """Write 2D tensor mesh to UBC-GIF formatted file. 319 | 320 | Parameters 321 | ---------- 322 | file_name : str or file name 323 | full path for the output mesh file 324 | comment_lines : str, optional 325 | comment lines preceded are preceeded with '!' 326 | """ 327 | if not mesh.dim == 2: 328 | raise Exception("Mesh must be 2D") 329 | 330 | def writeF(fx, outStr=""): 331 | # Init 332 | i = 0 333 | origin = True 334 | x0 = fx[i] 335 | f = fx[i] 336 | number_segment = 0 337 | auxStr = "" 338 | 339 | while True: 340 | i = i + 1 341 | if i >= fx.size: 342 | break 343 | dx = -f + fx[i] 344 | f = fx[i] 345 | n = 1 346 | 347 | for j in range(i + 1, fx.size): 348 | if -f + fx[j] == dx: 349 | n += 1 350 | i += 1 351 | f = fx[j] 352 | else: 353 | break 354 | 355 | number_segment += 1 356 | if origin: 357 | auxStr += "{:.10f} {:.10f} {:d} \n".format(x0, f, n) 358 | origin = False 359 | else: 360 | auxStr += "{:.10f} {:d} \n".format(f, n) 361 | 362 | auxStr = "{:d}\n".format(number_segment) + auxStr 363 | outStr += auxStr 364 | 365 | return outStr 366 | 367 | # Grab face coordinates 368 | fx = mesh.nodes_x 369 | fz = -mesh.nodes_y[::-1] 370 | 371 | # Create the string 372 | outStr = comment_lines 373 | outStr = writeF(fx, outStr=outStr) 374 | outStr += "\n" 375 | outStr = writeF(fz, outStr=outStr) 376 | 377 | # Write file 378 | f = open(file_name, "w") 379 | f.write(outStr) 380 | f.close() 381 | 382 | def write_UBC(mesh, file_name, models=None, directory=None, comment_lines=""): 383 | """Write 2D or 3D tensor mesh (and models) to UBC-GIF formatted file(s). 384 | 385 | Parameters 386 | ---------- 387 | file_name : str or file name 388 | full path for the output mesh file or just its name if directory is specified 389 | models : dict of [str, (n_cells) numpy.ndarray], optional 390 | The dictionary key is a string representing the model's name. Each model 391 | is an (n_cells) array. 392 | directory : str, optional 393 | output directory 394 | comment_lines : str, optional 395 | comment lines preceded are preceeded with '!' 396 | """ 397 | if directory is None: 398 | directory = "" 399 | fname = os.path.join(directory, file_name) 400 | if mesh.dim == 3: 401 | mesh._writeUBC_3DMesh(fname, comment_lines=comment_lines) 402 | elif mesh.dim == 2: 403 | mesh._writeUBC_2DMesh(fname, comment_lines=comment_lines) 404 | else: 405 | raise Exception("mesh must be a Tensor Mesh 2D or 3D") 406 | 407 | if models is None: 408 | return 409 | if not isinstance(models, dict): 410 | raise TypeError("models must be a dict") 411 | for key in models: 412 | if not isinstance(key, str): 413 | raise TypeError( 414 | "The dict key must be a string representing the file name" 415 | ) 416 | mesh.write_model_UBC(key, models[key], directory=directory) 417 | 418 | # DEPRECATED 419 | @classmethod 420 | def readUBC(TensorMesh, file_name, directory=""): 421 | """Read 2D or 3D tensor mesh from UBC-GIF formatted file. 422 | 423 | *readUBC* has been deprecated and replaced by *read_UBC* 424 | See Also 425 | -------- 426 | read_UBC 427 | """ 428 | raise NotImplementedError( 429 | "TensorMesh.readUBC has been removed and this be removed in" 430 | "discretize 1.0.0. please use TensorMesh.read_UBC", 431 | ) 432 | 433 | readModelUBC = deprecate_method( 434 | "read_model_UBC", "readModelUBC", removal_version="1.0.0", error=True 435 | ) 436 | writeUBC = deprecate_method( 437 | "write_UBC", "writeUBC", removal_version="1.0.0", error=True 438 | ) 439 | writeModelUBC = deprecate_method( 440 | "write_model_UBC", "writeModelUBC", removal_version="1.0.0", error=True 441 | ) 442 | 443 | 444 | class TreeMeshIO(object): 445 | """Class for managing the input/output of tree meshes and models. 446 | 447 | The ``TreeMeshIO`` class contains a set of class methods specifically 448 | for the :class:`~discretize.TreeMesh` class. These include: 449 | 450 | - Read/write tree meshes to file 451 | - Read/write models defined on tree meshes 452 | 453 | """ 454 | 455 | @classmethod 456 | def read_UBC(TreeMesh, file_name, directory=None): 457 | """Read 3D tree mesh (OcTree mesh) from UBC-GIF formatted file. 458 | 459 | Parameters 460 | ---------- 461 | file_name : str or file name 462 | full path to the UBC-GIF formatted mesh file or just its name if directory is specified 463 | directory : str, optional 464 | directory where the UBC-GIF file lives 465 | 466 | Returns 467 | ------- 468 | discretize.TreeMesh 469 | The tree mesh 470 | """ 471 | if directory is None: 472 | directory = "" 473 | fname = os.path.join(directory, file_name) 474 | fileLines = np.genfromtxt(fname, dtype=str, delimiter="\n", comments="!") 475 | nCunderMesh = np.array(fileLines[0].split("!")[0].split(), dtype=int) 476 | tswCorn = np.array(fileLines[1].split("!")[0].split(), dtype=float) 477 | smallCell = np.array(fileLines[2].split("!")[0].split(), dtype=float) 478 | # Read the index array 479 | indArr = np.genfromtxt( 480 | (line.encode("utf8") for line in fileLines[4::]), dtype=np.int64 481 | ) 482 | nCunderMesh = nCunderMesh[: len(tswCorn)] # remove information related to core 483 | 484 | hs = [np.ones(nr) * sz for nr, sz in zip(nCunderMesh, smallCell)] 485 | origin = tswCorn 486 | origin[-1] -= np.sum(hs[-1]) 487 | 488 | ls = np.log2(nCunderMesh).astype(int) 489 | # if all ls are equal 490 | if min(ls) == max(ls): 491 | max_level = ls[0] 492 | else: 493 | max_level = min(ls) + 1 494 | 495 | mesh = TreeMesh(hs, origin=origin, diagonal_balance=False) 496 | levels = indArr[:, -1] 497 | indArr = indArr[:, :-1] 498 | 499 | indArr -= 1 # shift by 1.... 500 | indArr = 2 * indArr + levels[:, None] # get cell center index 501 | indArr[:, -1] = 2 * nCunderMesh[-1] - indArr[:, -1] # switch direction of iz 502 | levels = max_level - np.log2(levels) # calculate level 503 | 504 | mesh.__setstate__((indArr, levels)) 505 | return mesh 506 | 507 | def read_model_UBC(mesh, file_name, directory=None): 508 | """Read UBC-GIF formatted file model file for 3D tree mesh (OcTree). 509 | 510 | Parameters 511 | ---------- 512 | file_name : str or list of str 513 | full path to the UBC-GIF formatted model file or 514 | just its name if directory is specified. It can also be a list of file_names. 515 | directory : str 516 | directory where the UBC-GIF file(s) lives (optional) 517 | 518 | Returns 519 | ------- 520 | (n_cells) numpy.ndarray or dict of [str, (n_cells) numpy.ndarray] 521 | The model defined on the mesh. If **file_name** is a ``dict``, it is a 522 | dictionary of models indexed by the file names. 523 | """ 524 | if directory is None: 525 | directory = "" 526 | if type(file_name) is list: 527 | out = {} 528 | for f in file_name: 529 | out[f] = mesh.read_model_UBC(f, directory=directory) 530 | return out 531 | 532 | modArr = np.loadtxt(file_name) 533 | 534 | ubc_order = mesh._ubc_order 535 | # order_ubc will re-order from treemesh ordering to UBC ordering 536 | # need the opposite operation 537 | un_order = np.empty_like(ubc_order) 538 | un_order[ubc_order] = np.arange(len(ubc_order)) 539 | 540 | model = modArr[un_order].copy() # ensure a contiguous array 541 | return model 542 | 543 | def write_UBC(mesh, file_name, models=None, directory=None): 544 | """Write OcTree mesh (and models) to UBC-GIF formatted files. 545 | 546 | Parameters 547 | ---------- 548 | file_name : str 549 | full path for the output mesh file or just its name if directory is specified 550 | models : dict of [str, (n_cells) numpy.ndarray], optional 551 | The dictionary key is a string representing the model's name. 552 | Each model is a 1D numpy array of size (n_cells). 553 | directory : str, optional 554 | output directory (optional) 555 | """ 556 | if directory is None: 557 | directory = "" 558 | uniform_hs = np.array([np.allclose(h, h[0]) for h in mesh.h]) 559 | if np.any(~uniform_hs): 560 | raise Exception("UBC form does not support variable cell widths") 561 | nCunderMesh = np.array([h.size for h in mesh.h], dtype=np.int64) 562 | 563 | tswCorn = mesh.origin.copy() 564 | tswCorn[-1] += np.sum(mesh.h[-1]) 565 | 566 | smallCell = np.array([h[0] for h in mesh.h]) 567 | nrCells = mesh.nC 568 | 569 | indArr, levels = mesh._ubc_indArr 570 | ubc_order = mesh._ubc_order 571 | 572 | indArr = indArr[ubc_order] 573 | levels = levels[ubc_order] 574 | 575 | # Write the UBC octree mesh file 576 | head = " ".join([f"{int(n)}" for n in nCunderMesh]) + " \n" 577 | head += " ".join([f"{v:.4f}" for v in tswCorn]) + " \n" 578 | head += " ".join([f"{v:.3f}" for v in smallCell]) + " \n" 579 | head += f"{int(nrCells)}" 580 | np.savetxt(file_name, np.c_[indArr, levels], fmt="%i", header=head, comments="") 581 | 582 | # Print the models 583 | if models is None: 584 | return 585 | if not isinstance(models, dict): 586 | raise TypeError("models must be a dict") 587 | for key in models: 588 | mesh.write_model_UBC(key, models[key], directory=directory) 589 | 590 | def write_model_UBC(mesh, file_name, model, directory=None): 591 | """Write 3D tree model (OcTree) to UBC-GIF formatted file. 592 | 593 | Parameters 594 | ---------- 595 | file_name : str 596 | full path for the output mesh file or just its name if directory is specified 597 | model : (n_cells) numpy.ndarray 598 | model values defined for each cell 599 | directory : str 600 | output directory (optional) 601 | """ 602 | if directory is None: 603 | directory = "" 604 | if type(file_name) is list: 605 | for f, m in zip(file_name, model): 606 | mesh.write_model_UBC(f, m) 607 | else: 608 | ubc_order = mesh._ubc_order 609 | fname = os.path.join(directory, file_name) 610 | m = model[ubc_order] 611 | np.savetxt(fname, m) 612 | 613 | # DEPRECATED 614 | @classmethod 615 | def readUBC(TreeMesh, file_name, directory=""): 616 | """Read 3D Tree mesh from UBC-GIF formatted file. 617 | 618 | *readUBC* has been deprecated and replaced by *read_UBC* 619 | 620 | See Also 621 | -------- 622 | read_UBC 623 | """ 624 | raise NotImplementedError( 625 | "TensorMesh.readUBC has been removed and this be removed in" 626 | "discretize 1.0.0. please use TensorMesh.read_UBC", 627 | ) 628 | 629 | readModelUBC = deprecate_method( 630 | "read_model_UBC", "readModelUBC", removal_version="1.0.0", error=True 631 | ) 632 | writeUBC = deprecate_method( 633 | "write_UBC", "writeUBC", removal_version="1.0.0", error=True 634 | ) 635 | writeModelUBC = deprecate_method( 636 | "write_model_UBC", "writeModelUBC", removal_version="1.0.0", error=True 637 | ) 638 | 639 | 640 | class SimplexMeshIO(InterfaceSimplexReadVTK): 641 | """Empty class for future text based IO of a SimplexMesh.""" 642 | 643 | pass 644 | --------------------------------------------------------------------------------