Repository: simpeg/discretize Branch: main Commit: c7763885314e Files: 214 Total size: 2.1 MB Directory structure: gitextract_xzf28t4b/ ├── .ci/ │ ├── azure/ │ │ ├── deploy.yml │ │ ├── docs.yml │ │ ├── run_tests.sh │ │ ├── sdist.yml │ │ ├── setup_env.sh │ │ ├── setup_miniconda_macos.sh │ │ ├── style.yml │ │ └── test.yml │ ├── environment_test.yml │ ├── environment_test_bare.yml │ ├── install_style.sh │ ├── parse_style_requirements.py │ └── setup_headless_display.sh ├── .git_archival.txt ├── .gitattributes ├── .github/ │ └── workflows/ │ └── build_distributions.yml ├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS.rst ├── CITATION.rst ├── LICENSE ├── README.rst ├── azure-pipelines.yml ├── discretize/ │ ├── Tests/ │ │ ├── __init__.py │ │ └── meson.build │ ├── View.py │ ├── __init__.py │ ├── _extensions/ │ │ ├── __init__.py │ │ ├── geom.cpp │ │ ├── geom.h │ │ ├── geom.pxd │ │ ├── interputils_cython.pxd │ │ ├── interputils_cython.pyx │ │ ├── meson.build │ │ ├── simplex_helpers.pyx │ │ ├── tree.cpp │ │ ├── tree.h │ │ ├── tree.pxd │ │ ├── tree_ext.pyx │ │ └── triplet.h │ ├── base/ │ │ ├── __init__.py │ │ ├── base_mesh.py │ │ ├── base_regular_mesh.py │ │ ├── base_tensor_mesh.py │ │ └── meson.build │ ├── curvilinear_mesh.py │ ├── cylindrical_mesh.py │ ├── meson.build │ ├── mixins/ │ │ ├── __init__.py │ │ ├── mesh_io.py │ │ ├── meson.build │ │ ├── mpl_mod.py │ │ ├── omf_mod.py │ │ └── vtk_mod.py │ ├── operators/ │ │ ├── __init__.py │ │ ├── differential_operators.py │ │ ├── inner_products.py │ │ └── meson.build │ ├── tensor_cell.py │ ├── tensor_mesh.py │ ├── tests.py │ ├── tree_mesh.py │ ├── unstructured_mesh.py │ └── utils/ │ ├── __init__.py │ ├── code_utils.py │ ├── codeutils.py │ ├── coordinate_utils.py │ ├── coordutils.py │ ├── curvilinear_utils.py │ ├── curvutils.py │ ├── interpolation_utils.py │ ├── interputils.py │ ├── io_utils.py │ ├── matrix_utils.py │ ├── matutils.py │ ├── mesh_utils.py │ ├── meshutils.py │ └── meson.build ├── docs/ │ ├── Makefile │ ├── _static/ │ │ ├── css/ │ │ │ └── custom.css │ │ └── versions.json │ ├── _templates/ │ │ └── autosummary/ │ │ ├── attribute.rst │ │ ├── base.rst │ │ ├── class.rst │ │ ├── function.rst │ │ └── method.rst │ ├── api/ │ │ ├── discretize.base.rst │ │ ├── discretize.mixins.rst │ │ ├── discretize.operators.rst │ │ ├── discretize.rst │ │ ├── discretize.tests.rst │ │ ├── discretize.utils.rst │ │ └── index.rst │ ├── conf.py │ ├── content/ │ │ ├── additional_resources.rst │ │ ├── big_picture.rst │ │ ├── finite_volume.rst │ │ ├── getting_started.rst │ │ ├── inner_products.rst │ │ ├── installing.rst │ │ ├── theory.rst │ │ └── user_guide.rst │ ├── index.rst │ ├── make.bat │ └── release/ │ ├── 0.10.0-notes.rst │ ├── 0.11.0-notes.rst │ ├── 0.11.1-notes.rst │ ├── 0.11.2-notes.rst │ ├── 0.11.3-notes.rst │ ├── 0.12.0-notes.rst │ ├── 0.4.12-notes.rst │ ├── 0.4.13-notes.rst │ ├── 0.4.14-notes.rst │ ├── 0.4.15-notes.rst │ ├── 0.5.0-notes.rst │ ├── 0.5.1-notes.rst │ ├── 0.6.0-notes.rst │ ├── 0.6.1-notes.rst │ ├── 0.6.2-notes.rst │ ├── 0.6.3-notes.rst │ ├── 0.7.0-notes.rst │ ├── 0.7.1-notes.rst │ ├── 0.7.2-notes.rst │ ├── 0.7.3-notes.rst │ ├── 0.7.4-notes.rst │ ├── 0.8.0-notes.rst │ ├── 0.8.1-notes.rst │ ├── 0.8.2-notes.rst │ ├── 0.8.3-notes.rst │ ├── 0.9.0-notes.rst │ └── index.rst ├── examples/ │ ├── README.txt │ ├── plot_cahn_hilliard.py │ ├── plot_cyl_mirror.py │ ├── plot_dc_resistivity.py │ ├── plot_image.py │ ├── plot_pyvista_laguna.py │ ├── plot_quadtree_divergence.py │ ├── plot_quadtree_hanging.py │ ├── plot_slicer_demo.py │ └── plot_streamThickness.py ├── meson.build ├── meson.options ├── pyproject.toml ├── tests/ │ ├── __init__.py │ ├── base/ │ │ ├── __init__.py │ │ ├── test_basemesh.py │ │ ├── test_coordutils.py │ │ ├── test_curvilinear.py │ │ ├── test_curvilinear_vtk.py │ │ ├── test_interpolation.py │ │ ├── test_operators.py │ │ ├── test_properties.py │ │ ├── test_slicer.py │ │ ├── test_tensor.py │ │ ├── test_tensor_cell.py │ │ ├── test_tensor_innerproduct.py │ │ ├── test_tensor_innerproduct_derivs.py │ │ ├── test_tensor_io.py │ │ ├── test_tensor_omf.py │ │ ├── test_tensor_vtk.py │ │ ├── test_tests.py │ │ ├── test_utils.py │ │ ├── test_view.py │ │ └── test_volume_avg.py │ ├── boundaries/ │ │ ├── test_boundary_integrals.py │ │ ├── test_boundary_maxwell.py │ │ ├── test_boundary_poisson.py │ │ ├── test_errors.py │ │ ├── test_tensor_boundary.py │ │ └── test_tensor_boundary_poisson.py │ ├── cyl/ │ │ ├── __init__.py │ │ ├── test_cyl.py │ │ ├── test_cyl3D.py │ │ ├── test_cylOperators.py │ │ ├── test_cyl_counting.py │ │ ├── test_cyl_innerproducts.py │ │ ├── test_cyl_io.py │ │ └── test_cyl_operators.py │ ├── simplex/ │ │ ├── __init__.py │ │ ├── test_inner_products.py │ │ ├── test_interpolation.py │ │ ├── test_operators.py │ │ └── test_utils.py │ └── tree/ │ ├── __init__.py │ ├── test_intersections.py │ ├── test_refine.py │ ├── test_safeguards.py │ ├── test_tree.py │ ├── test_tree_balancing.py │ ├── test_tree_innerproduct_derivs.py │ ├── test_tree_interpolation.py │ ├── test_tree_io.py │ ├── test_tree_operators.py │ ├── test_tree_plotting.py │ ├── test_tree_utils.py │ └── test_tree_vtk.py └── tutorials/ ├── inner_products/ │ ├── 1_basic.py │ ├── 2_physical_properties.py │ ├── 3_calculus.py │ ├── 4_advanced.py │ └── README.txt ├── mesh_generation/ │ ├── 1_mesh_overview.py │ ├── 2_tensor_mesh.py │ ├── 3_cylindrical_mesh.py │ ├── 4_tree_mesh.py │ └── README.txt ├── operators/ │ ├── 1_averaging.py │ ├── 2_differential.py │ └── README.txt └── pde/ ├── 1_poisson.py ├── 2_advection_diffusion.py ├── 3_nodal_dirichlet_poisson.py └── README.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ci/azure/deploy.yml ================================================ jobs: - job: displayName: "Deploy Docs" pool: vmImage: ubuntu-latest steps: # No need to checkout the repo here! - checkout: none - bash: | echo $IS_TAG echo $IS_MAIN echo $BRANCH_NAME displayName: Report branch parameters - task: DownloadPipelineArtifact@2 inputs: buildType: 'current' artifactName: 'html_docs' targetPath: 'html' - bash: | ls -l html displayName: Report downloaded cache contents. - bash: | git config --global user.name ${GH_NAME} git config --global user.email ${GH_EMAIL} git config --list | grep user. displayName: 'Configure git' env: GH_NAME: $(gh.name) GH_EMAIL: $(gh.email) # upload documentation to discretize-docs gh-pages on tags - bash: | git clone -q --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/simpeg/discretize-docs.git displayName: Checkout doc repository env: GH_TOKEN: $(gh.token) - bash: | cd discretize-docs rm -rf "en/$BRANCH_NAME" mv ../html "en/$BRANCH_NAME" touch .nojekyll displayName: Set Doc Folder - bash: | # Update latest symlink cd discretize-docs/en rm -f latest ln -s "$BRANCH_NAME" latest displayName: Point Latest to tag condition: eq(variables.IS_TAG, true) - bash: | # Commit and push cd discretize-docs git add --all git commit -am "Azure CI commit ref $(Build.SourceVersion)" git push displayName: Push documentation to discretize-docs env: GH_TOKEN: $(gh.token) ================================================ FILE: .ci/azure/docs.yml ================================================ jobs: - job: BuildDocs displayName: "Build Documentation" pool: vmImage: ubuntu-latest variables: python.version: "3.11" doc.build: True PYVISTA_OFF_SCREEN: True DISPLAY: ":99" steps: - bash: git fetch --tags displayName: Fetch tags - bash: echo "##vso[task.prependpath]$CONDA/bin" displayName: Add conda to PATH - bash: .ci/azure/setup_env.sh displayName: Setup discretize environment - bash: | source activate discretize-test make -C docs html displayName: 'Building HTML' - bash: | source activate discretize-test make -C docs linkcheck displayName: 'Checking Links' - task: PublishPipelineArtifact@1 inputs: targetPath: 'docs/_build/html' artifact: 'html_docs' parallel: true ================================================ FILE: .ci/azure/run_tests.sh ================================================ #!/bin/bash set -ex #echo on and exit if any line fails # TF_BUILD is set to True on azure pipelines. is_azure=$(echo "${TF_BUILD:-false}" | tr '[:upper:]' '[:lower:]') do_doc=$(echo "${DOC_BUILD:-false}" | tr '[:upper:]' '[:lower:]') do_cov=$(echo "${COVERAGE:-false}" | tr '[:upper:]' '[:lower:]') test_args="" source activate discretize-test if [[ "$is_azure" == "true" ]]; then if [[ "$do_doc" == "true" ]]; then .ci/setup_headless_display.sh fi fi if [[ "do_cov" == "true" ]]; then echo "Testing with coverage" test_args="--cov --cov-config=pyproject.toml $test_args" fi pytest -vv $test_args if [[ "do_cov" == "true" ]]; then coverage xml fi ================================================ FILE: .ci/azure/sdist.yml ================================================ jobs: - job: displayName: "Build source dist." pool: vmImage: ubuntu-latest steps: - task: UsePythonVersion@0 inputs: versionSpec: "3.11" - bash: git fetch --tags displayName: Fetch tags - bash: | set -o errexit python -m pip install --upgrade pip pip install build displayName: Install source build tools. - bash: | python -m build --skip-dependency-check --sdist . ls -la dist displayName: Build Source - task: PublishPipelineArtifact@1 inputs: targetPath: 'dist' artifact: 'source_dist' ================================================ FILE: .ci/azure/setup_env.sh ================================================ #!/bin/bash set -ex #echo on and exit if any line fails # TF_BUILD is set to True on azure pipelines. is_azure=$(echo "${TF_BUILD:-false}" | tr '[:upper:]' '[:lower:]') do_doc=$(echo "${DOC_BUILD:-false}" | tr '[:upper:]' '[:lower:]') is_free_threaded=$(echo "${PYTHON_FREETHREADING:-false}" | tr '[:upper:]' '[:lower:]') is_rc=$(echo "${PYTHON_RELEASE_CANDIDATE:-false}" | tr '[:upper:]' '[:lower:]') is_bare=$(echo "${ENVIRON_BARE:-false}" | tr '[:upper:]' '[:lower:]') if [[ "$is_azure" == "true" ]]; then if [[ "$do_doc" == "true" ]]; then .ci/setup_headless_display.sh fi fi if [[ "$is_free_threaded" == "true" || "$is_bare" == "true" ]]; then cp .ci/environment_test_bare.yml environment_test_with_pyversion.yml echo " - python-freethreading="$PYTHON_VERSION >> environment_test_with_pyversion.yml else cp .ci/environment_test.yml environment_test_with_pyversion.yml echo " - python="$PYTHON_VERSION >> environment_test_with_pyversion.yml fi if [[ "$is_rc" == "true" ]]; then sed -i '/^channels:/a\ - conda-forge/label/python_rc' environment_test_with_pyversion.yml fi conda env create --file environment_test_with_pyversion.yml rm environment_test_with_pyversion.yml if [[ "$is_azure" == "true" ]]; then source activate discretize-test pip install pytest-azurepipelines else conda activate discretize-test fi # The --vsenv config setting will prefer msvc compilers on windows. # but will do nothing on mac and linux. pip install --no-build-isolation --editable . --config-settings=setup-args="--vsenv" echo "Conda Environment:" conda list echo "Installed discretize version:" python -c "import discretize; print(discretize.__version__)" ================================================ FILE: .ci/azure/setup_miniconda_macos.sh ================================================ #!/bin/bash set -ex #echo on and exit if any line fails echo "arch is $ARCH" if [[ $ARCH == "X64" ]]; then MINICONDA_ARCH_LABEL="x86_64" else MINICONDA_ARCH_LABEL="arm64" fi echo $MINICONDA_ARCH_LABEL mkdir -p ~/miniconda3 curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-$MINICONDA_ARCH_LABEL.sh -o ~/miniconda3/miniconda.sh bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3 rm ~/miniconda3/miniconda.sh echo "##vso[task.setvariable variable=CONDA;]${HOME}/miniconda3" ================================================ FILE: .ci/azure/style.yml ================================================ jobs: - job: displayName: Run style checks with Black pool: vmImage: ubuntu-latest steps: - task: UsePythonVersion@0 inputs: versionSpec: "3.11" - bash: .ci/install_style.sh displayName: "Install dependencies to run the checks" - script: black --check . displayName: "Run black" - job: displayName: Run (permissive) style checks with flake8 pool: vmImage: ubuntu-latest steps: - task: UsePythonVersion@0 inputs: versionSpec: "3.11" - bash: .ci/install_style.sh displayName: "Install dependencies to run the checks" - script: flake8 displayName: "Run flake8" ================================================ FILE: .ci/azure/test.yml ================================================ jobs: - job: strategy: matrix: linux-Python311: image: ubuntu-latest python.version: '3.11' coverage: True linux-Python312: image: ubuntu-latest python.version: '3.12' linux-Python313: image: ubuntu-latest python.version: '3.13' linux-Python313t: image: ubuntu-latest python.version: '3.13' environ.bare: True python.freethreading: True coverage: True linux-Python314: image: ubuntu-latest environ.bare: True python.version: '3.14' linux-Python314t: image: ubuntu-latest python.version: '3.14' environ.bare: True python.freethreading: True osx-Python311: image: macOS-latest python.version: '3.11' osx-Python312: image: macOS-latest python.version: '3.12' osx-Python313: image: macOS-latest python.version: '3.13' osx-Python313t: image: macOS-latest python.version: '3.13' python.freethreading: True osx-Python314: image: macOS-latest python.version: '3.14' environ.bare: True osx-Python314t: image: macOS-latest python.version: '3.14' environ.bare: True python.freethreading: True win-Python311: image: windows-latest python.version: '3.11' win-Python312: image: windows-latest python.version: '3.12' win-Python313: image: windows-latest python.version: '3.13' win-Python313t: image: windows-latest python.version: '3.13' environ.bare: True python.freethreading: True win-Python314: image: windows-latest environ.bare: True python.version: '3.14' win-Python314t: image: windows-latest python.version: '3.14' environ.bare: True python.freethreading: True displayName: "${{ variables.image }} ${{ variables.python.version }}" pool: vmImage: $(image) variables: varOS: $(Agent.OS) ARCH: $(Agent.OSArchitecture) steps: - bash: .ci/azure/setup_miniconda_macos.sh displayName: Install miniconda on mac condition: eq(variables.varOS, 'Darwin') - bash: echo "##vso[task.prependpath]$CONDA/bin" displayName: Add conda to PATH condition: ne(variables.varOS, 'Windows_NT') - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH condition: eq(variables.varOS, 'Windows_NT') - bash: .ci/azure/setup_env.sh displayName: Setup discretize environment - bash: .ci/azure/run_tests.sh displayName: 'Testing' - bash: | curl -Os https://uploader.codecov.io/latest/linux/codecov chmod +x codecov ./codecov displayName: 'Upload coverage to codecov.io' condition: variables.coverage ================================================ FILE: .ci/environment_test.yml ================================================ name: discretize-test channels: - conda-forge dependencies: - numpy>=1.22.4 - scipy>=1.8 # optionals - vtk>=6 - pyvista - omf - matplotlib # documentation - sphinx==8.1.3 - pydata-sphinx-theme==0.16.1 - sphinx-gallery==0.19.0 - numpydoc==1.9.0 - jupyter - graphviz - pillow - pooch # testing - sympy - pytest - pytest-cov # Building - pip - meson-python>=0.14.0 - meson - ninja - cython>=3.1.0 - setuptools_scm ================================================ FILE: .ci/environment_test_bare.yml ================================================ name: discretize-test channels: - conda-forge dependencies: - numpy>=1.22.4 - scipy>=1.12 # testing - sympy - pytest - pytest-cov # Building - pip - meson-python>=0.14.0 - meson - ninja - cython>=3.1.0 - setuptools_scm ================================================ FILE: .ci/install_style.sh ================================================ #!/bin/bash set -ex #echo on and exit if any line fails # get directory of this script script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) style_script=$script_dir/parse_style_requirements.py # parse the style requirements requirements=$(python $style_script) pip install $requirements ================================================ FILE: .ci/parse_style_requirements.py ================================================ import tomllib import pathlib root_dir = pathlib.Path(__file__).parent.parent.resolve() pyproject_file = root_dir / "pyproject.toml" with open(pyproject_file, "rb") as f: pyproject = tomllib.load(f) style_requirements = pyproject["project"]["optional-dependencies"]["style"] for req in style_requirements: print(req) ================================================ FILE: .ci/setup_headless_display.sh ================================================ #!/bin/sh set -x sudo apt update # Install items for headless pyvista display. sudo apt-get install -y \ libglx-mesa0 \ libgl1 \ xvfb \ x11-xserver-utils # qt dependents sudo apt-get install -y \ libdbus-1-3 \ libegl1 \ libopengl0 \ libosmesa6 \ libxcb-cursor0 \ libxcb-icccm4 \ libxcb-image0 \ libxcb-keysyms1 \ libxcb-randr0 \ libxcb-render-util0 \ libxcb-shape0 \ libxcb-xfixes0 \ libxcb-xinerama0 \ libxcb-xinput0 \ libxkbcommon-x11-0 \ mesa-utils \ x11-utils which Xvfb Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & # Debugging commands: # ls -l /etc/init.d/ # sh -e /etc/init.d/xvfb start # give xvfb some time to start sleep 3 set +x ================================================ FILE: .git_archival.txt ================================================ node: $Format:%H$ node-date: $Format:%cI$ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ ref-names: $Format:%D$ ================================================ FILE: .gitattributes ================================================ .git_archival.txt export-subst # Excluding files from an sdist generated by meson-python .azure-pipelines/* export-ignore .ci/* export-ignore docs/* export-ignore examples/* export-ignore tests/* export-ignore tutorials/* export-ignore .coveragerc export-ignore .flake8 export-ignore .git* export-ignore *.yml export-ignore *.yaml export-ignore requirements_style.txt export-ignore ================================================ FILE: .github/workflows/build_distributions.yml ================================================ name: Build Distribution artifacts on: [push, pull_request] jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: # macos-15-intel is an Intel runner, macos-14 is Apple silicon os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-15-intel, macos-14] steps: - uses: actions/checkout@v5 - name: Build wheels uses: pypa/cibuildwheel@v3.2.0 - uses: actions/upload-artifact@v4 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl build_sdist: name: Build source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build sdist run: pipx run build --sdist - uses: actions/upload-artifact@v4 with: name: cibw-sdist path: dist/*.tar.gz upload_pypi: needs: [build_wheels, build_sdist] runs-on: ubuntu-latest environment: pypi permissions: id-token: write if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/download-artifact@v4 with: # unpacks all CIBW artifacts into dist/ pattern: cibw-* path: dist merge-multiple: true - uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} skip-existing: true packages-dir: ./dist/ ================================================ FILE: .gitignore ================================================ *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject *.sublime-project *.sublime-workspace .DS_Store tree_ext.cpp simplex_helpers.cpp interputils_cython.c # Jupyter *.ipynb # docs docs/_build/ docs/warnings.txt docs/api/generated/* docs/examples docs/gallery/* docs/tutorials/* # Examples data /examples/Chile_GRAV_4_Miller /examples/*.tar.gz examples/Chile_GRAV_4_Miller examples/*.tar.gz # downloads for examples *.tar.gz *.dat *.dir *.bak # setuptools_scm discretize/version.py .idea/ docs/sg_execution_times.rst ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.3.0 hooks: - id: black language_version: python3.11 - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 language_version: python3.11 additional_dependencies: - flake8-bugbear==23.12.2 - flake8-builtins==2.2.0 - flake8-mutable==1.2.0 - flake8-rst-docstrings==0.3.0 - flake8-docstrings==1.7.0 - flake8-pyproject==1.2.3 ================================================ FILE: AUTHORS.rst ================================================ - Rowan Cockett, (`@rowanc1 `_) - Lindsey Heagy, (`@lheagy `_) - Seogi Kang, (`@sgkang `_) - Brendan Smithyman, (`@bsmithyman `_) - Gudni Rosenkjaer, (`@grosenkj `_) - Dom Fournier, (`@fourndo `_) - Dave Marchant, (`@dwfmarchant `_) - Lars Ruthotto, (`@lruthotto `_) - Mike Wathen, (`@wathenmp `_) - Luz Angelica Caudillo-Mata, (`@lacmajedrez `_) - Eldad Haber, (`@eldadhaber `_) - Doug Oldenburg, (`@dougoldenburg `_) - Devin Cowan, (`@dccowan `_) - Adam Pidlisecky, (`@aPid1 `_) - Dieter Werthmüller, (`@prisae `_) - Bane Sullivan, (`@banesullivan `_) - Joseph Capriotti, (`@jcapriot `_) ================================================ FILE: CITATION.rst ================================================ Citing discretize ----------------- There is a `paper about discretize `_ as it is used in SimPEG, if you use this code, please help our scientific visibility by citing our work! 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. BibTex: .. code:: Latex @article{Cockett2015, title={SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications}, author={Cockett, Rowan and Kang, Seogi and Heagy, Lindsey J and Pidlisecky, Adam and Oldenburg, Douglas W}, journal={Computers \& Geosciences}, year={2015}, publisher={Elsevier} } ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2013-2025 SimPEG Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.rst ================================================ .. image:: https://raw.github.com/simpeg/discretize/main/docs/images/discretize-logo.png :alt: Discretize Logo discretize ========== .. image:: https://img.shields.io/pypi/v/discretize.svg :target: https://pypi.python.org/pypi/discretize :alt: Latest PyPI version .. image:: https://anaconda.org/conda-forge/discretize/badges/version.svg :target: https://anaconda.org/conda-forge/discretize :alt: Latest conda-forge version .. image:: https://img.shields.io/github/license/simpeg/simpeg.svg :target: https://github.com/simpeg/discretize/blob/main/LICENSE :alt: MIT license .. image:: https://dev.azure.com/simpeg/discretize/_apis/build/status/simpeg.discretize?branchName=main :target: https://dev.azure.com/simpeg/discretize/_build/latest?definitionId=1&branchName=main :alt: Azure pipelines build status .. image:: https://codecov.io/gh/simpeg/discretize/branch/main/graph/badge.svg :target: https://codecov.io/gh/simpeg/discretize :alt: Coverage status .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.596411.svg :target: https://doi.org/10.5281/zenodo.596411 .. image:: https://img.shields.io/discourse/users?server=http%3A%2F%2Fsimpeg.discourse.group%2F :target: http://simpeg.discourse.group/ .. image:: https://img.shields.io/badge/simpeg-purple?logo=mattermost&label=Mattermost :target: https://mattermost.softwareunderground.org/simpeg .. image:: https://img.shields.io/badge/Youtube%20channel-GeoSci.xyz-FF0000.svg?logo=youtube :target: https://www.youtube.com/channel/UCBrC4M8_S4GXhyHht7FyQqw **discretize** - A python package for finite volume discretization. The vision is to create a package for finite volume simulation with a focus on large scale inverse problems. This package has the following features: * modular with respect to the spacial discretization * built with the inverse problem in mind * supports 1D, 2D and 3D problems * access to sparse matrix operators * access to derivatives to mesh variables .. image:: https://raw.githubusercontent.com/simpeg/figures/master/finitevolume/cell-anatomy-tensor.png Currently, discretize supports: * Tensor Meshes (1D, 2D and 3D) * Cylindrically Symmetric Meshes * QuadTree and OcTree Meshes (2D and 3D) * Logically Rectangular Meshes (2D and 3D) * Triangular (2D) and Tetrahedral (3D) Meshes Installing ^^^^^^^^^^ **discretize** is on conda-forge, and is the recommended installation method. .. code:: shell conda install -c conda-forge discretize Prebuilt wheels of **discretize** are on pypi for most platforms .. code:: shell pip install discretize To install from source, note this requires a `c++` compiler supporting the `c++17` standard. .. code:: shell git clone https://github.com/simpeg/discretize.git cd discretize pip install . Citing discretize ^^^^^^^^^^^^^^^^^ Please cite the SimPEG paper when using discretize in your work: 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. **BibTex:** .. code:: Latex @article{cockett2015simpeg, title={SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications}, author={Cockett, Rowan and Kang, Seogi and Heagy, Lindsey J and Pidlisecky, Adam and Oldenburg, Douglas W}, journal={Computers \& Geosciences}, year={2015}, publisher={Elsevier} } Links ^^^^^ Website: http://simpeg.xyz Documentation: http://discretize.simpeg.xyz Code: https://github.com/simpeg/discretize Tests: https://dev.azure.com/simpeg/discretize/_build Bugs & Issues: https://github.com/simpeg/discretize/issues Questions: http://simpeg.discourse.group/ Chat: https://mattermost.softwareunderground.org/simpeg ================================================ FILE: azure-pipelines.yml ================================================ trigger: branches: include: - 'main' exclude: - '*no-ci*' tags: include: - '*' pr: branches: include: - '*' exclude: - '*no-ci*' variables: BRANCH_NAME: $(Build.SourceBranchName) IS_TAG: $[startsWith(variables['Build.SourceBranch'], 'refs/tags/')] IS_MAIN: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] stages: - stage: StyleChecks displayName: "Style Checks" jobs: - template: .ci/azure/style.yml - stage: Testing dependsOn: StyleChecks jobs: - template: .ci/azure/test.yml - stage: DocBuild dependsOn: StyleChecks jobs: - template: .ci/azure/docs.yml - stage: Deploy displayName: "Deploy Docs" dependsOn: - Testing - DocBuild condition: and(succeeded(), or(eq(variables.IS_TAG, true), eq(variables.IS_MAIN, true))) jobs: - template: .ci/azure/deploy.yml ================================================ FILE: discretize/Tests/__init__.py ================================================ from discretize.tests import * # NOQA F401,F403 from discretize.utils.code_utils import deprecate_module # note this needs to be a module with an __init__ so we can avoid name clash # with tests.py in the discretize directory on systems that are agnostic to Case. deprecate_module( "discretize.Tests", "discretize.tests", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/Tests/meson.build ================================================ python_sources = [ '__init__.py', ] py.install_sources( python_sources, subdir: 'discretize/Tests' ) ================================================ FILE: discretize/View.py ================================================ """Deprecated view module.""" from discretize.utils.code_utils import deprecate_module deprecate_module( "discretize.View", "discretize.mixins.mpl_mod", removal_version="1.0.0", error=True, ) try: from discretize.mixins.mpl_mod import Slicer # NOQA F401 except ImportError: pass ================================================ FILE: discretize/__init__.py ================================================ """ ===================================== Discretize Meshes (:mod:`discretize`) ===================================== .. currentmodule:: discretize The ``discretize`` package contains four types of meshes for soliving partial differential equations using the finite volume method. Mesh Classes ============ .. autosummary:: :toctree: generated/ TensorMesh CylindricalMesh CurvilinearMesh TreeMesh SimplexMesh Mesh Cells ========== The :class:`~discretize.tensor_cell.TensorCell` and :class:`~discretize.tree_mesh.TreeCell` classes were designed specifically to define the cells within tensor and tree meshes, respectively. Instances of :class:`~discretize.tree_mesh.TreeCell` and :class:`~discretize.tensor_cell.TensorCell` are not meant to be created on their own. However, they can be returned directly by indexing a particular cell within a tensor or tree mesh. .. autosummary:: :toctree: generated/ tensor_cell.TensorCell tree_mesh.TreeCell """ from discretize.tensor_mesh import TensorMesh from discretize.cylindrical_mesh import CylMesh, CylindricalMesh from discretize.curvilinear_mesh import CurvilinearMesh from discretize.unstructured_mesh import SimplexMesh from discretize.utils.io_utils import load_mesh from .tensor_cell import TensorCell try: from discretize.tree_mesh import TreeMesh except ImportError as err: import os # Check if being called from non-standard location (i.e. a git repository) # is tree_ext.pyx here? will not be in the folder if installed to site-packages... file_test = os.path.dirname(os.path.abspath(__file__)) + "/_extensions/tree_ext.pyx" if os.path.isfile(file_test): # Then we are being run from a repository raise ImportError( """ It would appear that discretize is being imported from its source code directory and is unable to load its compiled extension modules. Try changing your directory and re-launching your python interpreter. If this was intentional, you need to install discretize in an editable mode. """ ) else: raise err from discretize import tests __author__ = "SimPEG Team" __license__ = "MIT" __copyright__ = "2013 - 2023, SimPEG Developers, https://simpeg.xyz" from importlib.metadata import version, PackageNotFoundError # Version try: # - Released versions just tags: 0.8.0 # - GitHub commits add .dev#+hash: 0.8.1.dev4+g2785721 # - Uncommitted changes add timestamp: 0.8.1.dev4+g2785721.d20191022 __version__ = version("discretize") except PackageNotFoundError: # If it was not installed, then we don't know the version. We could throw a # warning here, but this case *should* be rare. discretize should be # installed properly! from datetime import datetime __version__ = "unknown-" + datetime.today().strftime("%Y%m%d") ================================================ FILE: discretize/_extensions/__init__.py ================================================ ================================================ FILE: discretize/_extensions/geom.cpp ================================================ #include #include #include "geom.h" #include #include // Define the 3D cross product as a pre-processor macro #define CROSS3D(e0, e1, out) \ out[0] = e0[1] * e1[2] - e0[2] * e1[1]; \ out[1] = e0[2] * e1[0] - e0[0] * e1[2]; \ out[2] = e0[0] * e1[1] - e0[1] * e1[0]; // simple geometric objects for intersection tests with an aabb Geometric::Geometric(){ dim = 0; } Geometric::Geometric(int_t dim){ this->dim = dim; } Ball::Ball() : Geometric(){ x0 = NULL; r = 0; rsq = 0; } Ball::Ball(int_t dim, double* x0, double r) : Geometric(dim){ this->x0 = x0; this->r = r; this->rsq = r * r; } bool Ball::intersects_cell(double *a, double *b) const{ // check if I intersect the ball double dx; double r2_test = 0.0; for(int_t i=0; ix0 = x0; this->x1 = x1; for(int_t i=0; i::infinity(); double t_far = std::numeric_limits::infinity(); double t0, t1; for(int_t i=0; i b[i])){ return false; } if(std::max(x0[i], x1[i]) < a[i]){ return false; } if(std::min(x0[i], x1[i]) > b[i]){ return false; } if (x0[i] != x1[i]){ t0 = (a[i] - x0[i]) * inv_dx[i]; t1 = (b[i] - x0[i]) * inv_dx[i]; if (t0 > t1){ std::swap(t0, t1); } t_near = std::max(t_near, t0); t_far = std::min(t_far, t1); if (t_near > t_far || t_far < 0 || t_near > 1){ return false; } } } return true; } Box::Box() : Geometric(){ x0 = NULL; x1 = NULL; } Box::Box(int_t dim, double* x0, double *x1) : Geometric(dim){ this->x0 = x0; this->x1 = x1; } bool Box::intersects_cell(double *a, double *b) const{ for(int_t i=0; i b[i]){ return false; } } return true; } Plane::Plane() : Geometric(){ origin = NULL; normal = NULL; } Plane::Plane(int_t dim, double* origin, double *normal) : Geometric(dim){ this->origin = origin; this->normal = normal; } bool Plane::intersects_cell(double *a, double *b) const{ double center; double half_width; double s = 0.0; double r = 0.0; for(int_t i=0;ix0 = x0; this->x1 = x1; this->x2 = x2; for(int_t i=0; i 2){ normal[0] = e0[1] * e1[2] - e0[2] * e1[1]; normal[1] = e0[2] * e1[0] - e0[0] * e1[2]; normal[2] = e0[0] * e1[1] - e0[1] * e1[0]; } } bool Triangle::intersects_cell(double *a, double *b) const{ double center; double v0[3], v1[3], v2[3], half[3]; double vmin, vmax; double p0, p1, p2, pmin, pmax, rad; for(int_t i=0; i < dim; ++i){ center = 0.5 * (b[i] + a[i]); v0[i] = x0[i] - center; v1[i] = x1[i] - center; vmin = std::min(v0[i], v1[i]); vmax = std::max(v0[i], v1[i]); v2[i] = x2[i] - center; vmin = std::min(vmin, v2[i]); vmax = std::max(vmax, v2[i]); half[i] = center - a[i]; // Bounding box check if (vmin > half[i] || vmax < -half[i]){ return false; } } // first do the 3 edge cross tests that apply in 2D and 3D // edge 0 cross z_hat //p0 = e0[1] * v0[0] - e0[0] * v0[1]; p1 = e0[1] * v1[0] - e0[0] * v1[1]; p2 = e0[1] * v2[0] - e0[0] * v2[1]; pmin = std::min(p1, p2); pmax = std::max(p1, p2); rad = std::abs(e0[1]) * half[0] + std::abs(e0[0]) * half[1]; if (pmin > rad || pmax < -rad){ return false; } // edge 1 cross z_hat p0 = e1[1] * v0[0] - e1[0] * v0[1]; p1 = e1[1] * v1[0] - e1[0] * v1[1]; //p2 = e1[1] * v2[0] - e1[0] * v2[1]; pmin = std::min(p0, p1); pmax = std::max(p0, p1); rad = std::abs(e1[1]) * half[0] + std::abs(e1[0]) * half[1]; if (pmin > rad || pmax < -rad){ return false; } // edge 2 cross z_hat //p0 = e2[1] * v0[0] - e2[0] * v0[1]; p1 = e2[1] * v1[0] - e2[0] * v1[1]; p2 = e2[1] * v2[0] - e2[0] * v2[1]; pmin = std::min(p1, p2); pmax = std::max(p1, p2); rad = std::abs(e2[1]) * half[0] + std::abs(e2[0]) * half[1]; if (pmin > rad || pmax < -rad){ return false; } if(dim > 2){ // edge 0 cross x_hat p0 = e0[2] * v0[1] - e0[1] * v0[2]; //p1 = e0[2] * v1[1] - e0[1] * v1[2]; p2 = e0[2] * v2[1] - e0[1] * v2[2]; pmin = std::min(p0, p2); pmax = std::max(p0, p2); rad = std::abs(e0[2]) * half[1] + std::abs(e0[1]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 0 cross y_hat p0 = -e0[2] * v0[0] + e0[0] * v0[2]; //p1 = -e0[2] * v1[0] + e0[0] * v1[2]; p2 = -e0[2] * v2[0] + e0[0] * v2[2]; pmin = std::min(p0, p2); pmax = std::max(p0, p2); rad = std::abs(e0[2]) * half[0] + std::abs(e0[0]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 1 cross x_hat p0 = e1[2] * v0[1] - e1[1] * v0[2]; //p1 = e1[2] * v1[1] - e1[1] * v1[2]; p2 = e1[2] * v2[1] - e1[1] * v2[2]; pmin = std::min(p0, p2); pmax = std::max(p0, p2); rad = std::abs(e1[2]) * half[1] + std::abs(e1[1]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 1 cross y_hat p0 = -e1[2] * v0[0] + e1[0] * v0[2]; //p1 = -e1[2] * v1[0] + e1[0] * v1[2]; p2 = -e1[2] * v2[0] + e1[0] * v2[2]; pmin = std::min(p0, p2); pmax = std::max(p0, p2); rad = std::abs(e1[2]) * half[0] + std::abs(e1[0]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 2 cross x_hat p0 = e2[2] * v0[1] - e2[1] * v0[2]; p1 = e2[2] * v1[1] - e2[1] * v1[2]; //p2 = e2[2] * v2[1] - e2[1] * v2[2]; pmin = std::min(p0, p1); pmax = std::max(p0, p1); rad = std::abs(e2[2]) * half[1] + std::abs(e2[1]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 2 cross y_hat p0 = -e2[2] * v0[0] + e2[0] * v0[2]; p1 = -e2[2] * v1[0] + e2[0] * v1[2]; //p2 = -e2[2] * v2[0] + e2[0] * v2[2]; pmin = std::min(p0, p1); pmax = std::max(p0, p1); rad = std::abs(e2[2]) * half[0] + std::abs(e2[0]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // triangle normal axis pmin = 0.0; pmax = 0.0; for(int_t i=0; i 0){ pmin += normal[i] * (-half[i] - v0[i]); pmax += normal[i] * (half[i] - v0[i]); }else{ pmin += normal[i] * (half[i] - v0[i]); pmax += normal[i] * (-half[i] - v0[i]); } } if (pmin > 0 || pmax < 0){ return false; } } return true; } VerticalTriangularPrism::VerticalTriangularPrism() : Triangle(){ h = 0; } VerticalTriangularPrism::VerticalTriangularPrism(int_t dim, double* x0, double *x1, double *x2, double h) : Triangle(dim, x0, x1, x2){ this->h = h; } bool VerticalTriangularPrism::intersects_cell(double *a, double *b) const{ double center; double v0[3], v1[3], v2[3], half[3]; double vmin, vmax; double p0, p1, p2, p3, pmin, pmax, rad; for(int_t i=0; i < dim; ++i){ center = 0.5 * (a[i] + b[i]); v0[i] = x0[i] - center; v1[i] = x1[i] - center; vmin = std::min(v0[i], v1[i]); vmax = std::max(v0[i], v1[i]); v2[i] = x2[i] - center; vmin = std::min(vmin, v2[i]); vmax = std::max(vmax, v2[i]); if(i == 2){ vmax += h; } half[i] = center - a[i]; // Bounding box check if (vmin > half[i] || vmax < -half[i]){ return false; } } // first do the 3 edge cross tests that apply in 2D and 3D // edge 0 cross z_hat //p0 = e0[1] * v0[0] - e0[0] * v0[1]; p1 = e0[1] * v1[0] - e0[0] * v1[1]; p2 = e0[1] * v2[0] - e0[0] * v2[1]; pmin = std::min(p1, p2); pmax = std::max(p1, p2); rad = std::abs(e0[1]) * half[0] + std::abs(e0[0]) * half[1]; if (pmin > rad || pmax < -rad){ return false; } // edge 1 cross z_hat p0 = e1[1] * v0[0] - e1[0] * v0[1]; p1 = e1[1] * v1[0] - e1[0] * v1[1]; //p2 = e1[1] * v2[0] - e1[0] * v2[1]; pmin = std::min(p0, p1); pmax = std::max(p0, p1); rad = std::abs(e1[1]) * half[0] + std::abs(e1[0]) * half[1]; if (pmin > rad || pmax < -rad){ return false; } // edge 2 cross z_hat //p0 = e2[1] * v0[0] - e2[0] * v0[1]; p1 = e2[1] * v1[0] - e2[0] * v1[1]; p2 = e2[1] * v2[0] - e2[0] * v2[1]; pmin = std::min(p1, p2); pmax = std::max(p1, p2); rad = std::abs(e2[1]) * half[0] + std::abs(e2[0]) * half[1]; if (pmin > rad || pmax < -rad){ return false; } // edge 0 cross x_hat p0 = e0[2] * v0[1] - e0[1] * v0[2]; p1 = e0[2] * v0[1] - e0[1] * (v0[2] + h); p2 = e0[2] * v2[1] - e0[1] * v2[2]; p3 = e0[2] * v2[1] - e0[1] * (v2[2] + h); pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(e0[2]) * half[1] + std::abs(e0[1]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 0 cross y_hat p0 = -e0[2] * v0[0] + e0[0] * v0[2]; p1 = -e0[2] * v0[0] + e0[0] * (v0[2] + h); p2 = -e0[2] * v2[0] + e0[0] * v2[2]; p3 = -e0[2] * v2[0] + e0[0] * (v2[2] + h); pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(e0[2]) * half[0] + std::abs(e0[0]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 1 cross x_hat p0 = e1[2] * v0[1] - e1[1] * v0[2]; p1 = e1[2] * v0[1] - e1[1] * (v0[2] + h); p2 = e1[2] * v2[1] - e1[1] * v2[2]; p3 = e1[2] * v2[1] - e1[1] * (v2[2] + h); pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(e1[2]) * half[1] + std::abs(e1[1]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 1 cross y_hat p0 = -e1[2] * v0[0] + e1[0] * v0[2]; p1 = -e1[2] * v0[0] + e1[0] * (v0[2] + h); p2 = -e1[2] * v2[0] + e1[0] * v2[2]; p3 = -e1[2] * v2[0] + e1[0] * (v2[2] + h); pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(e1[2]) * half[0] + std::abs(e1[0]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 2 cross x_hat p0 = e2[2] * v0[1] - e2[1] * v0[2]; p1 = e2[2] * v0[1] - e2[1] * (v0[2] + h); p2 = e2[2] * v1[1] - e2[1] * v1[2]; p3 = e2[2] * v1[1] - e2[1] * (v1[2] + h); pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(e2[2]) * half[1] + std::abs(e2[1]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // edge 2 cross y_hat p0 = -e2[2] * v0[0] + e2[0] * v0[2]; p1 = -e2[2] * v0[0] + e2[0] * (v0[2] + h); p2 = -e2[2] * v1[0] + e2[0] * v1[2]; p3 = -e2[2] * v1[0] + e2[0] * (v1[2] + h); pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(e2[2]) * half[0] + std::abs(e2[0]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // triangle normal axis p0 = normal[0] * v0[0] + normal[1] * v0[1] + normal[2] * v0[2]; p1 = normal[0] * v0[0] + normal[1] * v0[1] + normal[2] * (v0[2] + h); pmin = std::min(p0, p1); pmax = std::max(p0, p1); rad = std::abs(normal[0]) * half[0] + std::abs(normal[1]) * half[1] + std::abs(normal[2]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } // the axes defined by the three vertical prism faces // should already be tested by the e0, e1, e2 cross z_hat tests return true; } Tetrahedron::Tetrahedron() : Geometric(){ x0 = NULL; x1 = NULL; x2 = NULL; x3 = NULL; for(int_t i=0; i<6; ++i){ for(int_t j=0; j<3; ++j){ edge_tans[i][j] = 0.0; } } for(int_t i=0; i<4; ++i){ for(int_t j=0; j<3; ++j){ face_normals[i][j] = 0.0; } } } Tetrahedron::Tetrahedron(int_t dim, double* x0, double *x1, double *x2, double *x3) : Geometric(dim){ this->x0 = x0; this->x1 = x1; this->x2 = x2; this->x3 = x3; for(int_t i=0; i half[i] || pmax < -half[i]){ return false; } } // first do the 3 edge cross tests that apply in 2D and 3D const double *axis; for(int_t i=0; i<6; ++i){ // edge cross [1, 0, 0] p0 = edge_tans[i][2] * v0[1] - edge_tans[i][1] * v0[2]; p1 = edge_tans[i][2] * v1[1] - edge_tans[i][1] * v1[2]; p2 = edge_tans[i][2] * v2[1] - edge_tans[i][1] * v2[2]; p3 = edge_tans[i][2] * v3[1] - edge_tans[i][1] * v3[2]; pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(edge_tans[i][2]) * half[1] + std::abs(edge_tans[i][1]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } p0 = -edge_tans[i][2] * v0[0] + edge_tans[i][0] * v0[2]; p1 = -edge_tans[i][2] * v1[0] + edge_tans[i][0] * v1[2]; p2 = -edge_tans[i][2] * v2[0] + edge_tans[i][0] * v2[2]; p3 = -edge_tans[i][2] * v3[0] + edge_tans[i][0] * v3[2]; pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(edge_tans[i][2]) * half[0] + std::abs(edge_tans[i][0]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } p0 = edge_tans[i][1] * v0[0] - edge_tans[i][0] * v0[1]; p1 = edge_tans[i][1] * v1[0] - edge_tans[i][0] * v1[1]; p2 = edge_tans[i][1] * v2[0] - edge_tans[i][0] * v2[1]; p3 = edge_tans[i][1] * v3[0] - edge_tans[i][0] * v3[1]; pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(edge_tans[i][1]) * half[0] + std::abs(edge_tans[i][0]) * half[1]; if (pmin > rad || pmax < -rad){ return false; } } // triangle face normals for(int_t i=0; i<4; ++i){ axis = face_normals[i]; p0 = axis[0] * v0[0] + axis[1] * v0[1] + axis[2] * v0[2]; p1 = axis[0] * v1[0] + axis[1] * v1[1] + axis[2] * v1[2]; p2 = axis[0] * v2[0] + axis[1] * v2[1] + axis[2] * v2[2]; p3 = axis[0] * v3[0] + axis[1] * v3[1] + axis[2] * v3[2]; pmin = std::min(std::min(std::min(p0, p1), p2), p3); pmax = std::max(std::max(std::max(p0, p1), p2), p3); rad = std::abs(axis[0]) * half[0] + std::abs(axis[1]) * half[1] + std::abs(axis[2]) * half[2]; if (pmin > rad || pmax < -rad){ return false; } } return true; } ================================================ FILE: discretize/_extensions/geom.h ================================================ #ifndef __GEOM_H #define __GEOM_H // simple geometric objects for intersection tests with an aabb typedef std::size_t int_t; class Geometric{ public: int_t dim; Geometric(); Geometric(int_t dim); virtual bool intersects_cell(double *a, double *b) const = 0; }; class Ball : public Geometric{ public: double *x0; double r; double rsq; Ball(); Ball(int_t dim, double* x0, double r); virtual bool intersects_cell(double *a, double *b) const; }; class Line : public Geometric{ public: double *x0; double *x1; double inv_dx[3]; Line(); Line(int_t dim, double* x0, double *x1); virtual bool intersects_cell(double *a, double *b) const; }; class Box : public Geometric{ public: double *x0; double *x1; Box(); Box(int_t dim, double* x0, double *x1); virtual bool intersects_cell(double *a, double *b) const; }; class Plane : public Geometric{ public: double *origin; double *normal; Plane(); Plane(int_t dim, double* origin, double *normal); virtual bool intersects_cell(double *a, double *b) const; }; class Triangle : public Geometric{ public: double *x0; double *x1; double *x2; double e0[3]; double e1[3]; double e2[3]; double normal[3]; Triangle(); Triangle(int_t dim, double* x0, double *x1, double *x2); virtual bool intersects_cell(double *a, double *b) const; }; class VerticalTriangularPrism : public Triangle{ public: double h; VerticalTriangularPrism(); VerticalTriangularPrism(int_t dim, double* x0, double *x1, double *x2, double h); virtual bool intersects_cell(double *a, double *b) const; }; class Tetrahedron : public Geometric{ public: double *x0; double *x1; double *x2; double *x3; double edge_tans[6][3]; double face_normals[4][3]; Tetrahedron(); Tetrahedron(int_t dim, double* x0, double *x1, double *x2, double *x3); virtual bool intersects_cell(double *a, double *b) const; }; #endif ================================================ FILE: discretize/_extensions/geom.pxd ================================================ from libcpp cimport bool cdef extern from "geom.h": ctypedef int int_t cdef cppclass Ball: Ball() except + Ball(int_t dim, double * x0, double r) except + cdef cppclass Line: Line() except + Line(int_t dim, double * x0, double *x1) except + cdef cppclass Box: Box() except + Box(int_t dim, double * x0, double *x1) except + cdef cppclass Plane: Plane() except + Plane(int_t dim, double * origin, double *normal) except + cdef cppclass Triangle: Triangle() except + Triangle(int_t dim, double * x0, double *x1, double *x2) except + cdef cppclass VerticalTriangularPrism: VerticalTriangularPrism() except + VerticalTriangularPrism(int_t dim, double * x0, double *x1, double *x2, double h) except + cdef cppclass Tetrahedron: Tetrahedron() except + Tetrahedron(int_t dim, double * x0, double *x1, double *x2, double *x3) except + ================================================ FILE: discretize/_extensions/interputils_cython.pxd ================================================ cimport numpy as np cdef np.int64_t _bisect_left(np.float64_t[:] a, np.float64_t x) nogil cdef np.int64_t _bisect_right(np.float64_t[:] a, np.float64_t x) nogil ================================================ FILE: discretize/_extensions/interputils_cython.pyx ================================================ # cython: embedsignature=True, language_level=3 # cython: linetrace=True # cython: freethreading_compatible = True import numpy as np import cython cimport numpy as np import scipy.sparse as sp def _interp_point_1D(np.ndarray[np.float64_t, ndim=1] x, float xr_i): """ given a point, xr_i, this will find which two integers it lies between. :param numpy.ndarray x: Tensor vector of 1st dimension of grid. :param float xr_i: Location of a point :rtype: int,int,float,float :return: index1, index2, portion1, portion2 """ cdef IIFF xs _get_inds_ws(x,xr_i,&xs) return xs.i1,xs.i2,xs.w1,xs.w2 cdef struct IIFF: np.int64_t i1,i2 np.float64_t w1,w2 @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) cdef np.int64_t _bisect_left(np.float64_t[:] a, np.float64_t x) nogil: cdef np.int64_t lo, hi, mid lo = 0 hi = a.shape[0] while lo < hi: mid = (lo+hi)//2 if a[mid] < x: lo = mid+1 else: hi = mid return lo @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) cdef np.int64_t _bisect_right(np.float64_t[:] a, np.float64_t x) nogil: cdef np.int64_t lo, hi, mid lo = 0 hi = a.shape[0] while lo < hi: mid = (lo+hi)//2 if x < a[mid]: hi = mid else: lo = mid+1 return lo @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) @cython.cdivision(True) cdef void _get_inds_ws(np.float64_t[:] x, np.float64_t xp, IIFF* out) nogil: cdef np.int64_t ind = _bisect_right(x,xp) cdef np.int64_t nx = x.shape[0] out.i2 = ind out.i1 = ind-1 out.i2 = max(min(out.i2,nx-1),0) out.i1 = max(min(out.i1,nx-1),0) if(out.i1==out.i2): out.w1 = 0.5 else: out.w1 = (x[out.i2]-xp)/(x[out.i2]-x[out.i1]) out.w2 = 1-out.w1 @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) def _interpmat1D(np.ndarray[np.float64_t, ndim=1] locs, np.ndarray[np.float64_t, ndim=1] x): cdef int nx = x.size cdef IIFF xs cdef int npts = locs.shape[0] cdef int i cdef np.ndarray[np.int64_t,ndim=1] inds = np.empty(npts*2,dtype=np.int64) cdef np.ndarray[np.float64_t,ndim=1] vals = np.empty(npts*2,dtype=np.float64) for i in range(npts): _get_inds_ws(x,locs[i],&xs) inds[2*i ] = xs.i1 inds[2*i+1] = xs.i2 vals[2*i ] = xs.w1 vals[2*i+1] = xs.w2 return inds,vals @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) def _interpmat2D(np.ndarray[np.float64_t, ndim=2] locs, np.ndarray[np.float64_t, ndim=1] x, np.ndarray[np.float64_t, ndim=1] y): cdef int nx,ny nx,ny = len(x),len(y) cdef int npts = locs.shape[0] cdef int i cdef IIFF xs,ys cdef np.ndarray[np.int64_t,ndim=2] inds = np.empty((npts*4,2),dtype=np.int64) cdef np.ndarray[np.float64_t,ndim=1] vals = np.empty(npts*4,dtype=np.float64) for i in range(npts): _get_inds_ws(x,locs[i,0],&xs) _get_inds_ws(y,locs[i,1],&ys) inds[4*i ,0] = xs.i1 inds[4*i+1,0] = xs.i1 inds[4*i+2,0] = xs.i2 inds[4*i+3,0] = xs.i2 inds[4*i ,1] = ys.i1 inds[4*i+1,1] = ys.i2 inds[4*i+2,1] = ys.i1 inds[4*i+3,1] = ys.i2 vals[4*i ] = xs.w1*ys.w1 vals[4*i+1] = xs.w1*ys.w2 vals[4*i+2] = xs.w2*ys.w1 vals[4*i+3] = xs.w2*ys.w2 return inds,vals @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) def _interpmat3D(np.ndarray[np.float64_t, ndim=2] locs, np.ndarray[np.float64_t, ndim=1] x, np.ndarray[np.float64_t, ndim=1] y, np.ndarray[np.float64_t, ndim=1] z): cdef int nx,ny,nz nx,ny,nz = len(x),len(y),len(z) cdef IIFF xs,ys,zs cdef int npts = locs.shape[0] cdef int i cdef np.ndarray[np.int64_t,ndim=2] inds = np.empty((npts*8,3),dtype=np.int64) cdef np.ndarray[np.float64_t,ndim=1] vals = np.empty(npts*8,dtype=np.float64) for i in range(npts): _get_inds_ws(x,locs[i,0],&xs) _get_inds_ws(y,locs[i,1],&ys) _get_inds_ws(z,locs[i,2],&zs) inds[8*i ,0] = xs.i1 inds[8*i+1,0] = xs.i1 inds[8*i+2,0] = xs.i2 inds[8*i+3,0] = xs.i2 inds[8*i+4,0] = xs.i1 inds[8*i+5,0] = xs.i1 inds[8*i+6,0] = xs.i2 inds[8*i+7,0] = xs.i2 inds[8*i ,1] = ys.i1 inds[8*i+1,1] = ys.i2 inds[8*i+2,1] = ys.i1 inds[8*i+3,1] = ys.i2 inds[8*i+4,1] = ys.i1 inds[8*i+5,1] = ys.i2 inds[8*i+6,1] = ys.i1 inds[8*i+7,1] = ys.i2 inds[8*i ,2] = zs.i1 inds[8*i+1,2] = zs.i1 inds[8*i+2,2] = zs.i1 inds[8*i+3,2] = zs.i1 inds[8*i+4,2] = zs.i2 inds[8*i+5,2] = zs.i2 inds[8*i+6,2] = zs.i2 inds[8*i+7,2] = zs.i2 vals[8*i ] = xs.w1*ys.w1*zs.w1 vals[8*i+1] = xs.w1*ys.w2*zs.w1 vals[8*i+2] = xs.w2*ys.w1*zs.w1 vals[8*i+3] = xs.w2*ys.w2*zs.w1 vals[8*i+4] = xs.w1*ys.w1*zs.w2 vals[8*i+5] = xs.w1*ys.w2*zs.w2 vals[8*i+6] = xs.w2*ys.w1*zs.w2 vals[8*i+7] = xs.w2*ys.w2*zs.w2 return inds,vals @cython.boundscheck(False) @cython.cdivision(True) def _tensor_volume_averaging(mesh_in, mesh_out, values=None, output=None): cdef np.int32_t[:] i1_in, i1_out, i2_in, i2_out, i3_in, i3_out cdef np.float64_t[:] w1, w2, w3 w1 = np.array([1.0], dtype=np.float64) w2 = np.array([1.0], dtype=np.float64) w3 = np.array([1.0], dtype=np.float64) i1_in = np.array([0], dtype=np.int32) i1_out = np.array([0], dtype=np.int32) i2_in = np.array([0], dtype=np.int32) i2_out = np.array([0], dtype=np.int32) i3_in = np.array([0], dtype=np.int32) i3_out = np.array([0], dtype=np.int32) cdef int dim = mesh_in.dim w1, i1_in, i1_out = _volume_avg_weights(mesh_in.nodes_x, mesh_out.nodes_x) if dim > 1: w2, i2_in, i2_out = _volume_avg_weights(mesh_in.nodes_y, mesh_out.nodes_y) if dim > 2: w3, i3_in, i3_out = _volume_avg_weights(mesh_in.nodes_z, mesh_out.nodes_z) cdef (np.int32_t, np.int32_t, np.int32_t) w_shape = (w1.shape[0], w2.shape[0], w3.shape[0]) cdef (np.int32_t, np.int32_t, np.int32_t) mesh_in_shape cdef (np.int32_t, np.int32_t, np.int32_t) mesh_out_shape nCv_in = [len(h) for h in mesh_in.h] nCv_out = [len(h) for h in mesh_out.h] if dim == 1: mesh_in_shape = (nCv_in[0], 1, 1) mesh_out_shape = (nCv_out[0], 1, 1) elif dim == 2: mesh_in_shape = (nCv_in[0], nCv_in[1], 1) mesh_out_shape = (nCv_out[0], nCv_out[1], 1) elif dim == 3: mesh_in_shape = (*nCv_in, ) mesh_out_shape = (*nCv_out, ) cdef np.float64_t[::1, :, :] val_in cdef np.float64_t[::1, :, :] val_out cdef int i1, i2, i3, i1i, i2i, i3i, i1o, i2o, i3o cdef np.float64_t w_3, w_32 cdef np.float64_t[::1, :, :] vol = mesh_out.cell_volumes.reshape(mesh_out_shape, order='F').astype(np.float64) if values is not None: # If given a values array, do the operation val_in = values.reshape(mesh_in_shape, order='F').astype(np.float64) if output is None: output = np.zeros(mesh_out.n_cells, dtype=np.float64) else: output = np.require(output, dtype=np.float64, requirements=['A', 'W']) v_o = output.reshape(mesh_out_shape, order='F') v_o.fill(0) val_out = v_o for i3 in range(w_shape[2]): i3i = i3_in[i3] i3o = i3_out[i3] w_3 = w3[i3] for i2 in range(w_shape[1]): i2i = i2_in[i2] i2o = i2_out[i2] w_32 = w_3*w2[i2] for i1 in range(w_shape[0]): i1i = i1_in[i1] i1o = i1_out[i1] val_out[i1o, i2o, i3o] += w_32*w1[i1]*val_in[i1i, i2i, i3i]/vol[i1o, i2o, i3o] return output # Else, build and return a sparse matrix representing the operation i_i = np.empty(w_shape, dtype=np.int32, order='F') i_o = np.empty(w_shape, dtype=np.int32, order='F') ws = np.empty(w_shape, dtype=np.float64, order='F') cdef np.int32_t[::1,:,:] i_in = i_i cdef np.int32_t[::1,:,:] i_out = i_o cdef np.float64_t[::1, :, :] w = ws for i3 in range(w.shape[2]): i3i = i3_in[i3] i3o = i3_out[i3] w_3 = w3[i3] for i2 in range(w.shape[1]): i2i = i2_in[i2] i2o = i2_out[i2] w_32 = w_3*w2[i2] for i1 in range(w.shape[0]): i1i = i1_in[i1] i1o = i1_out[i1] w[i1, i2, i3] = w_32*w1[i1]/vol[i1o, i2o, i3o] i_in[i1, i2, i3] = (i3i*mesh_in_shape[1] + i2i)*mesh_in_shape[0] + i1i i_out[i1, i2, i3] = (i3o*mesh_out_shape[1] + i2o)*mesh_out_shape[0] + i1o ws = ws.reshape(-1, order='F') i_i = i_i.reshape(-1, order='F') i_o = i_o.reshape(-1, order='F') A = sp.csr_matrix((ws, (i_o, i_i)), shape=(mesh_out.nC, mesh_in.nC)) return A @cython.boundscheck(False) def _volume_avg_weights(np.float64_t[:] x1, np.float64_t[:] x2): cdef int n1 = x1.shape[0] cdef int n2 = x2.shape[0] cdef np.float64_t[:] xs = np.empty(n1 + n2) # Fill xs with uniques and truncate cdef int i1, i2, i, ii i1 = i2 = i = 0 while i1x2[i2]: xs[i] = x2[i2] i2 += 1 else: xs[i] = x1[i1] i1 += 1 i2 += 1 elif i1=x1[i1]: i1 += 1 while i2=x2[i2]: i2 += 1 _ix1[ii] = min(max(i1-1, 0), n1-1) _ix2[ii] = min(max(i2-1, 0), n2-1) ii += 1 hs = hs[:ii] ix1 = ix1[:ii] ix2 = ix2[:ii] return hs, ix1, ix2 ================================================ FILE: discretize/_extensions/meson.build ================================================ # NumPy include directory numpy_nodepr_api = ['-DNPY_NO_DEPRECATED_API=NPY_1_22_API_VERSION'] np_dep = dependency('numpy') # Deal with M_PI & friends; add `use_math_defines` to c_args or cpp_args # Cython doesn't always get this right itself (see, e.g., gh-16800), so # explicitly add the define as a compiler flag for Cython-generated code. is_windows = host_machine.system() == 'windows' if is_windows use_math_defines = ['-D_USE_MATH_DEFINES'] else use_math_defines = [] endif c_undefined_ok = ['-Wno-maybe-uninitialized'] cython_c_args = [numpy_nodepr_api, use_math_defines] cy_line_trace = get_option('cy_line_trace') if cy_line_trace cython_c_args += ['-DCYTHON_TRACE_NOGIL=1'] endif cython_args = [] if cy.version().version_compare('>=3.1.0') cython_args += ['-Xfreethreading_compatible=True'] endif cython_cpp_args = cython_c_args module_path = 'discretize/_extensions' py.extension_module( 'interputils_cython', 'interputils_cython.pyx', cython_args: cython_args, c_args: cython_c_args, install: true, subdir: module_path, dependencies : [py_dep, np_dep], ) py.extension_module( 'tree_ext', ['tree_ext.pyx' , 'tree.cpp', 'geom.cpp'], cython_args: cython_args, cpp_args: cython_cpp_args, install: true, subdir: module_path, dependencies : [py_dep, np_dep], override_options : ['cython_language=cpp'], ) py.extension_module( 'simplex_helpers', 'simplex_helpers.pyx', cython_args: cython_args, cpp_args: cython_cpp_args, install: true, subdir: module_path, dependencies : [py_dep, np_dep], override_options : ['cython_language=cpp'], ) python_sources = [ '__init__.py', ] py.install_sources( python_sources, subdir: module_path ) ================================================ FILE: discretize/_extensions/simplex_helpers.pyx ================================================ # distutils: language=c++ # cython: embedsignature=True, language_level=3 # cython: linetrace=True # cython: freethreading_compatible = True from libcpp.pair cimport pair from libcpp.unordered_map cimport unordered_map from cython.operator cimport dereference cimport cython cimport numpy as np from cython cimport view from libc.math cimport sqrt cdef extern from "triplet.h": cdef cppclass triplet[T, U, V]: T v1 U v2 V v3 triplet() triplet(T, U, V) import numpy as np ctypedef fused ints: size_t np.int32_t np.int64_t ctypedef fused pointers: size_t np.intp_t np.int32_t np.int64_t @cython.boundscheck(False) def _build_faces_edges(ints[:, :] simplices): # the node index in each simplex must be in increasing order cdef: int dim = simplices.shape[1] - 1 ints n_simplex = simplices.shape[0] ints[:] simplex ints v1, v2, v3 unordered_map[pair[ints, ints], ints] edges pair[ints, ints] edge ints n_edges = 0 ints edges_per_simplex = 3 if dim==2 else 6 ints[:, :] simplex_edges unordered_map[triplet[ints, ints, ints], ints] faces triplet[ints, ints, ints] face ints n_faces = 0 ints faces_per_simplex = dim + 1 ints[:, :] simplex_faces if ints is size_t: int_type = np.uintp elif ints is np.int32_t: int_type = np.int32 elif ints is np.int64_t: int_type = np.int64 simplex_edges = np.empty((n_simplex, edges_per_simplex), dtype=int_type) if dim == 3: simplex_faces = np.empty((n_simplex, faces_per_simplex), dtype=int_type) else: simplex_faces = simplex_edges cdef ints[:,:] edge_pairs = np.array( [[1, 2], [0, 2], [0, 1], [0, 3], [1, 3], [2, 3]], dtype=int_type ) for i_simp in range(n_simplex): simplex = simplices[i_simp] # build edges for i_edge in range(edges_per_simplex): v1 = simplex[edge_pairs[i_edge, 0]] v2 = simplex[edge_pairs[i_edge, 1]] edge = pair[ints, ints](v1, v2) edge_search = edges.find(edge) if edge_search != edges.end(): ind = dereference(edge_search).second else: ind = n_edges edges[edge] = ind n_edges += 1 simplex_edges[i_simp, i_edge] = ind # build faces in 3D if dim == 3: for i_face in range(4): if i_face == 0: v1 = simplex[1] v2 = simplex[2] v3 = simplex[3] elif i_face == 1: v1 = simplex[0] v2 = simplex[2] v3 = simplex[3] elif i_face == 2: v1 = simplex[0] v2 = simplex[1] v3 = simplex[3] else: v1 = simplex[0] v2 = simplex[1] v3 = simplex[2] face = triplet[ints, ints, ints](v1, v2, v3) face_search = faces.find(face) if face_search != faces.end(): ind = dereference(face_search).second else: ind = n_faces faces[face] = ind n_faces += 1 simplex_faces[i_simp, i_face] = ind cdef ints[:, :] _edges = np.empty((n_edges, 2), dtype=int_type) for edge_it in edges: _edges[edge_it.second, 0] = edge_it.first.first _edges[edge_it.second, 1] = edge_it.first.second cdef ints[:, :] _faces if dim == 3: _faces = np.empty((n_faces, 3), dtype=int_type) for face_it in faces: _faces[face_it.second, 0] = face_it.first.v1 _faces[face_it.second, 1] = face_it.first.v2 _faces[face_it.second, 2] = face_it.first.v3 else: _faces = _edges cdef ints[:, :] face_edges cdef ints[:] _face if dim == 3: face_edges = np.empty((n_faces, 3), dtype=int_type) for i_face in range(n_faces): _face = _faces[i_face] # get indices of each edge in the face # 3 edges per face for i_edge in range(3): if i_edge == 0: v1 = _face[1] v2 = _face[2] elif i_edge == 1: v1 = _face[0] v2 = _face[2] elif i_edge == 2: v1 = _face[0] v2 = _face[1] # because of how faces were constructed, v1 < v2 always edge = pair[ints, ints](v1, v2) ind = edges[edge] face_edges[i_face, i_edge] = ind else: face_edges = np.empty((1, 1), dtype=int_type) return simplex_faces, _faces, simplex_edges, _edges, face_edges @cython.boundscheck(False) def _build_adjacency(ints[:, :] simplex_faces, n_faces): cdef: size_t n_cells = simplex_faces.shape[0] int dim = simplex_faces.shape[1] - 1 np.int64_t[:, :] neighbors np.int64_t[:] visited ints[:] simplex ints i_cell, j, k, i_face, i_other if ints is size_t: int_type = np.uintp elif ints is np.int32_t: int_type = np.int32 elif ints is np.int64_t: int_type = np.int64 neighbors = np.full((n_cells, dim + 1), -1, dtype=np.int64) visited = np.full((n_faces), -1, dtype=np.int64) for i_cell in range(n_cells): simplex = simplex_faces[i_cell] for j in range(dim + 1): i_face = simplex[j] i_other = visited[i_face] if i_other == -1: visited[i_face] = i_cell else: neighbors[i_cell, j] = i_other k = 0 while (k < dim + 1) and (simplex_faces[i_other, k] != i_face): k += 1 neighbors[i_other, k] = i_cell return neighbors @cython.boundscheck(False) @cython.linetrace(False) cdef void _compute_bary_coords( np.float64_t[:] point, np.float64_t[:, :] Tinv, np.float64_t[:] shift, np.float64_t * bary ) nogil: cdef: int dim = point.shape[0] int i, j bary[dim] = 1.0 for i in range(dim): bary[i] = 0.0 for j in range(dim): bary[i] += Tinv[i, j] * (point[j] - shift[j]) bary[dim] -= bary[i] @cython.boundscheck(False) def _directed_search( np.float64_t[:, :] locs, pointers[:] nearest_cc, np.float64_t[:, :] nodes, ints[:, :] simplex_nodes, np.int64_t[:, :] neighbors, np.float64_t[:, :, :] transform, np.float64_t[:, :] shift, np.float64_t eps=1E-15, bint zeros_outside=False, bint return_bary=True ): cdef: int i, j pointers i_simp int n_locs = locs.shape[0], dim = locs.shape[1] int max_directed = 1 + simplex_nodes.shape[0] // 4 int i_directed bint is_inside np.int64_t[:] inds = np.full(len(locs), -1, dtype=np.int64) np.float64_t[:, :] all_barys = np.empty((1, 1), dtype=np.float64) np.float64_t barys[4] np.float64_t[:] loc np.float64_t[:, :] Tinv np.float64_t[:] rD if return_bary: all_barys = np.empty((len(locs), dim+1), dtype=np.float64) for i in range(n_locs): loc = locs[i] i_simp = nearest_cc[i] # start at the nearest cell center i_directed = 0 while i_directed < max_directed: Tinv = transform[i_simp] rD = shift[i_simp] _compute_bary_coords(loc, Tinv, rD, barys) j = 0 is_inside = True while j <= dim: if barys[j] < -eps: is_inside = False # if not -1, move towards neighbor if neighbors[i_simp, j] != -1: i_simp = neighbors[i_simp, j] break j += 1 # If inside, I found my container if is_inside: break # Else, if I cycled through every bary # without breaking out of the above loop, that means I'm completely outside elif j == dim + 1: if zeros_outside: i_simp = -1 break i_directed += 1 if i_directed == max_directed: # made it through the whole loop without breaking out # Mark as failed i_simp = -2 inds[i] = i_simp if return_bary: for j in range(dim+1): all_barys[i, j] = barys[j] if return_bary: return np.array(inds), np.array(all_barys) return np.array(inds) @cython.boundscheck(False) @cython.cdivision(True) def _interp_cc( np.float64_t[:, :] locs, np.float64_t[:, :] cell_centers, np.float64_t[:] mat_data, ints[:] mat_indices, ints[:] mat_indptr, ): cdef: ints i, j, diff, start, stop, i_d ints n_max_per_row = 0 ints[:] close_cells int dim = locs.shape[1] np.float64_t[:, :] drs np.float64_t[:] rs np.float64_t[:] rhs np.float64_t[:] lambs np.float64_t[:] weights np.float64_t[:] point np.float64_t[:] close_cell np.float64_t det, weight_sum np.float64_t xx, xy, xz, yy, yz, zz bint too_close np.float64_t eps = 1E-15 # Find maximum number per row to pre-allocate a storage for i in range(locs.shape[0]): diff = mat_indptr[i+1] - mat_indptr[i] if diff > n_max_per_row: n_max_per_row = diff # drs = np.empty((n_max_per_row, dim), dtype=np.float64) rs = np.empty((n_max_per_row,), dtype=np.float64) rhs = np.empty((dim,),dtype=np.float64) lambs = np.empty((dim,),dtype=np.float64) for i in range(locs.shape[0]): point = locs[i] start = mat_indptr[i] stop = mat_indptr[i+1] diff = stop-start close_cells = mat_indices[start:stop] for j in range(diff): rs[j] = 0.0 close_cell = cell_centers[close_cells[j]] for i_d in range(dim): drs[j, i_d] = close_cell[i_d] - point[i_d] rs[j] += drs[j, i_d]*drs[j, i_d] rs[j] = sqrt(rs[j]) weights = mat_data[start:stop] weights[:] = 0.0 too_close = False i_d = 0 for j in range(diff): if rs[j] < eps: too_close = True i_d = j if too_close: weights[i_d] = 1.0 else: for j in range(diff): for i_d in range(dim): drs[j, i_d] /= rs[j] xx = xy = yy = 0.0 rhs[:] = 0.0 if dim == 2: for j in range(diff): xx += drs[j, 0] * drs[j, 0] xy += drs[j, 0] * drs[j, 1] yy += drs[j, 1] * drs[j, 1] rhs[0] -= drs[j, 0] rhs[1] -= drs[j, 1] det = xx * yy - xy * xy lambs[0] = (yy * rhs[0] - xy * rhs[1])/det lambs[1] = (-xy * rhs[0] + xx * rhs[1])/det if dim == 3: zz = xz = yz = 0.0 for j in range(diff): xx += drs[j, 0] * drs[j, 0] xy += drs[j, 0] * drs[j, 1] yy += drs[j, 1] * drs[j, 1] xz += drs[j, 0] * drs[j, 2] yz += drs[j, 1] * drs[j, 2] zz += drs[j, 2] * drs[j, 2] rhs[0] -= drs[j, 0] rhs[1] -= drs[j, 1] rhs[2] -= drs[j, 2] det = ( xx * (yy * zz - yz * yz) + xy * (xz * yz - xy * zz) + xz * (xy * yz - xz * yy) ) lambs[0] = ( (yy * zz - yz * yz) * rhs[0] + (xz * yz - xy * zz) * rhs[1] + (xy * yz - xz * yy) * rhs[2] )/det lambs[1] = ( (xz * yz - xy * zz) * rhs[0] + (xx * zz - xz * xz) * rhs[1] + (xy * xz - xx * yz) * rhs[2] )/det lambs[2] = ( (xy * yz - xz * yy) * rhs[0] + (xz * xy - xx * yz) * rhs[1] + (xx * yy - xy * xy) * rhs[2] )/det weight_sum = 0.0 for j in range(diff): weights[j] = 1.0 for i_d in range(dim): weights[j] += lambs[i_d] * drs[j, i_d] weights[j] /= rs[j] weight_sum += weights[j] for j in range(diff): weights[j] /= weight_sum ================================================ FILE: discretize/_extensions/tree.cpp ================================================ #include #include #include "tree.h" #include "geom.h" #include #include #include Node::Node(){ location_ind[0] = 0; location_ind[1] = 0; location_ind[2] = 0; location[0] = 0; location[1] = 0; location[2] = 0; key = 0; reference = 0; index = 0; hanging = false; parents[0] = NULL; parents[1] = NULL; parents[2] = NULL; parents[3] = NULL; }; Node::Node(int_t ix, int_t iy, int_t iz, double* xs, double *ys, double *zs){ location_ind[0] = ix; location_ind[1] = iy; location_ind[2] = iz; location[0] = xs[ix]; location[1] = ys[iy]; location[2] = zs[iz]; key = key_func(ix, iy, iz); reference = 0; index = 0; hanging = false; parents[0] = NULL; parents[1] = NULL; parents[2] = NULL; parents[3] = NULL; }; Edge::Edge(){ location_ind[0] = 0; location_ind[1] = 0; location_ind[2] = 0; location[0] = 0; location[1] = 0; location[2] = 0; key = 0; index = 0; reference = 0; length = 0.0; hanging = false; points[0] = NULL; points[1] = NULL; parents[0] = NULL; parents[1] = NULL; }; Edge::Edge(Node& p1, Node& p2){ points[0] = &p1; points[1] = &p2; int_t ix, iy, iz; ix = (p1.location_ind[0]+p2.location_ind[0])/2; iy = (p1.location_ind[1]+p2.location_ind[1])/2; iz = (p1.location_ind[2]+p2.location_ind[2])/2; key = key_func(ix, iy, iz); location_ind[0] = ix; location_ind[1] = iy; location_ind[2] = iz; location[0] = (p1[0]+p2[0]) * 0.5; location[1] = (p1[1]+p2[1]) * 0.5; location[2] = (p1[2]+p2[2]) * 0.5; length = (p2[0]-p1[0]) + (p2[1]-p1[1]) + (p2[2]-p1[2]); reference = 0; index = 0; hanging = false; parents[0] = NULL; parents[1] = NULL; } Face::Face(){ location_ind[0] = 0; location_ind[1] = 0; location_ind[2] = 0; location[0] = 0.0; location[1] = 0.0; location[2] = 0.0; key = 0; reference = 0; index = 0; area = 0; hanging = false; points[0] = NULL; points[1] = NULL; points[2] = NULL; points[3] = NULL; edges[0] = NULL; edges[1] = NULL; edges[2] = NULL; edges[3] = NULL; parent = NULL; } Face::Face(Node& p1, Node& p2, Node& p3, Node& p4){ points[0] = &p1; points[1] = &p2; points[2] = &p3; points[3] = &p4; int_t ix, iy, iz; ix = (p1.location_ind[0]+p2.location_ind[0]+p3.location_ind[0]+p4.location_ind[0])/4; iy = (p1.location_ind[1]+p2.location_ind[1]+p3.location_ind[1]+p4.location_ind[1])/4; iz = (p1.location_ind[2]+p2.location_ind[2]+p3.location_ind[2]+p4.location_ind[2])/4; key = key_func(ix, iy, iz); location_ind[0] = ix; location_ind[1] = iy; location_ind[2] = iz; location[0] = (p1[0]+p2[0]+p3[0]+p4[0]) * 0.25; location[1] = (p1[1]+p2[1]+p3[1]+p4[1]) * 0.25; location[2] = (p1[2]+p2[2]+p3[2]+p4[2]) * 0.25; area = ((p2[0]-p1[0]) + (p2[1]-p1[1]) + (p2[2]-p1[2])) * ((p3[0]-p1[0]) + (p3[1]-p1[1]) + (p3[2]-p1[2])); reference = 0; index = 0; hanging = false; parent = NULL; edges[0] = NULL; edges[1] = NULL; edges[2] = NULL; edges[3] = NULL; } Node * set_default_node(node_map_t& nodes, int_t x, int_t y, int_t z, double *xs, double *ys, double *zs){ int_t key = key_func(x, y, z); auto [it, inserted] = nodes.try_emplace(key, nullptr); if(inserted){ // construct a new item at the emplaced location it->second = new Node(x, y, z, xs, ys, zs); } return it->second; } Edge * set_default_edge(edge_map_t& edges, Node& p1, Node& p2){ int_t xC = (p1.location_ind[0]+p2.location_ind[0])/2; int_t yC = (p1.location_ind[1]+p2.location_ind[1])/2; int_t zC = (p1.location_ind[2]+p2.location_ind[2])/2; int_t key = key_func(xC, yC, zC); auto [it, inserted] = edges.try_emplace(key, nullptr); if(inserted){ // construct a new item at the emplaced location it->second = new Edge(p1, p2); } return it->second; }; Face * set_default_face(face_map_t& faces, Node& p1, Node& p2, Node& p3, Node& p4){ int_t x, y, z, key; x = (p1.location_ind[0]+p2.location_ind[0]+p3.location_ind[0]+p4.location_ind[0])/4; y = (p1.location_ind[1]+p2.location_ind[1]+p3.location_ind[1]+p4.location_ind[1])/4; z = (p1.location_ind[2]+p2.location_ind[2]+p3.location_ind[2]+p4.location_ind[2])/4; key = key_func(x, y, z); auto [it, inserted] = faces.try_emplace(key, nullptr); if(inserted){ // construct a new item at the emplaced location it->second = new Face(p1, p2, p3, p4); } return it->second; } Cell::Cell(Node *pts[8], int_t ndim, int_t maxlevel){ n_dim = ndim; int_t n_points = 1<n_dim; int_t n_points = 1<level + 1; max_level = parent->max_level; Node p1 = *pts[0]; Node p2 = *pts[n_points - 1]; location_ind[0] = (p1.location_ind[0]+p2.location_ind[0])/2; location_ind[1] = (p1.location_ind[1]+p2.location_ind[1])/2; location_ind[2] = (p1.location_ind[2]+p2.location_ind[2])/2; location[0] = (p1[0]+p2[0]) * 0.5; location[1] = (p1[1]+p2[1]) * 0.5; location[2] = (p1[2]+p2[2]) * 0.5; volume = (p2[0]-p1[0]) * (p2[1]-p1[1]); if(n_dim == 3) volume *= (p2[2] - p1[2]); key = key_func(location_ind[0], location_ind[1], location_ind[2]); for(int_t i = 0; i < n_points; ++i) children[i] = NULL; for(int_t i = 0; i < 2*n_dim; ++i) neighbors[i] = NULL; }; void Cell::spawn(node_map_t& nodes, Cell *kids[8], double *xs, double *ys, double *zs){ /* z0 z0+dz/2 z0+dz p03--p13--p04 p20--p21--p22 p07--p27--p08 | | | | | | | | | p10--p11--p12 p17--p18--p19 p24--p25--p26 | | | | | | | | | p01--p09--p02 p14--p15--p16 p05--p23--p06 */ Node *p1 = points[0]; Node *p2 = points[1]; Node *p3 = points[2]; Node *p4 = points[3]; int_t x0, y0, xC, yC, xF, yF, z0; x0 = p1->location_ind[0]; y0 = p1->location_ind[1]; xF = p4->location_ind[0]; yF = p4->location_ind[1]; z0 = p1->location_ind[2]; xC = location_ind[0]; yC = location_ind[1]; Node *p9, *p10, *p11, *p12, *p13; p9 = set_default_node(nodes, xC, y0, z0, xs, ys, zs); p10 = set_default_node(nodes, x0, yC, z0, xs, ys, zs); p11 = set_default_node(nodes, xC, yC, z0, xs, ys, zs); p12 = set_default_node(nodes, xF, yC, z0, xs, ys, zs); p13 = set_default_node(nodes, xC, yF, z0, xs, ys, zs); //Increment node references for new nodes p9->reference += 2; p10->reference += 2; p11->reference += 4; p12->reference += 2; p13->reference += 2; if(n_dim>2){ Node *p5 = points[4]; Node *p6 = points[5]; Node *p7 = points[6]; Node *p8 = points[7]; int_t zC, zF; zF = p8->location_ind[2]; zC = location_ind[2]; Node *p14, *p15, *p16, *p17, *p18, *p19, *p20, *p21, *p22; Node *p23, *p24, *p25, *p26, *p27; p14 = set_default_node(nodes, x0, y0, zC, xs, ys, zs); p15 = set_default_node(nodes, xC, y0, zC, xs, ys, zs); p16 = set_default_node(nodes, xF, y0, zC, xs, ys, zs); p17 = set_default_node(nodes, x0, yC, zC, xs, ys, zs); p18 = set_default_node(nodes, xC, yC, zC, xs, ys, zs); p19 = set_default_node(nodes, xF, yC, zC, xs, ys, zs); p20 = set_default_node(nodes, x0, yF, zC, xs, ys, zs); p21 = set_default_node(nodes, xC, yF, zC, xs, ys, zs); p22 = set_default_node(nodes, xF, yF, zC, xs, ys, zs); p23 = set_default_node(nodes, xC, y0, zF, xs, ys, zs); p24 = set_default_node(nodes, x0, yC, zF, xs, ys, zs); p25 = set_default_node(nodes, xC, yC, zF, xs, ys, zs); p26 = set_default_node(nodes, xF, yC, zF, xs, ys, zs); p27 = set_default_node(nodes, xC, yF, zF, xs, ys, zs); //Increment node references p14->reference += 2; p15->reference += 4; p16->reference += 2; p17->reference += 4; p18->reference += 8; p19->reference += 4; p20->reference += 2; p21->reference += 4; p22->reference += 2; p23->reference += 2; p24->reference += 2; p25->reference += 4; p26->reference += 2; p27->reference += 2; Node * pQC1[8] = { p1, p9,p10,p11,p14,p15,p17,p18}; Node * pQC2[8] = { p9, p2,p11,p12,p15,p16,p18,p19}; Node * pQC3[8] = {p10,p11, p3,p13,p17,p18,p20,p21}; Node * pQC4[8] = {p11,p12,p13, p4,p18,p19,p21,p22}; Node * pQC5[8] = {p14,p15,p17,p18, p5,p23,p24,p25}; Node * pQC6[8] = {p15,p16,p18,p19,p23, p6,p25,p26}; Node * pQC7[8] = {p17,p18,p20,p21,p24,p25, p7,p27}; Node * pQC8[8] = {p18,p19,p21,p22,p25,p26,p27, p8}; kids[0] = new Cell(pQC1, this); kids[1] = new Cell(pQC2, this); kids[2] = new Cell(pQC3, this); kids[3] = new Cell(pQC4, this); kids[4] = new Cell(pQC5, this); kids[5] = new Cell(pQC6, this); kids[6] = new Cell(pQC7, this); kids[7] = new Cell(pQC8, this); } else{ Node * pQC1[8] = { p1, p9,p10,p11, NULL, NULL, NULL, NULL}; Node * pQC2[8] = { p9, p2,p11,p12, NULL, NULL, NULL, NULL}; Node * pQC3[8] = {p10,p11, p3,p13, NULL, NULL, NULL, NULL}; Node * pQC4[8] = {p11,p12,p13, p4, NULL, NULL, NULL, NULL}; kids[0] = new Cell(pQC1, this); kids[1] = new Cell(pQC2, this); kids[2] = new Cell(pQC3, this); kids[3] = new Cell(pQC4, this); } }; void Cell::set_neighbor(Cell * other, int_t position){ if(other==NULL){ return; } if(level != other->level){ neighbors[position] = other; }else{ neighbors[position] = other; other->neighbors[position^1] = this; } }; void Cell::shift_centers(double *shift){ for(int_t id = 0; idshift_centers(shift); } } } // intersections tests: bool Cell::intersects_point(double *x){ // A simple bounding box check: double *p0 = min_node()->location; double *p1 = max_node()->location; for(int_t i=0; i < n_dim; ++i){ if(x[i] < p0[i] || x[i] > p1[i]){ return false; } } return true; } void Cell::insert_cell(node_map_t& nodes, double *new_cell, int_t p_level, double *xs, double *ys, double *zs, bool diag_balance){ //Inserts a cell at min(max_level,p_level) that contains the given point if(p_level > level){ // Need to go look in children, // Need to spawn children if i don't have any... if(is_leaf()){ divide(nodes, xs, ys, zs, true, diag_balance); } int ix = new_cell[0] > children[0]->points[3]->location[0]; int iy = new_cell[1] > children[0]->points[3]->location[1]; int iz = n_dim>2 && new_cell[2]>children[0]->points[7]->location[2]; children[ix + 2*iy + 4*iz]->insert_cell(nodes, new_cell, p_level, xs, ys, zs, diag_balance); } }; void Cell::refine_func(node_map_t& nodes, function test_func, double *xs, double *ys, double *zs, bool diag_balance){ // return if I'm at the maximum level if (level == max_level){ return; } if(is_leaf()){ // only evaluate the function on leaf cells int test_level = (*test_func)(this); if(test_level < 0){ test_level = (max_level + 1) - (abs(test_level) % (max_level + 1)); } if (test_level <= level){ return; } divide(nodes, xs, ys, zs, true, diag_balance); } // should only be here if not a leaf cell, or I was divided by the function // recurse into children for(int_t i = 0; i < (1<refine_func(nodes, test_func, xs, ys, zs, diag_balance); } } void Cell::refine_image(node_map_t& nodes, double* image, int_t *shape_cells, double *xs, double*ys, double *zs, bool diag_balance){ // early exit if my level is higher than or equal to target if (level == max_level){ return; } int_t start_ix = points[0]->location_ind[0]/2; int_t start_iy = points[0]->location_ind[1]/2; int_t start_iz = n_dim == 2 ? 0 : points[0]->location_ind[2]/2; int_t end_ix = points[3]->location_ind[0]/2; int_t end_iy = points[3]->location_ind[1]/2; int_t end_iz = n_dim == 2? 1 : points[7]->location_ind[2]/2; int_t nx = shape_cells[0]; int_t ny = shape_cells[1]; int_t nz = shape_cells[2]; int_t i_image = (nx * ny) * start_iz + nx * start_iy + start_ix; double val_start = image[i_image]; bool all_unique = true; // if any of the image data contained in the cell are different, subdivide myself for(int_t iz=start_iz; izrefine_image(nodes, image, shape_cells, xs, ys, zs, diag_balance); } } } void Cell::divide(node_map_t& nodes, double* xs, double* ys, double* zs, bool balance, bool diag_balance){ // Gaurd against dividing a cell that is already at the max level if (level == max_level){ return; } //If i haven't already been split... if(is_leaf()){ spawn(nodes, children, xs, ys, zs); //If I need to be split, and my neighbor is below my level //Then it needs to be split //-x,+x,-y,+y,-z,+z if(balance){ for(int_t i = 0; i < 2*n_dim; ++i){ if(neighbors[i] != NULL && neighbors[i]->level < level){ neighbors[i]->divide(nodes, xs, ys, zs, balance, diag_balance); } } } if(diag_balance){ Cell *neighbor; if (neighbors[0] != NULL){ // -x-y if (neighbors[2] != NULL){ neighbor = neighbors[0]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } // -x+y if (neighbors[3] != NULL){ neighbor = neighbors[0]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } if (neighbors[1] != NULL){ // +x-y if (neighbors[2] != NULL){ neighbor = neighbors[1]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } // +x+y if (neighbors[3] != NULL){ neighbor = neighbors[1]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } if(n_dim == 3){ // -z if (neighbors[4] != NULL){ if (neighbors[0] != NULL){ // -z-x neighbor = neighbors[4]->neighbors[0]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } // -z-x-y if (neighbors[2] != NULL){ neighbor = neighbors[4]->neighbors[0]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } // -z-x+y if (neighbors[3] != NULL){ neighbor = neighbors[4]->neighbors[0]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } if (neighbors[1] != NULL){ // -z+x neighbor = neighbors[4]->neighbors[1]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } // -z+x-y if (neighbors[2] != NULL){ neighbor = neighbors[4]->neighbors[1]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } // -z+x+y if (neighbors[3] != NULL){ neighbor = neighbors[4]->neighbors[1]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } if (neighbors[2] != NULL){ // -z-y neighbor = neighbors[4]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } if (neighbors[3] != NULL){ // -z+y neighbor = neighbors[4]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } // +z if (neighbors[5] != NULL){ if (neighbors[0] != NULL){ // +z-x neighbor = neighbors[5]->neighbors[0]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } // +z-x-y if (neighbors[2] != NULL){ neighbor = neighbors[5]->neighbors[0]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } // +z-x+y if (neighbors[3] != NULL){ neighbor = neighbors[5]->neighbors[0]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } if (neighbors[1] != NULL){ // +z+x neighbor = neighbors[5]->neighbors[1]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } // +z+x-y if (neighbors[2] != NULL){ neighbor = neighbors[5]->neighbors[1]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } // +z+x+y if (neighbors[3] != NULL){ neighbor = neighbors[5]->neighbors[1]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } if (neighbors[2] != NULL){ // +z-y neighbor = neighbors[5]->neighbors[2]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } if (neighbors[3] != NULL){ // +z+y neighbor = neighbors[5]->neighbors[3]; if(neighbor->level < level){ neighbor->divide(nodes, xs, ys, zs, balance, diag_balance); } } } } } //Set children's neighbors (first do the easy ones) // all of the children live next to each other children[0]->set_neighbor(children[1], 1); children[0]->set_neighbor(children[2], 3); children[1]->set_neighbor(children[3], 3); children[2]->set_neighbor(children[3], 1); if(n_dim == 3){ children[4]->set_neighbor(children[5], 1); children[4]->set_neighbor(children[6], 3); children[5]->set_neighbor(children[7], 3); children[6]->set_neighbor(children[7], 1); children[0]->set_neighbor(children[4], 5); children[1]->set_neighbor(children[5], 5); children[2]->set_neighbor(children[6], 5); children[3]->set_neighbor(children[7], 5); } // -x direction if(neighbors[0]!=NULL && !(neighbors[0]->is_leaf())){ children[0]->set_neighbor(neighbors[0]->children[1], 0); children[2]->set_neighbor(neighbors[0]->children[3], 0); } else{ children[0]->set_neighbor(neighbors[0], 0); children[2]->set_neighbor(neighbors[0], 0); } // +x direction if(neighbors[1]!=NULL && !neighbors[1]->is_leaf()){ children[1]->set_neighbor(neighbors[1]->children[0], 1); children[3]->set_neighbor(neighbors[1]->children[2], 1); }else{ children[1]->set_neighbor(neighbors[1], 1); children[3]->set_neighbor(neighbors[1], 1); } // -y direction if(neighbors[2]!=NULL && !neighbors[2]->is_leaf()){ children[0]->set_neighbor(neighbors[2]->children[2], 2); children[1]->set_neighbor(neighbors[2]->children[3], 2); }else{ children[0]->set_neighbor(neighbors[2], 2); children[1]->set_neighbor(neighbors[2], 2); } // +y direction if(neighbors[3]!=NULL && !neighbors[3]->is_leaf()){ children[2]->set_neighbor(neighbors[3]->children[0], 3); children[3]->set_neighbor(neighbors[3]->children[1], 3); }else{ children[2]->set_neighbor(neighbors[3], 3); children[3]->set_neighbor(neighbors[3], 3); } if(n_dim==3){ // -x direction if(neighbors[0]!=NULL && !(neighbors[0]->is_leaf())){ children[4]->set_neighbor(neighbors[0]->children[5], 0); children[6]->set_neighbor(neighbors[0]->children[7], 0); } else{ children[4]->set_neighbor(neighbors[0], 0); children[6]->set_neighbor(neighbors[0], 0); } // +x direction if(neighbors[1]!=NULL && !neighbors[1]->is_leaf()){ children[5]->set_neighbor(neighbors[1]->children[4], 1); children[7]->set_neighbor(neighbors[1]->children[6], 1); }else{ children[5]->set_neighbor(neighbors[1], 1); children[7]->set_neighbor(neighbors[1], 1); } // -y direction if(neighbors[2]!=NULL && !neighbors[2]->is_leaf()){ children[4]->set_neighbor(neighbors[2]->children[6], 2); children[5]->set_neighbor(neighbors[2]->children[7], 2); }else{ children[4]->set_neighbor(neighbors[2], 2); children[5]->set_neighbor(neighbors[2], 2); } // +y direction if(neighbors[3]!=NULL && !neighbors[3]->is_leaf()){ children[6]->set_neighbor(neighbors[3]->children[4], 3); children[7]->set_neighbor(neighbors[3]->children[5], 3); }else{ children[6]->set_neighbor(neighbors[3], 3); children[7]->set_neighbor(neighbors[3], 3); } // -z direction if(neighbors[4]!=NULL && !neighbors[4]->is_leaf()){ children[0]->set_neighbor(neighbors[4]->children[4], 4); children[1]->set_neighbor(neighbors[4]->children[5], 4); children[2]->set_neighbor(neighbors[4]->children[6], 4); children[3]->set_neighbor(neighbors[4]->children[7], 4); }else{ children[0]->set_neighbor(neighbors[4], 4); children[1]->set_neighbor(neighbors[4], 4); children[2]->set_neighbor(neighbors[4], 4); children[3]->set_neighbor(neighbors[4], 4); } // +z direction if(neighbors[5]!=NULL && !neighbors[5]->is_leaf()){ children[4]->set_neighbor(neighbors[5]->children[0], 5); children[5]->set_neighbor(neighbors[5]->children[1], 5); children[6]->set_neighbor(neighbors[5]->children[2], 5); children[7]->set_neighbor(neighbors[5]->children[3], 5); }else{ children[4]->set_neighbor(neighbors[5], 5); children[5]->set_neighbor(neighbors[5], 5); children[6]->set_neighbor(neighbors[5], 5); children[7]->set_neighbor(neighbors[5], 5); } } } }; void Cell::build_cell_vector(cell_vec_t& cells){ if(this->is_leaf()){ cells.push_back(this); return; } for(int_t i = 0; i < (1<build_cell_vector(cells); } } Cell* Cell::containing_cell(double x, double y, double z){ if(is_leaf()){ return this; } int ix = x > children[0]->points[3]->location[0]; int iy = y > children[0]->points[3]->location[1]; int iz = n_dim>2 && z>children[0]->points[7]->location[2]; return children[ix + 2*iy + 4*iz]->containing_cell(x, y, z); }; Cell::~Cell(){ if(is_leaf()){ return; } for(int_t i = 0; i< (1< > > points; if(n_dim == 2){ points.resize(1); }else{ points.resize(nz_roots+1); } for(int_t iz = 0; izkey] = points[iz][iy][ix]; } } } // Create grid of root cells for (int_t iz = 0; izlevel = 0; }else{ roots[iz][iy][ix]->level = 1; } for(int_t i = 0; i < (1<reference += 1; } } } } // Set root cell neighbors // +x neighbors for(int_t iz=0; izset_neighbor(roots[iz][iy][ix+1], 1); // +y neighbors for(int_t iz=0; izset_neighbor(roots[iz][iy+1][ix], 3); // +z neighbors for(int_t iz=0; izset_neighbor(roots[iz+1][iy][ix], 5); } } void Tree::insert_cell(double *new_center, int_t p_level, bool diagonal_balance){ // find containing root int_t ix = 0; int_t iy = 0; int_t iz = 0; while (new_center[0]>=xs[ixs[ix+1]] && ix=ys[iys[iy+1]] && iy=zs[izs[iz+1]] && izinsert_cell(nodes, new_center, p_level, xs, ys, zs, diagonal_balance); } void Tree::refine_function(function test_func, bool diagonal_balance){ //Now we can divide for(int_t iz=0; izrefine_func(nodes, test_func, xs, ys, zs, diagonal_balance); }; void Tree::refine_image(double *image, bool diagonal_balance){ int_t shape_cells[3]; shape_cells[0] = nx/2; shape_cells[1] = ny/2; shape_cells[2] = nz/2; for(int_t iz=0; izrefine_image(nodes, image, shape_cells, xs, ys, zs, diagonal_balance); } void Tree::finalize_lists(){ for(int_t iz=0; izbuild_cell_vector(cells); if(n_dim == 3){ // Generate Faces and edges for(std::vector::size_type i = 0; i != cells.size(); i++){ Cell *cell = cells[i]; Node *p[8]; for(int_t it = 0; it < 8; ++it) p[it] = cell->points[it]; Edge *ex[4]; Edge *ey[4]; Edge *ez[4]; ex[0] = set_default_edge(edges_x, *p[0], *p[1]); ex[1] = set_default_edge(edges_x, *p[2], *p[3]); ex[2] = set_default_edge(edges_x, *p[4], *p[5]); ex[3] = set_default_edge(edges_x, *p[6], *p[7]); ey[0] = set_default_edge(edges_y, *p[0], *p[2]); ey[1] = set_default_edge(edges_y, *p[1], *p[3]); ey[2] = set_default_edge(edges_y, *p[4], *p[6]); ey[3] = set_default_edge(edges_y, *p[5], *p[7]); ez[0] = set_default_edge(edges_z, *p[0], *p[4]); ez[1] = set_default_edge(edges_z, *p[1], *p[5]); ez[2] = set_default_edge(edges_z, *p[2], *p[6]); ez[3] = set_default_edge(edges_z, *p[3], *p[7]); Face *fx1, *fx2, *fy1, *fy2, *fz1, *fz2; fx1 = set_default_face(faces_x, *p[0], *p[2], *p[4], *p[6]); fx2 = set_default_face(faces_x, *p[1], *p[3], *p[5], *p[7]); fy1 = set_default_face(faces_y, *p[0], *p[1], *p[4], *p[5]); fy2 = set_default_face(faces_y, *p[2], *p[3], *p[6], *p[7]); fz1 = set_default_face(faces_z, *p[0], *p[1], *p[2], *p[3]); fz2 = set_default_face(faces_z, *p[4], *p[5], *p[6], *p[7]); fx1->edges[0] = ez[0]; fx1->edges[1] = ey[2]; fx1->edges[2] = ez[2]; fx1->edges[3] = ey[0]; fx2->edges[0] = ez[1]; fx2->edges[1] = ey[3]; fx2->edges[2] = ez[3]; fx2->edges[3] = ey[1]; fy1->edges[0] = ez[0]; fy1->edges[1] = ex[2]; fy1->edges[2] = ez[1]; fy1->edges[3] = ex[0]; fy2->edges[0] = ez[2]; fy2->edges[1] = ex[3]; fy2->edges[2] = ez[3]; fy2->edges[3] = ex[1]; fz1->edges[0] = ey[0]; fz1->edges[1] = ex[1]; fz1->edges[2] = ey[1]; fz1->edges[3] = ex[0]; fz2->edges[0] = ey[2]; fz2->edges[1] = ex[3]; fz2->edges[2] = ey[3]; fz2->edges[3] = ex[2]; cell->faces[0] = fx1; cell->faces[1] = fx2; cell->faces[2] = fy1; cell->faces[3] = fy2; cell->faces[4] = fz1; cell->faces[5] = fz2; for(int_t it = 0; it < 4; ++it){ cell->edges[it ] = ex[it]; cell->edges[it + 4] = ey[it]; cell->edges[it + 8] = ez[it]; } for(int_t it = 0; it < 6; ++it) cell->faces[it]->reference++; for(int_t it = 0; it < 12; ++it) cell->edges[it]->reference++; } // Process hanging x faces for(face_it_type it = faces_x.begin(); it != faces_x.end(); ++it){ Face *face = it->second; if(face->reference < 2){ int_t x; x = face->location_ind[0]; if(x==0 || x==nx) continue; // Face was on the outside, and is not hanging if(nodes.count(face->key)) continue; // I will have children (there is a node at my center) Node *node; //Find Parent int_t ip; for(int_t i = 0; i < 4; ++i){ node = face->points[i]; ip = i; if(faces_x.count(node->key)){ face->parent = faces_x[node->key]; break; } } //all of my edges are hanging, and most of my points for(int_t i = 0; i < 4; ++i){ face->edges[i]->hanging = true; face->points[i]->hanging = true; } // the point oposite the parent node key should not be hanging // and also label the edges' parents if(face->points[ip^3]->reference != 6) face->points[ip^3]->hanging = false; face->edges[0]->parents[0] = face->parent->edges[0]; face->edges[0]->parents[1] = face->parent->edges[((ip&1)^1)<<1]; //2020 face->edges[1]->parents[0] = face->parent->edges[1]; face->edges[1]->parents[1] = face->parent->edges[ip>>1<<1^1]; //1133 face->edges[2]->parents[0] = face->parent->edges[((ip&1)^1)<<1]; //2020 face->edges[2]->parents[1] = face->parent->edges[2]; face->edges[3]->parents[0] = face->parent->edges[ip>>1<<1^1]; //1133 face->edges[3]->parents[1] = face->parent->edges[3]; face->points[ip^1]->parents[0] = face->parent->points[(ip&1)^1]; //1010 face->points[ip^1]->parents[1] = face->parent->points[(ip&1)^3]; //3232 face->points[ip^1]->parents[2] = face->parent->points[(ip&1)^1]; //1010 face->points[ip^1]->parents[3] = face->parent->points[(ip&1)^3]; //3232 face->points[ip^2]->parents[0] = face->parent->points[(ip>>1^1)<<1]; //2200 face->points[ip^2]->parents[1] = face->parent->points[(ip>>1^1)<<1^1]; //3311 face->points[ip^2]->parents[2] = face->parent->points[(ip>>1^1)<<1]; //2200 face->points[ip^2]->parents[3] = face->parent->points[(ip>>1^1)<<1^1]; //3311 face->hanging = true; hanging_faces_x.push_back(face); for(int_t i = 0; i < 4; ++i) node->parents[i] = face->parent->points[i]; } } // Process hanging y faces for(face_it_type it = faces_y.begin(); it != faces_y.end(); ++it){ Face *face = it->second; if(face->reference < 2){ int_t y; y = face->location_ind[1]; if(y==0 || y==ny) continue; // Face was on the outside, and is not hanging if(nodes.count(face->key)) continue; // I will have children (there is a node at my center) Node *node; //Find Parent int_t ip; for(int_t i = 0; i < 4; ++i){ node = face->points[i]; ip = i; if(faces_y.count(node->key)){ face->parent = faces_y[node->key]; break; } } //all of my edges are hanging, and most of my points for(int_t i = 0; i < 4; ++i){ face->edges[i]->hanging = true; face->points[i]->hanging = true; } // the point oposite the parent node key should not be hanging // and also label the edges' parents if(face->points[ip^3]->reference != 6) face->points[ip^3]->hanging = false; face->edges[0]->parents[0] = face->parent->edges[0]; face->edges[0]->parents[1] = face->parent->edges[((ip&1)^1)<<1]; //2020 face->edges[1]->parents[0] = face->parent->edges[1]; face->edges[1]->parents[1] = face->parent->edges[ip>>1<<1^1]; //1133 face->edges[2]->parents[0] = face->parent->edges[((ip&1)^1)<<1]; //2020 face->edges[2]->parents[1] = face->parent->edges[2]; face->edges[3]->parents[0] = face->parent->edges[ip>>1<<1^1]; //1133 face->edges[3]->parents[1] = face->parent->edges[3]; face->points[ip^1]->parents[0] = face->parent->points[(ip&1)^1]; //1010 face->points[ip^1]->parents[1] = face->parent->points[(ip&1)^3]; //3232 face->points[ip^1]->parents[2] = face->parent->points[(ip&1)^1]; //1010 face->points[ip^1]->parents[3] = face->parent->points[(ip&1)^3]; //3232 face->points[ip^2]->parents[0] = face->parent->points[(ip>>1^1)<<1]; //2200 face->points[ip^2]->parents[1] = face->parent->points[(ip>>1^1)<<1^1]; //3311 face->points[ip^2]->parents[2] = face->parent->points[(ip>>1^1)<<1]; //2200 face->points[ip^2]->parents[3] = face->parent->points[(ip>>1^1)<<1^1]; //3311 face->hanging = true; hanging_faces_y.push_back(face); for(int_t i = 0; i < 4; ++i){ node->parents[i] = face->parent->points[i]; } } } // Process hanging z faces for(face_it_type it = faces_z.begin(); it != faces_z.end(); ++it){ Face *face = it->second; if(face->reference < 2){ int_t z; z = face->location_ind[2]; if(z==0 || z==nz){ // Face was on the outside, and is not hanging continue; } //check if I am a parent or a child if(nodes.count(face->key)){ // I will have children (there is a node at my center) continue; } Node *node; //Find Parent int_t ip; for(int_t i = 0; i < 4; ++i){ node = face->points[i]; ip = i; if(faces_z.count(node->key)){ face->parent = faces_z[node->key]; ip = i; break; } } //all of my edges are hanging, and most of my points for(int_t i = 0; i < 4; ++i){ face->edges[i]->hanging = true; face->points[i]->hanging = true; } // the point oposite the parent node key should not be hanging // most of the time // and also label the edges' parents if(face->points[ip^3]->reference != 6) face->points[ip^3]->hanging = false; face->edges[0]->parents[0] = face->parent->edges[0]; face->edges[0]->parents[1] = face->parent->edges[((ip&1)^1)<<1]; //2020 face->edges[1]->parents[0] = face->parent->edges[1]; face->edges[1]->parents[1] = face->parent->edges[ip>>1<<1^1]; //1133 face->edges[2]->parents[0] = face->parent->edges[((ip&1)^1)<<1]; //2020 face->edges[2]->parents[1] = face->parent->edges[2]; face->edges[3]->parents[0] = face->parent->edges[ip>>1<<1^1]; //1133 face->edges[3]->parents[1] = face->parent->edges[3]; face->points[ip^1]->parents[0] = face->parent->points[(ip&1)^1]; //1010 face->points[ip^1]->parents[1] = face->parent->points[(ip&1)^3]; //3232 face->points[ip^1]->parents[2] = face->parent->points[(ip&1)^1]; //1010 face->points[ip^1]->parents[3] = face->parent->points[(ip&1)^3]; //3232 face->points[ip^2]->parents[0] = face->parent->points[(ip>>1^1)<<1]; //2200 face->points[ip^2]->parents[1] = face->parent->points[(ip>>1^1)<<1^1]; //3311 face->points[ip^2]->parents[2] = face->parent->points[(ip>>1^1)<<1]; //2200 face->points[ip^2]->parents[3] = face->parent->points[(ip>>1^1)<<1^1]; //3311 face->hanging = true; hanging_faces_z.push_back(face); for(int_t i = 0; i < 4; ++i){ node->parents[i] = face->parent->points[i]; } } } } else{ //Generate Edges (and 1 face for consistency) for(std::vector::size_type i=0; i != cells.size(); i++){ Cell *cell = cells[i]; Node *p[4]; for(int_t i = 0; i < 4; ++i) p[i] = cell->points[i]; Edge *e[4]; e[0] = set_default_edge(edges_x, *p[0], *p[1]); e[1] = set_default_edge(edges_x, *p[2], *p[3]); e[2] = set_default_edge(edges_y, *p[0], *p[2]); e[3] = set_default_edge(edges_y, *p[1], *p[3]); Face *face = set_default_face(faces_z, *p[0], *p[1], *p[2], *p[3]); cell->edges[0] = e[0]; // -x cell->edges[1] = e[1]; // +x cell->edges[2] = e[2]; // -y cell->edges[3] = e[3]; // +y // number these clockwise from x0,y0 face->edges[0] = e[2]; // -y face->edges[1] = e[1]; // +x face->edges[2] = e[3]; // +y face->edges[3] = e[0]; // -x for(int_t i = 0; i < 4; ++i){ e[i]->reference++; } face->hanging=false; } //Process hanging x edges for(edge_it_type it = edges_x.begin(); it != edges_x.end(); ++it){ Edge *edge = it->second; if(edge->reference < 2){ int_t y = edge->location_ind[1]; if(y==0 || y==ny) continue; //I am on the boundary if(nodes.count(edge->key)) continue; //I am a parent //I am a hanging edge find my parent Node *node; if(edges_x.count(edge->points[0]->key)){ node = edge->points[0]; }else{ node = edge->points[1]; } edge->parents[0] = edges_x[node->key]; edge->parents[1] = edge->parents[0]; node->hanging = true; for(int_t i = 0; i<4; ++i) node->parents[i] = edge->parents[0]->points[i%2]; edge->hanging = true; } } //Process hanging y edges for(edge_it_type it = edges_y.begin(); it != edges_y.end(); ++it){ Edge *edge = it->second; if(edge->reference < 2){ int_t x = edge->location_ind[0]; if(x==0 || x==nx) continue; //I am on the boundary if(nodes.count(edge->key)) continue; //I am a parent //I am a hanging edge find my parent Node *node; if(edges_y.count(edge->points[0]->key)){ node = edge->points[0]; }else{ node = edge->points[1]; } edge->parents[0] = edges_y[node->key]; edge->parents[1] = edge->parents[0]; node->hanging = true; for(int_t i = 0; i < 4; ++i) node->parents[i] = edge->parents[0]->points[i%2]; edge->hanging = true; } } } //List hanging edges x for(edge_it_type it = edges_x.begin(); it != edges_x.end(); ++it){ Edge *edge = it->second; if(edge->hanging){ hanging_edges_x.push_back(edge); } } //List hanging edges y for(edge_it_type it = edges_y.begin(); it != edges_y.end(); ++it){ Edge *edge = it->second; if(edge->hanging){ hanging_edges_y.push_back(edge); } } if(n_dim==3){ //List hanging edges z for(edge_it_type it = edges_z.begin(); it != edges_z.end(); ++it){ Edge *edge = it->second; if(edge->hanging){ hanging_edges_z.push_back(edge); } } } //List hanging nodes for(node_it_type it = nodes.begin(); it != nodes.end(); ++it){ Node *node = it->second; if(node->hanging){ hanging_nodes.push_back(node); } } } void Tree::number(){ //Number Nodes int_t ii, ih; ii = 0; ih = nodes.size() - hanging_nodes.size(); for(node_it_type it = nodes.begin(); it != nodes.end(); ++it){ Node *node = it->second; if(node->hanging){ node->index = ih; ++ih; }else{ node->index = ii; ++ii; } } //Number Cells for(std::vector::size_type i = 0; i != cells.size(); ++i) cells[i]->index = i; //Number edges_x ii = 0; ih = edges_x.size() - hanging_edges_x.size(); for(edge_it_type it = edges_x.begin(); it != edges_x.end(); ++it){ Edge *edge = it->second; if(edge->hanging){ edge->index = ih; ++ih; }else{ edge->index = ii; ++ii; } } //Number edges_y ii = 0; ih = edges_y.size() - hanging_edges_y.size(); for(edge_it_type it = edges_y.begin(); it != edges_y.end(); ++it){ Edge *edge = it->second; if(edge->hanging){ edge->index = ih; ++ih; }else{ edge->index = ii; ++ii; } } if(n_dim==3){ //Number faces_x ii = 0; ih = faces_x.size() - hanging_faces_x.size(); for(face_it_type it = faces_x.begin(); it != faces_x.end(); ++it){ Face *face = it->second; if(face->hanging){ face->index = ih; ++ih; }else{ face->index = ii; ++ii; } } //Number faces_y ii = 0; ih = faces_y.size() - hanging_faces_y.size(); for(face_it_type it = faces_y.begin(); it != faces_y.end(); ++it){ Face *face = it->second; if(face->hanging){ face->index = ih; ++ih; }else{ face->index = ii; ++ii; } } //Number faces_z ii = 0; ih = faces_z.size() - hanging_faces_z.size(); for(face_it_type it = faces_z.begin(); it != faces_z.end(); ++it){ Face *face = it->second; if(face->hanging){ face->index = ih; ++ih; }else{ face->index = ii; ++ii; } } //Number edges_z ii = 0; ih = edges_z.size() - hanging_edges_z.size(); for(edge_it_type it = edges_z.begin(); it != edges_z.end(); ++it){ Edge *edge = it->second; if(edge->hanging){ edge->index = ih; ++ih; }else{ edge->index = ii; ++ii; } } }else{ //Ensure Fz and cells are numbered the same in 2D for(std::vector::size_type i = 0; i != cells.size(); ++i) faces_z[cells[i]->key]->index = cells[i]->index; } }; Tree::~Tree(){ if (roots.size() == 0){ return; } for(int_t iz=0; izsecond; } for(face_it_type it = faces_x.begin(); it != faces_x.end(); ++it){ delete it->second; } for(face_it_type it = faces_y.begin(); it != faces_y.end(); ++it){ delete it->second; } for(face_it_type it = faces_z.begin(); it != faces_z.end(); ++it){ delete it->second; } for(edge_it_type it = edges_x.begin(); it != edges_x.end(); ++it){ delete it->second; } for(edge_it_type it = edges_y.begin(); it != edges_y.end(); ++it){ delete it->second; } for(edge_it_type it = edges_z.begin(); it != edges_z.end(); ++it){ delete it->second; } roots.clear(); cells.clear(); nodes.clear(); faces_x.clear(); faces_y.clear(); faces_z.clear(); edges_x.clear(); edges_y.clear(); edges_z.clear(); }; Cell* Tree::containing_cell(double x, double y, double z){ // find containing root int_t ix = 0; int_t iy = 0; int_t iz = 0; while (x>=xs[ixs[ix+1]] && ix=ys[iys[iy+1]] && iy=zs[izs[iz+1]] && izcontaining_cell(x, y, z); } void Tree::shift_cell_centers(double *shift){ for(int_t iz=0; izshift_centers(shift); } ================================================ FILE: discretize/_extensions/tree.h ================================================ #ifndef __TREE_H #define __TREE_H #include #include #include #include #include "geom.h" typedef std::size_t int_t; inline int_t key_func(int_t x, int_t y){ //Double Cantor pairing return ((x+y)*(x+y+1))/2+y; } inline int_t key_func(int_t x, int_t y, int_t z){ return key_func(key_func(x, y), z); } class Node; class Edge; class Face; class Cell; class Tree; class PyWrapper; typedef PyWrapper* function; typedef std::map node_map_t; typedef std::map edge_map_t; typedef std::map face_map_t; typedef node_map_t::iterator node_it_type; typedef edge_map_t::iterator edge_it_type; typedef face_map_t::iterator face_it_type; typedef std::vector cell_vec_t; typedef std::vector int_vec_t; class PyWrapper{ public: void *py_func; int (*eval)(void *, Cell*); PyWrapper(){ py_func = NULL; }; void set(void* func, int (*wrapper)(void*, Cell*)){ py_func = func; eval = wrapper; }; int operator()(Cell * cell){ return eval(py_func, cell); }; }; class Node{ public: int_t location_ind[3]; double location[3]; int_t key; int_t reference; int_t index; bool hanging; Node *parents[4]; Node(); Node(int_t, int_t, int_t, double*, double*, double*); double operator[](int_t index){ return location[index]; }; }; class Edge{ public: int_t location_ind[3]; double location[3]; int_t key; int_t reference; int_t index; double length; bool hanging; Node *points[2]; Edge *parents[2]; Edge(); Edge(Node& p1, Node&p2); double operator[](int_t index){ return location[index]; }; }; class Face{ public: int_t location_ind[3]; double location[3]; int_t key; int_t reference; int_t index; double area; bool hanging; Node *points[4]; Edge *edges[4]; Face *parent; Face(); Face(Node& p1, Node& p2, Node& p3, Node& p4); double operator[](int_t index){ return location[index]; }; }; class Cell{ public: int_t n_dim; Cell *parent, *children[8], *neighbors[6]; Node *points[8]; Edge *edges[12]; Face *faces[6]; int_t location_ind[3], key, level, max_level; long long int index; // non root parents will have a -1 value double location[3]; double operator[](int_t index){ return location[index]; }; double volume; Cell(); Cell(Node *pts[8], int_t ndim, int_t maxlevel);//, function func); Cell(Node *pts[8], Cell *parent); ~Cell(); inline Node* min_node(){ return points[0];}; inline Node* max_node(){ return points[(1< void refine_geom(node_map_t& nodes, const T& geom, int_t p_level, double *xs, double *ys, double* zs, bool diag_balance=false){ // early exit if my level is higher than or equal to target if (level >= p_level || level == max_level){ return; } double *a = min_node()->location; double *b = max_node()->location; // if I intersect cell, I will need to be divided (if I'm not already) if (geom.intersects_cell(a, b)){ if(is_leaf()){ divide(nodes, xs, ys, zs, true, diag_balance); } // recurse into children for(int_t i = 0; i < (1<refine_geom(nodes, geom, p_level, xs, ys, zs, diag_balance); } } } template void find_cells_geom(int_vec_t &cells, const T& geom){ double *a = min_node()->location; double *b = max_node()->location; if(geom.intersects_cell(a, b)){ if(this->is_leaf()){ cells.push_back(index); return; } for(int_t i = 0; i < (1<find_cells_geom(cells, geom); } } } }; class Tree{ public: int_t n_dim; std::vector > > roots; int_t max_level, nx, ny, nz; int_t *ixs, *iys, *izs; int_t nx_roots, ny_roots, nz_roots; double *xs; double *ys; double *zs; std::vector cells; node_map_t nodes; edge_map_t edges_x, edges_y, edges_z; face_map_t faces_x, faces_y, faces_z; std::vector hanging_nodes; std::vector hanging_edges_x, hanging_edges_y, hanging_edges_z; std::vector hanging_faces_x, hanging_faces_y, hanging_faces_z; Tree(); ~Tree(); void set_dimension(int_t dim); void set_levels(int_t l_x, int_t l_y, int_t l_z); void set_xs(double *x , double *y, double *z); void initialize_roots(); void number(); void finalize_lists(); void shift_cell_centers(double *shift); void insert_cell(double *new_center, int_t p_level, bool diagonal_balance=false); Cell* containing_cell(double, double, double); void refine_function(function test_func, bool diagonal_balance=false); void refine_image(double* image, bool diagonal_balance=false); template void refine_geom(const T& geom, int_t p_level, bool diagonal_balance=false){ for(int_t iz=0; izrefine_geom(nodes, geom, p_level, xs, ys, zs, diagonal_balance); }; template int_vec_t find_cells_geom(const T& geom){ int_vec_t intersections; for(int_t iz=0; izfind_cells_geom(intersections, geom); } } } return intersections; }; }; #endif ================================================ FILE: discretize/_extensions/tree.pxd ================================================ from libcpp cimport bool from libcpp.vector cimport vector from libcpp.map cimport map cdef extern from "tree.h": ctypedef int int_t cdef cppclass Node: int_t location_ind[3] double location[3] int_t key int_t reference int_t index bool hanging Node *parents[4] Node() Node(int_t, int_t, int_t, double, double, double) double operator[](int_t) cdef cppclass Edge: int_t location_ind[3] double location[3] int_t key int_t reference int_t index double length bool hanging Node *points[2] Edge *parents[2] Edge() Edge(Node& p1, Node& p2) double operator[](int_t) cdef cppclass Face: int_t location_ind[3] double location[3] int_t key int_t reference int_t index double area bool hanging Node *points[4] Edge *edges[4] Face *parent Face() Face(Node& p1, Node& p2, Node& p3, Node& p4) double operator[](int_t) ctypedef map[int_t, Node *] node_map_t ctypedef map[int_t, Edge *] edge_map_t ctypedef map[int_t, Face *] face_map_t cdef cppclass Cell: int_t n_dim Cell *parent Cell *children[8] Cell *neighbors[6] Node *points[8] Edge *edges[12] Face *faces[6] int_t location_ind[3] double location[3] int_t key, level, max_level long long int index double volume inline bool is_leaf() inline Node* min_node() inline Node* max_node() double operator[](int_t) ctypedef int (*eval_func_ptr)(void*, Cell*) cdef cppclass PyWrapper: PyWrapper() void set(void*, eval_func_ptr eval) cdef cppclass Tree: int_t n_dim int_t max_level, nx, ny, nz vector[Cell *] cells node_map_t nodes edge_map_t edges_x, edges_y, edges_z face_map_t faces_x, faces_y, faces_z vector[Node *] hanging_nodes vector[Edge *] hanging_edges_x, hanging_edges_y, hanging_edges_z vector[Face *] hanging_faces_x, hanging_faces_y, hanging_faces_z Tree() void set_dimension(int_t) void set_levels(int_t, int_t, int_t) void set_xs(double*, double*, double*) void refine_function(PyWrapper *, bool) void refine_geom[T](const T&, int_t, bool) void refine_image(double*, bool) void number() void initialize_roots() void insert_cell(double *new_center, int_t p_level, bool) void finalize_lists() Cell * containing_cell(double, double, double) vector[int_t] find_cells_geom[T](const T& geom) void shift_cell_centers(double*) ================================================ FILE: discretize/_extensions/tree_ext.pyx ================================================ # distutils: language=c++ # cython: embedsignature=True, language_level=3 # cython: linetrace=True # cython: freethreading_compatible=True cimport cython cimport numpy as np from libc.stdlib cimport malloc, free from libcpp.vector cimport vector from libcpp cimport bool from libc.math cimport INFINITY from .tree cimport int_t, Tree as c_Tree, PyWrapper, Node, Edge, Face, Cell as c_Cell from . cimport geom import scipy.sparse as sp import numpy as np from .interputils_cython cimport _bisect_left, _bisect_right class TreeMeshNotFinalizedError(RuntimeError): """Raise when a TreeMesh is not finalized.""" cdef class TreeCell: """A class for defining cells within instances of :class:`~discretize.TreeMesh`. This cannot be created in python, it can only be accessed by indexing the :class:`~discretize.TreeMesh` object. ``TreeCell`` is the object being passed to the user defined refine function when calling the :py:attr:`~discretize.TreeMesh.refine` method for a :class:`~discretize.TreeMesh`. Examples -------- Here, we define a basic :class:`~discretize.TreeMesh` whose refinement is defined by a simple function handle. After we have finalized the mesh, we index a ``TreeCell`` from the mesh. Once indexed, the user may examine its properties (center location, dimensions, index of its neighbors, etc...) >>> from discretize import TreeMesh >>> import numpy as np Refine a tree mesh radially outward from the center >>> mesh = TreeMesh([32,32]) >>> def func(cell): ... r = np.linalg.norm(cell.center-0.5) ... return mesh.max_level if r<0.2 else mesh.max_level-2 >>> mesh.refine(func) Then we can index the mesh to get access to the cell, >>> tree_cell = mesh[16] >>> tree_cell.origin array([0.375, 0.25 ]) """ cdef double _x, _y, _z, _x0, _y0, _z0, _wx, _wy, _wz cdef int_t _dim cdef c_Cell* _cell cdef void _set(self, c_Cell* cell): self._cell = cell self._dim = cell.n_dim @property def nodes(self): """Indices for this cell's nodes within its parent tree mesh. This property returns the indices of the nodes in the parent tree mesh which correspond to this tree cell's nodes. Returns ------- list of int Indices for this cell's nodes within its parent tree mesh """ cdef Node *points[8] points = self._cell.points if self._dim == 3: return [points[0].index, points[1].index, points[2].index, points[3].index, points[4].index, points[5].index, points[6].index, points[7].index] return [points[0].index, points[1].index, points[2].index, points[3].index] @property def edges(self): """Indices for this cell's edges within its parent tree mesh. This property returns the indices of the edges in the parent tree mesh which correspond to this tree cell's edges. Returns ------- list of int Indices for this cell's edges within its parent tree mesh """ cdef Edge *edges[12] edges = self._cell.edges if self._dim == 2: return [edges[0].index, edges[1].index, edges[2].index, edges[3].index] return [ edges[0].index, edges[1].index, edges[2].index, edges[3].index, edges[4].index, edges[5].index, edges[6].index, edges[7].index, edges[8].index, edges[9].index, edges[10].index, edges[11].index, ] @property def faces(self): """Indices for this cell's faces within its parent tree mesh. This property returns the indices of the faces in the parent tree mesh which correspond to this tree cell's faces. Returns ------- list of int Indices for this cell's faces within its parent tree mesh """ cdef Face *faces[6] faces = self._cell.faces if self._dim == 3: return [ faces[0].index, faces[1].index, faces[2].index, faces[3].index, faces[4].index, faces[5].index ] cdef Edge *edges[12] edges = self._cell.edges return [edges[2].index, edges[3].index, edges[0].index, edges[1].index] @property def center(self): """Cell center location for the tree cell. Returns ------- (dim) numpy.ndarray Cell center location for the tree cell """ loc = self._cell.location if self._dim == 2: return np.array([loc[0], loc[1]]) return np.array([loc[0], loc[1], loc[2]]) @property def origin(self): """Origin location ('anchor point') for the tree cell. This property returns the origin location (or 'anchor point') for the tree cell. The origin location is defined as the bottom-left-front corner of the tree cell. Returns ------- (dim) numpy.ndarray Origin location ('anchor point') for the tree cell """ loc = self._cell.min_node().location if self._dim == 2: return np.array([loc[0], loc[1]]) return np.array([loc[0], loc[1], loc[2]]) @property def x0(self): """Origin location ('anchor point') for the tree cell. This property returns the origin location (or 'anchor point') for the tree cell. The origin location is defined as the bottom-left-front corner of the tree cell. Returns ------- (dim) numpy.ndarray Origin location ('anchor point') for the tree cell """ return self.origin @property def h(self): """Cell dimension along each axis direction. This property returns a 1D array containing the dimensions of the tree cell along the x, y (and z) directions, respectively. Returns ------- (dim) numpy.ndarray Cell dimension along each axis direction """ loc_min = self._cell.min_node().location loc_max = self._cell.max_node().location if self._dim == 2: return np.array([ loc_max[0] - loc_min[0], loc_max[1] - loc_min[1], ]) return np.array([ loc_max[0] - loc_min[0], loc_max[1] - loc_min[1], loc_max[2] - loc_min[2], ]) @property def dim(self): """Dimension of the tree cell; 1, 2 or 3. Returns ------- int Dimension of the tree cell; 1, 2 or 3 """ return self._dim @property def index(self): """Index of the tree cell within its parent tree mesh. Returns ------- int Index of the tree cell within its parent tree mesh """ return self._cell.index @property def bounds(self): """ Bounds of the cell. Coordinates that define the bounds of the cell. Bounds are returned in the following order: ``x0``, ``x1``, ``y0``, ``y1``, ``z0``, ``z1``. Returns ------- bounds : (2 * dim) array Array with the cell bounds. """ loc_min = self._cell.min_node().location loc_max = self._cell.max_node().location if self.dim == 2: return np.array( [ loc_min[0], loc_max[0], loc_min[1], loc_max[1], ] ) return np.array( [ loc_min[0], loc_max[0], loc_min[1], loc_max[1], loc_min[2], loc_max[2], ] ) @property def neighbors(self): """Indices for this cell's neighbors within its parent tree mesh. Returns a list containing the indexes for the cell's neighbors. The ordering of the neighboring cells (i.e. the list) is [-x, +x, -y, +y, -z, +z]. For each entry in the list, there are several cases: - *ind >= 0:* the cell has a single neighbor in this direct and *ind* denotes its index - *ind = -1:* the neighbour in this direction lies outside the mesh - *ind = [ind_1, ind_2, ...]:* When the level changes between cells it shares a boarder with more than 1 cell in this direction (2 if `dim==2`, 4 if `dim==3`). Returns ------- list of int or (list of int) Indices for this cell's neighbors within its parent tree mesh """ neighbors = [-1]*self._dim*2 for i in range(self._dim*2): neighbor = self._cell.neighbors[i] if neighbor is NULL: continue elif neighbor.is_leaf(): neighbors[i] = neighbor.index else: if self._dim==2: if i==0: neighbors[i] = [neighbor.children[1].index, neighbor.children[3].index] elif i==1: neighbors[i] = [neighbor.children[0].index, neighbor.children[2].index] elif i==2: neighbors[i] = [neighbor.children[2].index, neighbor.children[3].index] else: neighbors[i] = [neighbor.children[0].index, neighbor.children[1].index] else: if i==0: neighbors[i] = [neighbor.children[1].index, neighbor.children[3].index, neighbor.children[5].index, neighbor.children[7].index] elif i==1: neighbors[i] = [neighbor.children[0].index, neighbor.children[2].index, neighbor.children[4].index, neighbor.children[6].index] elif i==2: neighbors[i] = [neighbor.children[2].index, neighbor.children[3].index, neighbor.children[6].index, neighbor.children[7].index] elif i==3: neighbors[i] = [neighbor.children[0].index, neighbor.children[1].index, neighbor.children[4].index, neighbor.children[5].index] elif i==4: neighbors[i] = [neighbor.children[4].index, neighbor.children[5].index, neighbor.children[6].index, neighbor.children[7].index] else: neighbors[i] = [neighbor.children[0].index, neighbor.children[1].index, neighbor.children[2].index, neighbor.children[3].index] return neighbors @property def _index_loc(self): loc_ind = self._cell.location_ind if self._dim == 2: return tuple((loc_ind[0], loc_ind[1])) return tuple((loc_ind[0], loc_ind[1], loc_ind[2])) @property def _level(self): return self._cell.level cdef int _evaluate_func(void* function, c_Cell* cell) noexcept with gil: # Wraps a function to be called in C++ func = function pycell = TreeCell() pycell._set(cell) return func(pycell) cdef class _TreeMesh: cdef c_Tree *tree cdef PyWrapper *wrapper cdef int_t _dim cdef int_t[3] ls cdef int _finalized cdef bool _diagonal_balance cdef cython.pymutex _tree_modify_lock cdef double[:] _xs, _ys, _zs cdef double[:] _origin cdef object _cell_centers, _nodes, _hanging_nodes cdef object _edges_x, _edges_y, _edges_z, _hanging_edges_x, _hanging_edges_y, _hanging_edges_z cdef object _faces_x, _faces_y, _faces_z, _hanging_faces_x, _hanging_faces_y, _hanging_faces_z cdef object _h_gridded cdef object _cell_volumes, _face_areas, _edge_lengths cdef object _average_face_x_to_cell, _average_face_y_to_cell, _average_face_z_to_cell, _average_face_to_cell, _average_face_to_cell_vector cdef object _average_node_to_cell, _average_node_to_edge, _average_node_to_edge_x, _average_node_to_edge_y, _average_node_to_edge_z cdef object _average_node_to_face, _average_node_to_face_x, _average_node_to_face_y, _average_node_to_face_z cdef object _average_edge_x_to_cell, _average_edge_y_to_cell, _average_edge_z_to_cell, _average_edge_to_cell, _average_edge_to_cell_vector cdef object _average_cell_to_face, _average_cell_vector_to_face, _average_cell_to_face_x, _average_cell_to_face_y, _average_cell_to_face_z cdef object _face_divergence cdef object _edge_curl, _nodal_gradient cdef object __ubc_order, __ubc_indArr def __cinit__(self, *args, **kwargs): self.wrapper = new PyWrapper() self.tree = new c_Tree() def __init__(self, h, origin, bool diagonal_balance=False): super().__init__(h=h, origin=origin) def is_pow2(num): return ((num & (num - 1)) == 0) and num != 0 for n in self.shape_cells: if not is_pow2(n): raise ValueError("length of cell width vectors must be a power of 2") h = self.h origin = self.origin nx2 = 2*len(h[0]) ny2 = 2*len(h[1]) self._dim = len(origin) self._origin = origin xs = np.empty(nx2 + 1, dtype=float) xs[::2] = np.cumsum(np.r_[origin[0], h[0]]) xs[1::2] = (xs[:-1:2] + xs[2::2])/2 self._xs = xs self.ls[0] = int(np.log2(len(h[0]))) ys = np.empty(ny2 + 1, dtype=float) ys[::2] = np.cumsum(np.r_[origin[1],h[1]]) ys[1::2] = (ys[:-1:2] + ys[2::2])/2 self._ys = ys self.ls[1] = int(np.log2(len(h[1]))) if self._dim > 2: nz2 = 2*len(h[2]) zs = np.empty(nz2 + 1, dtype=float) zs[::2] = np.cumsum(np.r_[origin[2],h[2]]) zs[1::2] = (zs[:-1:2] + zs[2::2])/2 self._zs = zs self.ls[2] = int(np.log2(len(h[2]))) else: self._zs = np.zeros(1, dtype=float) self.ls[2] = 1 self.tree.set_dimension(self._dim) self.tree.set_levels(self.ls[0], self.ls[1], self.ls[2]) self.tree.set_xs(&self._xs[0], &self._ys[0], &self._zs[0]) self.tree.initialize_roots() self._finalized = False self._diagonal_balance = diagonal_balance self._clear_cache() def _clear_cache(self): self._cell_centers = None self._nodes = None self._hanging_nodes = None self._h_gridded = None self._edges_x = None self._edges_y = None self._edges_z = None self._hanging_edges_x = None self._hanging_edges_y = None self._hanging_edges_z = None self._faces_x = None self._faces_y = None self._faces_z = None self._hanging_faces_x = None self._hanging_faces_y = None self._hanging_faces_z = None self._cell_volumes = None self._face_areas = None self._edge_lengths = None self._average_cell_to_face = None self._average_cell_to_face_x = None self._average_cell_to_face_y = None self._average_cell_to_face_z = None self._average_face_x_to_cell = None self._average_face_y_to_cell = None self._average_face_z_to_cell = None self._average_face_to_cell = None self._average_face_to_cell_vector = None self._average_edge_x_to_cell = None self._average_edge_y_to_cell = None self._average_edge_z_to_cell = None self._average_edge_to_cell = None self._average_edge_to_cell_vector = None self._average_edge_to_face = None self._average_node_to_cell = None self._average_node_to_edge = None self._average_node_to_face = None self._average_node_to_edge_x = None self._average_node_to_edge_y = None self._average_node_to_edge_z = None self._average_node_to_face_x = None self._average_node_to_face_y = None self._average_node_to_face_z = None self._face_divergence = None self._nodal_gradient = None self._edge_curl = None self.__ubc_order = None self.__ubc_indArr = None def refine(self, function, finalize=True, diagonal_balance=None): """Refine :class:`~discretize.TreeMesh` with user-defined function. Refines the :class:`~discretize.TreeMesh` according to a user-defined function. The function is recursively called on each cell of the mesh. The user-defined function **must** accept an object of type :class:`~discretize.tree_mesh.TreeCell` and **must** return an integer-like object denoting the desired refinement level. Instead of a function, the user may also supply an integer defining the minimum refinement level for all cells. Parameters ---------- function : callable or int a function defining the desired refinement level, or an integer to refine all cells to at least that level. The input argument of the function **must** be an instance of :class:`~discretize.tree_mesh.TreeCell`. finalize : bool, optional whether to finalize the mesh diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- Here, we define a QuadTree mesh with a domain width of 1 along the x and y axes. We then refine the mesh to its maximum level at locations within a distance of 0.2 of point (0.5, 0.5). The function accepts and instance of :class:`~discretize.tree_mesh.TreeCell` and returns an integer value denoting its level of refinement. >>> from discretize import TreeMesh >>> from matplotlib import pyplot Define a mesh and refine it radially outward using the custom defined function >>> mesh = TreeMesh([32,32]) >>> def func(cell): ... r = np.linalg.norm(cell.center-0.5) ... return mesh.max_level if r<0.2 else mesh.max_level-2 >>> mesh.refine(func) >>> mesh.plot_grid() >>> pyplot.show() """ if isinstance(function, int): level = function function = lambda cell: level if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance #Wrapping function so it can be called in c++ cdef void * func_ptr = function with self._tree_modify_lock: self.wrapper.set(func_ptr, _evaluate_func) #Then tell c++ to build the tree self.tree.refine_function(self.wrapper, diag_balance) if finalize: self.finalize() @cython.cdivision(True) def refine_ball(self, points, radii, levels, finalize=True, diagonal_balance=None): """Refine :class:`~discretize.TreeMesh` using radial distance (ball) and refinement level for a cluster of points. For each point in the array `points`, this method refines the tree mesh based on the radial distance (ball) and refinement level supplied. The method accomplishes this by determining which cells intersect ball(s) and refining them to the prescribed level(s) of refinement. Parameters ---------- points : (N, dim) array_like The centers of the refinement balls radii : float or (N) array_like of float A 1D array defining the radius for each ball levels : int or (N) array_like of int A 1D array defining the maximum refinement level for each ball finalize : bool, optional Whether to finalize after refining diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- We create a simple mesh and refine the tree mesh such that all cells that intersect the spherical balls are at the given levels. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> tree_mesh = discretize.TreeMesh([32, 32]) >>> tree_mesh.max_level 5 Next we define the center and radius of the two spheres, as well as the level we want to refine them to, and refine the mesh. >>> centers = [[0.1, 0.3], [0.6, 0.8]] >>> radii = [0.2, 0.3] >>> levels = [4, 5] >>> tree_mesh.refine_ball(centers, radii, levels) Now lets look at the mesh, and overlay the balls on it to ensure it refined where we wanted it to. >>> ax = tree_mesh.plot_grid() >>> circ = patches.Circle(centers[0], radii[0], facecolor='none', edgecolor='r', linewidth=3) >>> ax.add_patch(circ) >>> circ = patches.Circle(centers[1], radii[1], facecolor='none', edgecolor='k', linewidth=3) >>> ax.add_patch(circ) >>> plt.show() """ points = self._require_ndarray_with_dim('points', points, ndim=2, dtype=np.float64) radii = np.require(np.atleast_1d(radii), dtype=np.float64, requirements='C') levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int_t n_balls = _check_first_dim_broadcast(points=points, radii=radii, levels=levels) cdef double[:, :] cs = points cdef double[:] rs = radii cdef int[:] ls = levels cdef int_t cs_step = cs.shape[0] > 1 cdef int_t rs_step = rs.shape[0] > 1 cdef int_t l_step = ls.shape[0] > 1 cdef int_t i_c=0, i_r=0, i_l=0 if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef geom.Ball ball cdef int_t i cdef int l cdef int max_level = self.max_level for i in range(n_balls): ball = geom.Ball(self._dim, &cs[i_c, 0], rs[i_r]) l = _wrap_levels(ls[i_l], max_level) with self._tree_modify_lock: self.tree.refine_geom(ball, l, diag_balance) i_c += cs_step i_r += rs_step i_l += l_step if finalize: self.finalize() @cython.cdivision(True) def refine_box(self, x0s, x1s, levels, finalize=True, diagonal_balance=None): """Refine the :class:`~discretize.TreeMesh` within the axis aligned boxes to the desired level. Refines the TreeMesh by determining if a cell intersects the given axis aligned box(es) to the prescribed level(s). Parameters ---------- x0s : (N, dim) array_like The minimum location of the boxes x1s : (N, dim) array_like The maximum location of the boxes levels : int or (N) array_like of int The level to refine intersecting cells to finalize : bool, optional Whether to finalize after refining diagonal_balance : None or bool, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- We create a simple mesh and refine the TreeMesh such that all cells that intersect the boxes are at the given levels. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> tree_mesh = discretize.TreeMesh([32, 32]) >>> tree_mesh.max_level 5 Next we define the origins and furthest corners of the two rectangles, as well as the level we want to refine them to, and refine the mesh. >>> x0s = [[0.1, 0.1], [0.8, 0.8]] >>> x1s = [[0.3, 0.2], [0.9, 1.0]] >>> levels = [4, 5] >>> tree_mesh.refine_box(x0s, x1s, levels) Now lets look at the mesh, and overlay the boxes on it to ensure it refined where we wanted it to. >>> ax = tree_mesh.plot_grid() >>> rect = patches.Rectangle([0.1, 0.1], 0.2, 0.1, facecolor='none', edgecolor='r', linewidth=3) >>> ax.add_patch(rect) >>> rect = patches.Rectangle([0.8, 0.8], 0.1, 0.2, facecolor='none', edgecolor='k', linewidth=3) >>> ax.add_patch(rect) >>> plt.show() """ x0s = self._require_ndarray_with_dim('x0s', x0s, ndim=2, dtype=np.float64) x1s = self._require_ndarray_with_dim('x1s', x1s, ndim=2, dtype=np.float64) levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int_t n_boxes = _check_first_dim_broadcast(x0s=x0s, x1s=x1s, levels=levels) cdef double[:, :] x0 = x0s cdef double[:, :] x1 = x1s cdef int[:] ls = levels cdef int_t x0_step = x0.shape[0] > 1 cdef int_t x1_step = x1.shape[0] > 1 cdef int_t l_step = ls.shape[0] > 1 cdef int_t i_x0=0, i_x1=0, i_l=0 if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef geom.Box box cdef int l cdef int max_level = self.max_level for i in range(n_boxes): box = geom.Box(self._dim, &x0[i_x0, 0], &x1[i_x1, 0]) l = _wrap_levels(ls[i_l], max_level) with self._tree_modify_lock: self.tree.refine_geom(box, l, diag_balance) i_x0 += x0_step i_x1 += x1_step i_l += l_step if finalize: self.finalize() @cython.cdivision(True) def refine_line(self, path, levels, finalize=True, diagonal_balance=None): """Refine the :class:`~discretize.TreeMesh` along the line segment to the desired level. Refines the TreeMesh by determining if a cell intersects the given line segment(s) to the prescribed level(s). Parameters ---------- path : (N+1, dim) array_like The nodes of the line segment(s). i.e. `[[x0, y0, z0], [x1, y1, z1], [x2, y2, z2]]` would be two segments. levels : int or (N) array_like of int The level to refine intersecting cells to. finalize : bool, optional Whether to finalize after refining diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- We create a simple mesh and refine the TreeMesh such that all cells that intersect the line segment path are at the given levels. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> tree_mesh = discretize.TreeMesh([32, 32]) >>> tree_mesh.max_level 5 Next we define the points along the line and the level we want to refine to, and refine the mesh. >>> segments = np.array([[0.1, 0.3], [0.3, 0.9], [0.8, 0.9]]) >>> levels = 5 >>> tree_mesh.refine_line(segments, levels) Now lets look at the mesh, and overlay the line on it to ensure it refined where we wanted it to. >>> ax = tree_mesh.plot_grid() >>> ax.plot(*segments.T, color='C1') >>> plt.show() """ path = self._require_ndarray_with_dim('path', path, ndim=2, dtype=np.float64) levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int_t n_segments = _check_first_dim_broadcast(path=path[:-1], levels=levels) cdef double[:, :] line_nodes = path cdef int[:] ls = levels cdef int_t line_step = line_nodes.shape[0] > 2 cdef int_t l_step = levels.shape[0] > 1 cdef int_t i_line=0, i_l=0 if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef geom.Line line cdef int l cdef int max_level = self.max_level cdef int i for i in range(n_segments): line = geom.Line(self._dim, &line_nodes[i_line, 0], &line_nodes[i_line+1, 0]) l = _wrap_levels(ls[i_l], max_level) with self._tree_modify_lock: self.tree.refine_geom(line, l, diag_balance) i_line += line_step i_l += l_step if finalize: self.finalize() @cython.cdivision(True) def refine_plane(self, origins, normals, levels, finalize=True, diagonal_balance=None): """Refine the :class:`~discretize.TreeMesh` along a plane to the desired level. Refines the TreeMesh by determining if a cell intersects the given plane(s) to the prescribed level(s). Parameters ---------- origins : (dim) or (N, dim) array_like of float The origin of the planes. normals : (dim) or (N, dim) array_like of float The normals to the planes. levels : int or (N) array_like of int The level to refine intersecting cells to. finalize : bool, optional Whether to finalize after refining. diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- We create a simple mesh and refine the TreeMesh such that all cells that intersect the plane path are at the given levels. (In 2D, the plane is also a line.) >>> import discretize >>> import matplotlib.pyplot as plt >>> tree_mesh = discretize.TreeMesh([32, 32]) >>> tree_mesh.max_level 5 Next we define the origin and normal of the plane, and the level we want to refine to. >>> origin = [0, 0.25] >>> normal = [-1, -1] >>> level = -1 >>> tree_mesh.refine_plane(origin, normal, level) Now lets look at the mesh, and overlay the plane on it to ensure it refined where we wanted it to. >>> ax = tree_mesh.plot_grid() >>> ax.axline(origin, slope=-normal[0]/normal[1], color='C1') >>> plt.show() """ origins = self._require_ndarray_with_dim('origins', origins, ndim=2, dtype=np.float64) normals = self._require_ndarray_with_dim('normals', normals, ndim=2, dtype=np.float64) levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int n_planes = _check_first_dim_broadcast(origins=origins, normals=normals, levels=levels) cdef double[:, :] x_0s = origins cdef double[:, :] norms = normals cdef int[:] ls = levels cdef int_t origin_step = x_0s.shape[0] > 1 cdef int_t normal_step = norms.shape[0] > 1 cdef int_t level_step = ls.shape[0] > 1 cdef int_t i_o=0, i_n=0, i_l=0 if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef geom.Plane plane cdef int l cdef int max_level = self.max_level cdef int i_plane for i in range(n_planes): plane = geom.Plane(self._dim, &x_0s[i_o, 0], &norms[i_n, 0]) l = _wrap_levels(ls[i_l], max_level) with self._tree_modify_lock: self.tree.refine_geom(plane, l, diag_balance) i_o += origin_step i_n += normal_step i_l += level_step if finalize: self.finalize() @cython.cdivision(True) def refine_triangle(self, triangle, levels, finalize=True, diagonal_balance=None): """Refine the :class:`~discretize.TreeMesh` along the triangle to the desired level. Refines the TreeMesh by determining if a cell intersects the given triangle(s) to the prescribed level(s). Parameters ---------- triangle : (N, 3, dim) array_like The nodes of the triangle(s). levels : int or (N) array_like of int The level to refine intersecting cells to. finalize : bool, optional Whether to finalize after refining diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- We create a simple mesh and refine the TreeMesh such that all cells that intersect the line segment path are at the given levels. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> tree_mesh = discretize.TreeMesh([32, 32]) >>> tree_mesh.max_level 5 Next we define the points along the line and the level we want to refine to, and refine the mesh. >>> triangle = [[0.14, 0.31], [0.32, 0.96], [0.23, 0.87]] >>> levels = 5 >>> tree_mesh.refine_triangle(triangle, levels) Now lets look at the mesh, and overlay the line on it to ensure it refined where we wanted it to. >>> ax = tree_mesh.plot_grid() >>> tri = patches.Polygon(triangle, fill=False) >>> ax.add_patch(tri) >>> plt.show() """ triangle = self._require_ndarray_with_dim('triangle', triangle, ndim=3, dtype=np.float64) if triangle.shape[-2] != 3: raise ValueError(f"triangle array must be (N, 3, {self.dim})") levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int n_triangles = _check_first_dim_broadcast(triangle=triangle, levels=levels) cdef double[:, :, :] tris = triangle cdef int[:] ls = levels cdef int_t tri_step = tris.shape[0] > 1 cdef int_t l_step = ls.shape[0] > 1 cdef int_t i_tri=0, i_l=0 if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef geom.Triangle triang cdef int l cdef int max_level = self.max_level for i in range(n_triangles): triang = geom.Triangle(self._dim, &tris[i_tri, 0, 0], &tris[i_tri, 1, 0], &tris[i_tri, 2, 0]) l = _wrap_levels(ls[i_l], max_level) with self._tree_modify_lock: self.tree.refine_geom(triang, l, diag_balance) i_tri += tri_step i_l += l_step if finalize: self.finalize() @cython.cdivision(True) def refine_vertical_trianglular_prism(self, triangle, h, levels, finalize=True, diagonal_balance=None): """Refine the :class:`~discretize.TreeMesh` along the trianglular prism to the desired level. Refines the TreeMesh by determining if a cell intersects the given trianglular prism(s) to the prescribed level(s). Parameters ---------- triangle : (N, 3, dim) array_like The nodes of the bottom triangle(s). h : (N) array_like The height of the prism(s). levels : int or (N) array_like of int The level to refine intersecting cells to. finalize : bool, optional Whether to finalize after refining diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. See Also -------- refine_surface Examples -------- We create a simple mesh and refine the TreeMesh such that all cells that intersect the line segment path are at the given levels. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> mesh = discretize.TreeMesh([32, 32, 32]) >>> mesh.max_level 5 Next we define the bottom points of the prism, its heights, and the level we want to refine to, then refine the mesh. >>> triangle = [[0.14, 0.31, 0.21], [0.32, 0.96, 0.34], [0.87, 0.23, 0.12]] >>> height = 0.35 >>> levels = 5 >>> mesh.refine_vertical_trianglular_prism(triangle, height, levels) Now lets look at the mesh. >>> v = mesh.cell_levels_by_index(np.arange(mesh.n_cells)) >>> fig, axs = plt.subplots(1, 3, figsize=(12,4)) >>> mesh.plot_slice(v, ax=axs[0], normal='x', grid=True, clim=[2, 5]) >>> mesh.plot_slice(v, ax=axs[1], normal='y', grid=True, clim=[2, 5]) >>> mesh.plot_slice(v, ax=axs[2], normal='z', grid=True, clim=[2, 5]) >>> plt.show() """ if self.dim == 2: raise NotImplementedError("refine_vertical_trianglular_prism only implemented in 3D.") triangle = self._require_ndarray_with_dim('triangle', triangle, ndim=3, dtype=np.float64) if triangle.shape[-2] != 3: raise ValueError(f"triangle array must be (N, 3, {self.dim})") h = np.require(np.atleast_1d(h), dtype=np.float64, requirements="C") if np.any(h < 0): raise ValueError("All heights must be positive.") levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int_t n_triangles = _check_first_dim_broadcast(triangle=triangle, h=h, levels=levels) cdef double[:, :, :] tris = triangle cdef double[:] hs = h cdef int[:] ls = levels cdef int_t tri_step = tris.shape[0] > 1 cdef int_t h_step = hs.shape[0] > 1 cdef int_t l_step = ls.shape[0] > 1 cdef int_t i_tri=0, i_h=0, i_l=0 if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef geom.VerticalTriangularPrism vert_prism cdef int l cdef int max_level = self.max_level for i in range(n_triangles): vert_prism = geom.VerticalTriangularPrism(self._dim, &tris[i_tri, 0, 0], &tris[i_tri, 1, 0], &tris[i_tri, 2, 0], hs[i_h]) l = _wrap_levels(ls[i_l], max_level) with self._tree_modify_lock: self.tree.refine_geom(vert_prism, l, diag_balance) i_tri += tri_step i_h += h_step i_l += l_step if finalize: self.finalize() @cython.cdivision(True) def refine_tetrahedron(self, tetra, levels, finalize=True, diagonal_balance=None): """Refine the :class:`~discretize.TreeMesh` along the tetrahedron to the desired level. Refines the TreeMesh by determining if a cell intersects the given triangle(s) to the prescribed level(s). Parameters ---------- tetra : (N, dim+1, dim) array_like The nodes of the tetrahedron(s). levels : int or (N) array_like of int The level to refine intersecting cells to. finalize : bool, optional Whether to finalize after refining diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- We create a simple mesh and refine the TreeMesh such that all cells that intersect the line segment path are at the given levels. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> tree_mesh = discretize.TreeMesh([32, 32, 32]) >>> tree_mesh.max_level 5 Next we define the points along the line and the level we want to refine to, and refine the mesh. >>> tetra = [ ... [0.32, 0.21, 0.15], ... [0.82, 0.19, 0.34], ... [0.14, 0.82, 0.29], ... [0.32, 0.27, 0.83], ... ] >>> levels = 5 >>> tree_mesh.refine_tetrahedron(tetra, levels) Now lets look at the mesh, checking how the refine function proceeded. >>> levels = tree_mesh.cell_levels_by_index(np.arange(tree_mesh.n_cells)) >>> ax = plt.gca() >>> tree_mesh.plot_slice(levels, normal='z', slice_loc=0.2, grid=True, ax=ax) >>> plt.show() """ if self.dim == 2: return self.refine_triangle(tetra, levels, finalize=finalize, diagonal_balance=diagonal_balance) tetra = self._require_ndarray_with_dim('tetra', tetra, ndim=3, dtype=np.float64) if tetra.shape[-2] != self.dim+1: raise ValueError(f"tetra array must be (N, {self.dim+1}, {self.dim})") levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int_t n_triangles = _check_first_dim_broadcast(tetra=tetra, levels=levels) cdef double[:, :, :] tris = tetra cdef int[:] ls = levels cdef int_t tri_step = tris.shape[0] > 1 cdef int_t l_step = ls.shape[0] > 1 cdef int_t i_tri=0, i_l=0 if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef geom.Tetrahedron tet cdef int l cdef int max_level = self.max_level for i in range(n_triangles): l = _wrap_levels(ls[i_l], max_level) tet = geom.Tetrahedron(self._dim, &tris[i_tri, 0, 0], &tris[i_tri, 1, 0], &tris[i_tri, 2, 0], &tris[i_tri, 3, 0]) with self._tree_modify_lock: self.tree.refine_geom(tet, l, diag_balance) i_tri += tri_step i_l += l_step if finalize: self.finalize() @cython.cdivision(True) def insert_cells(self, points, levels, finalize=True, diagonal_balance=None): """Insert cells into the :class:`~discretize.TreeMesh` that contain given points. Insert cell(s) into the :class:`~discretize.TreeMesh` that contain the given point(s) at the assigned level(s). Parameters ---------- points : (N, dim) array_like levels : (N) array_like of int finalize : bool, optional Whether to finalize after inserting point(s) diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. Examples -------- >>> from discretize import TreeMesh >>> mesh = TreeMesh([32,32]) >>> mesh.insert_cells([0.5, 0.5], mesh.max_level) >>> mesh QuadTreeMesh: 3.91%% filled Level : Number of cells Mesh Extent Cell Widths ----------------------- min , max min , max 2 : 12 --------------------------- -------------------- 3 : 13 x: 0.0 , 1.0 0.03125 , 0.25 4 : 11 y: 0.0 , 1.0 0.03125 , 0.25 5 : 4 ----------------------- Total : 40 """ points = self._require_ndarray_with_dim('points', points, ndim=2, dtype=np.float64) levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') cdef int_t n_points = _check_first_dim_broadcast(points=points, levels=levels) cdef double[:, :] cs = points cdef int[:] ls = levels cdef int l cdef int max_level = self.max_level if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance cdef int_t p_step = cs.shape[0] > 1 cdef int_t l_step = ls.shape[0] > 1 cdef int_t i_p=0, i_l=0 for i in range(ls.shape[0]): l = _wrap_levels(ls[i_l], max_level) with self._tree_modify_lock: self.tree.insert_cell(&cs[i_p, 0], l, diagonal_balance) i_l += l_step i_p += p_step if finalize: self.finalize() def refine_image(self, image, finalize=True, diagonal_balance=None): """Refine using an ND image, ensuring that each cell contains exactly one unique value. This function takes an N-dimensional image, defined on the underlying fine tensor mesh, and recursively subdivides each cell if that cell contains more than 1 unique value in the image. This is useful when using the `TreeMesh` to represent an exact compressed form of an input model. Parameters ---------- image : (shape_cells) numpy.ndarray Must have the same shape as the base tensor mesh (`TreeMesh.shape_cells`), as if every cell on this mesh was refined to it's maximum level. finalize : bool, optional Whether to finalize after inserting point(s) diagonal_balance : bool or None, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the `TreeMesh`. """ if diagonal_balance is None: diagonal_balance = self._diagonal_balance cdef bool diag_balance = diagonal_balance image = np.require(image, dtype=np.float64, requirements="F") cdef size_t n_expected = np.prod(self.shape_cells) if image.size != n_expected: raise ValueError( f"image array size: {image.size} must match the total number of cells in the base tensor mesh: {n_expected}" ) if image.ndim == 1: image = image.reshape(self.shape_cells, order="F") if image.shape != self.shape_cells: raise ValueError( f"image array shape: {image.shape} must match the base cell shapes: {self.shape_cells}" ) if self.dim == 2: image = image[..., None] cdef double[::1,:,:] image_dat = image with self._tree_modify_lock: self.tree.refine_image(&image_dat[0, 0, 0], diag_balance) if finalize: self.finalize() def finalize(self): """Finalize the :class:`~discretize.TreeMesh`. Called once a tree mesh has been finalized; i.e. no further mesh refinement will be carried out. The tree mesh must be finalized before it can be used to call most of its properties or construct operators. When finalized, mesh refinement is no longer enabled. """ with self._tree_modify_lock: if not self._finalized: self.tree.finalize_lists() self.tree.number() self._finalized=True @property def finalized(self): """Whether tree mesh is finalized. This property returns a boolean stating whether the tree mesh has been finalized; i.e. no further mesh refinement will be carried out. A tree mesh must be finalized before it can be used to call most of its properties or construct operators. When finalized, mesh refinement is no longer enabled. Returns ------- bool Returns *True* if finalized, *False* otherwise """ with self._tree_modify_lock: val = self._finalized return val @property @cython.boundscheck(False) def cell_bounds(self): cell_bounds = np.empty((self.n_cells, self.dim, 2), dtype=np.float64) cdef np.float64_t[:, :, ::1] cell_bounds_view = cell_bounds for cell in self.tree.cells: min_loc = cell.min_node().location max_loc = cell.max_node().location for i in range(self._dim): cell_bounds_view[cell.index, i, 0] = min_loc[i] cell_bounds_view[cell.index, i, 1] = max_loc[i] return cell_bounds.reshape((self.n_cells, -1)) def number(self): """Number the cells, nodes, faces, and edges of the TreeMesh.""" with self._tree_modify_lock: self.tree.number() def get_containing_cells(self, points): """Return the cells containing the given points. Parameters ---------- points : (dim) or (n_point, dim) array_like The locations to query for the containing cells Returns ------- int or (n_point) numpy.ndarray of int The indexes of cells containing each point. """ cdef double[:,:] d_locs = self._require_ndarray_with_dim( 'locs', points, ndim=2, dtype=np.float64 ) cdef int_t n_locs = d_locs.shape[0] cdef np.int64_t[:] indexes = np.empty(n_locs, dtype=np.int64) cdef double x, y, z for i in range(n_locs): x = d_locs[i, 0] y = d_locs[i, 1] if self._dim == 3: z = d_locs[i, 2] else: z = 0 indexes[i] = self.tree.containing_cell(x, y, z).index if n_locs==1: return indexes[0] return np.array(indexes) def get_cells_in_ball(self, center, double radius): """Find the indices of cells that intersect a ball Parameters ---------- center : (dim) array_like center of the ball. radius : float radius of the ball Returns ------- numpy.ndarray of int The indices of cells which overlap the ball. """ cdef double[:] a = self._require_ndarray_with_dim('center', center, dtype=np.float64) cdef geom.Ball ball = geom.Ball(self._dim, &a[0], radius) return np.array(self.tree.find_cells_geom(ball)) def get_cells_on_line(self, segment): """Find the cells intersecting a line segment. Parameters ---------- segment : (2, dim) array-like Beginning and ending point of the line segment. Returns ------- numpy.ndarray of int Indices for cells that intersect the line defined by the two input points. """ segment = self._require_ndarray_with_dim('segment', segment, ndim=2, dtype=np.float64) if segment.shape[0] != 2: raise ValueError(f"A line segment has two points, not {segment.shape[0]}") cdef double[:] start = segment[0] cdef double[:] end = segment[1] cdef geom.Line line = geom.Line(self._dim, &start[0], &end[0]) return np.array(self.tree.find_cells_geom(line)) def get_cells_in_aabb(self, x_min, x_max): """Find the indices of cells that intersect an axis aligned bounding box (aabb) Parameters ---------- x_min : (dim, ) array_like Minimum extent of the box. x_max : (dim, ) array_like Maximum extent of the box. Returns ------- numpy.ndarray of int The indices of cells which overlap the axis aligned bounding box. """ cdef double[:] a = self._require_ndarray_with_dim('x_min', x_min, dtype=np.float64) cdef double[:] b = self._require_ndarray_with_dim('x_max', x_max, dtype=np.float64) cdef geom.Box box = geom.Box(self._dim, &a[0], &b[0]) return np.array(self.tree.find_cells_geom(box)) def get_cells_on_plane(self, origin, normal): """Find the indices of cells that intersect a plane. Parameters ---------- origin : (dim) array_like normal : (dim) array_like Returns ------- numpy.ndarray of int The indices of cells which intersect the plane. """ cdef double[:] orig = self._require_ndarray_with_dim('origin', origin, dtype=np.float64) cdef double[:] norm = self._require_ndarray_with_dim('normal', normal, dtype=np.float64) cdef geom.Plane plane = geom.Plane(self._dim, &orig[0], &norm[0]) return np.array(self.tree.find_cells_geom(plane)) def get_cells_in_triangle(self, triangle): """Find the indices of cells that intersect a triangle. Parameters ---------- triangle : (3, dim) array_like The three points of the triangle. Returns ------- numpy.ndarray of int The indices of cells which overlap the triangle. """ triangle = self._require_ndarray_with_dim('triangle', triangle, ndim=2, dtype=np.float64) if triangle.shape[0] != 3: raise ValueError(f"Triangle array must have three points, saw {triangle.shape[0]}") cdef double[:, :] tri = triangle cdef geom.Triangle poly = geom.Triangle(self._dim, &tri[0, 0], &tri[1, 0], &tri[2, 0]) return np.array(self.tree.find_cells_geom(poly)) def get_cells_in_vertical_trianglular_prism(self, triangle, double h): """Find the indices of cells that intersect a vertical triangular prism. Parameters ---------- triangle : (3, dim) array_like The three points of the triangle, assumes the top and bottom faces are parallel. h : float The height of the prism. Returns ------- numpy.ndarray of int The indices of cells which overlap the vertical triangular prism. """ if self.dim == 2: raise NotImplementedError("vertical_trianglular_prism only implemented in 3D.") triangle = self._require_ndarray_with_dim('triangle', triangle, ndim=2, dtype=np.float64) if triangle.shape[0] != 3: raise ValueError(f"Triangle array must have three points, saw {triangle.shape[0]}") cdef double[:, :] tri = triangle cdef geom.VerticalTriangularPrism vert = geom.VerticalTriangularPrism(self._dim, &tri[0, 0], &tri[1, 0], &tri[2, 0], h) return np.array(self.tree.find_cells_geom(vert)) def get_cells_in_tetrahedron(self, tetra): """Find the indices of cells that intersect a tetrahedron. Parameters ---------- tetra : (dim+1, dim) array_like The points of the tetrahedron(s). Returns ------- numpy.ndarray of int The indices of cells which overlap the triangle. """ if self.dim == 2: return self.get_cells_in_triangle(tetra) tetra = self._require_ndarray_with_dim('tetra', tetra, ndim=2, dtype=np.float64) if tetra.shape[0] != 4: raise ValueError(f"A tetrahedron is defined by 4 points in 3D, not {tetra.shape[0]}.") cdef double[:, :] tet = tetra cdef geom.Tetrahedron poly = geom.Tetrahedron(self._dim, &tet[0, 0], &tet[1, 0], &tet[2, 0], &tet[3, 0]) return np.array(self.tree.find_cells_geom(poly)) def _set_origin(self, origin): if not isinstance(origin, (list, tuple, np.ndarray)): raise ValueError('origin must be a list, tuple or numpy array') self._origin = np.asarray(origin, dtype=np.float64) cdef int_t dim = self._origin.shape[0] cdef double[:] shift #cdef c_Cell *cell cdef Node *node cdef Edge *edge cdef Face *face if self.tree.n_dim > 0: # Will only happen if __init__ has been called shift = np.empty(dim, dtype=np.float64) shift[0] = self._origin[0] - self._xs[0] shift[1] = self._origin[1] - self._ys[0] if dim == 3: shift[2] = self._origin[2] - self._zs[0] with self._tree_modify_lock: for i in range(self._xs.shape[0]): self._xs[i] += shift[0] for i in range(self._ys.shape[0]): self._ys[i] += shift[1] if dim == 3: for i in range(self._zs.shape[0]): self._zs[i] += shift[2] #update the locations of all of the items self.tree.shift_cell_centers(&shift[0]) for itN in self.tree.nodes: node = itN.second for i in range(dim): node.location[i] += shift[i] for itE in self.tree.edges_x: edge = itE.second for i in range(dim): edge.location[i] += shift[i] for itE in self.tree.edges_y: edge = itE.second for i in range(dim): edge.location[i] += shift[i] if dim == 3: for itE in self.tree.edges_z: edge = itE.second for i in range(dim): edge.location[i] += shift[i] for itF in self.tree.faces_x: face = itF.second for i in range(dim): face.location[i] += shift[i] for itF in self.tree.faces_y: face = itF.second for i in range(dim): face.location[i] += shift[i] for itF in self.tree.faces_z: face = itF.second for i in range(dim): face.location[i] += shift[i] #clear out all cached grids self._cell_centers = None self._nodes = None self._hanging_nodes = None self._edges_x = None self._hanging_edges_x = None self._edges_y = None self._hanging_edges_y = None self._edges_z = None self._hanging_edges_z = None self._faces_x = None self._hanging_faces_x = None self._faces_y = None self._hanging_faces_y = None self._faces_z = None self._hanging_faces_z = None @property def fill(self): """How 'filled' the tree mesh is compared to the underlying tensor mesh. This property outputs the ratio between the number of cells in the tree mesh and the number of cells in the underlying tensor mesh; where the underlying tensor mesh is equivalent to the uniform tensor mesh that uses the smallest cell size. Thus the output is a number between 0 and 1. Returns ------- float A fractional percent denoting how 'filled' the tree mesh is """ #Tensor mesh cells: cdef int_t nxc, nyc, nzc; nxc = (self._xs.shape[0]-1)//2 nyc = (self._ys.shape[0]-1)//2 nzc = (self._zs.shape[0]-1)//2 if self._dim==3 else 1 return float(self.n_cells)/(nxc * nyc * nzc) @property def max_used_level(self): """Maximum refinement level used. Returns the maximum refinement level used to construct the tree mesh. The maximum used level is equal or less than the maximum allowable level; see :py:attr:`.~TreeMesh.max_level`. Returns ------- int Maximum level used when refining the mesh """ cdef int level = 0 for cell in self.tree.cells: level = max(level, cell.level) return level @property def max_level(self): r"""Maximum allowable refinement level for the mesh. The maximum refinement level for a tree mesh depends on the number of underlying tensor mesh cells along each axis direction; which are always powers of 2. Where *N* is the number of underlying tensor mesh cells along a given axis, then the maximum allowable level of refinement :math:`k_{max}` is given by: .. math:: k_{max} = \log_2(N) Returns ------- int Maximum allowable refinement level for the mesh """ return self.tree.max_level @property def n_cells(self): """Total number of cells in the mesh. Returns ------- int Number of cells in the mesh Notes ----- Property also accessible as using the shorthand **nC** """ return self.tree.cells.size() @property def n_nodes(self): """Total number of nodes in the mesh. Returns ------- int Number of nodes in the mesh Notes ----- Property also accessible as using the shorthand **nN** """ return self.n_total_nodes - self.n_hanging_nodes @property def n_total_nodes(self): """Number of hanging and non-hanging nodes. Returns ------- int Number of hanging and non-hanging nodes """ return self.tree.nodes.size() @property def n_hanging_nodes(self): """Number of hanging nodes. Returns ------- int Number of hanging nodes """ return self.tree.hanging_nodes.size() @property def n_edges(self): """Total number of edges in the mesh. Returns ------- int Total number of edges in the mesh Notes ----- Property also accessible as using the shorthand **nE** """ return self.n_edges_x + self.n_edges_y + self.n_edges_z @property def n_hanging_edges(self): """Total number of hanging edges in all dimensions. Returns ------- int Number of hanging edges in all dimensions """ return self.n_hanging_edges_x + self.n_hanging_edges_y + self.n_hanging_edges_z @property def n_total_edges(self): """Total number of hanging and non-hanging edges in all dimensions. Returns ------- int Number of hanging and non-hanging edges in all dimensions """ return self.n_edges + self.n_hanging_edges @property def n_edges_x(self): """Number of x-edges in the mesh. This property returns the number of edges that are parallel to the x-axis; i.e. x-edges. Returns ------- int Number of x-edges in the mesh Notes ----- Property also accessible as using the shorthand **nEx** """ return self.n_total_edges_x - self.n_hanging_edges_x @property def n_edges_y(self): """Number of y-edges in the mesh. This property returns the number of edges that are parallel to the y-axis; i.e. y-edges. Returns ------- int Number of y-edges in the mesh Notes ----- Property also accessible as using the shorthand **nEy** """ return self.n_total_edges_y - self.n_hanging_edges_y @property def n_edges_z(self): """Number of z-edges in the mesh. This property returns the number of edges that are parallel to the z-axis; i.e. z-edges. Returns ------- int Number of z-edges in the mesh Notes ----- Property also accessible as using the shorthand **nEz** """ return self.n_total_edges_z - self.n_hanging_edges_z @property def n_total_edges_x(self): """Number of hanging and non-hanging x-edges in the mesh. Returns ------- int Number of hanging and non-hanging x-edges in the mesh """ return self.tree.edges_x.size() @property def n_total_edges_y(self): """Number of hanging and non-hanging y-edges in the mesh. Returns ------- int Number of hanging and non-hanging y-edges in the mesh """ return self.tree.edges_y.size() @property def n_total_edges_z(self): """Number of hanging and non-hanging z-edges in the mesh. Returns ------- int Number of hanging and non-hanging z-edges in the mesh """ return self.tree.edges_z.size() @property def n_hanging_edges_x(self): """Number of hanging x-edges in the mesh. Returns ------- int Number of hanging x-edges in the mesh """ return self.tree.hanging_edges_x.size() @property def n_hanging_edges_y(self): """Number of hanging y-edges in the mesh. Returns ------- int Number of hanging y-edges in the mesh """ return self.tree.hanging_edges_y.size() @property def n_hanging_edges_z(self): """Number of hanging z-edges in the mesh. Returns ------- int Number of hanging z-edges in the mesh """ return self.tree.hanging_edges_z.size() @property def n_faces(self): """Total number of faces in the mesh. Returns ------- int Total number of faces in the mesh Notes ----- Property also accessible as using the shorthand **nF** """ return self.n_faces_x + self.n_faces_y + self.n_faces_z @property def n_hanging_faces(self): """Total number of hanging faces in the mesh. Returns ------- int Total number of non-hanging faces in the mesh """ return self.n_hanging_faces_x + self.n_hanging_faces_y + self.n_hanging_faces_z @property def n_total_faces(self): """Total number of hanging and non-hanging faces in the mesh. Returns ------- int Total number of non-hanging faces in the mesh """ return self.n_faces + self.n_hanging_faces @property def n_faces_x(self): """Number of x-faces in the mesh. This property returns the number of faces whose normal vector is parallel to the x-axis; i.e. x-faces. Returns ------- int Number of x-faces in the mesh Notes ----- Property also accessible as using the shorthand **nFx** """ return self.n_total_faces_x - self.n_hanging_faces_x @property def n_faces_y(self): """Number of y-faces in the mesh. This property returns the number of faces whose normal vector is parallel to the y-axis; i.e. y-faces. Returns ------- int Number of y-faces in the mesh Notes ----- Property also accessible as using the shorthand **nFy** """ return self.n_total_faces_y - self.n_hanging_faces_y @property def n_faces_z(self): """Number of z-faces in the mesh. This property returns the number of faces whose normal vector is parallel to the z-axis; i.e. z-faces. Returns ------- int Number of z-faces in the mesh Notes ----- Property also accessible as using the shorthand **nFz** """ return self.n_total_faces_z - self.n_hanging_faces_z @property def n_total_faces_x(self): """Number of hanging and non-hanging x-faces in the mesh. Returns ------- int Number of hanging and non-hanging x-faces in the mesh """ if(self._dim == 2): return self.n_total_edges_y return self.tree.faces_x.size() @property def n_total_faces_y(self): """Number of hanging and non-hanging y-faces in the mesh. Returns ------- int Number of hanging and non-hanging y-faces in the mesh """ if(self._dim == 2): return self.n_total_edges_x return self.tree.faces_y.size() @property def n_total_faces_z(self): """Number of hanging and non-hanging z-faces in the mesh. Returns ------- int Number of hanging and non-hanging z-faces in the mesh """ if(self._dim == 2): return 0 return self.tree.faces_z.size() @property def n_hanging_faces_x(self): """Number of hanging x-faces in the mesh. Returns ------- int Number of hanging x-faces in the mesh """ if(self._dim == 2): return self.n_hanging_edges_y return self.tree.hanging_faces_x.size() @property def n_hanging_faces_y(self): """Number of hanging y-faces in the mesh. Returns ------- int Number of hanging y-faces in the mesh """ if(self._dim == 2): return self.n_hanging_edges_x return self.tree.hanging_faces_y.size() @property def n_hanging_faces_z(self): """Number of hanging z-faces in the mesh. Returns ------- int Number of hanging z-faces in the mesh """ if(self._dim == 2): return 0 return self.tree.hanging_faces_z.size() @property def cell_centers(self): """Gridded cell center locations. This property returns a numpy array of shape (n_cells, dim) containing gridded cell center locations for all cells in the mesh. Returns ------- (n_cells, dim) numpy.ndarray of float Gridded cell center locations """ self._error_if_not_finalized("cell_centers") cdef np.float64_t[:, :] gridCC cdef np.int64_t ii, ind, dim if self._cell_centers is None: dim = self._dim self._cell_centers = np.empty((self.n_cells, self._dim), dtype=np.float64) gridCC = self._cell_centers for cell in self.tree.cells: ind = cell.index for ii in range(dim): gridCC[ind, ii] = cell.location[ii] return self._cell_centers @property def nodes(self): """Gridded non-hanging nodes locations. This property returns a numpy array of shape (n_nodes, dim) containing gridded locations for all non-hanging nodes in the mesh. Returns ------- (n_nodes, dim) numpy.ndarray of float Gridded non-hanging node locations """ self._error_if_not_finalized("nodes") cdef np.float64_t[:, :] gridN cdef Node *node cdef np.int64_t ii, ind, dim if self._nodes is None: dim = self._dim self._nodes = np.empty((self.n_nodes, dim) ,dtype=np.float64) gridN = self._nodes for it in self.tree.nodes: node = it.second if not node.hanging: ind = node.index for ii in range(dim): gridN[ind, ii] = node.location[ii] return self._nodes @property def hanging_nodes(self): """Gridded hanging node locations. This property returns a numpy array of shape (n_hanging_nodes, dim) containing gridded locations for all hanging nodes in the mesh. Returns ------- (n_hanging_nodes, dim) numpy.ndarray of float Gridded hanging node locations """ self._error_if_not_finalized("hanging_nodes") cdef np.float64_t[:, :] gridN cdef Node *node cdef np.int64_t ii, ind, dim if self._hanging_nodes is None: dim = self._dim self._hanging_nodes = np.empty((self.n_hanging_nodes, dim), dtype=np.float64) gridhN = self._hanging_nodes for node in self.tree.hanging_nodes: ind = node.index-self.n_nodes for ii in range(dim): gridhN[ind, ii] = node.location[ii] return self._hanging_nodes @property def boundary_nodes(self): """Gridded boundary node locations. This property returns a numpy array of shape (n_boundary_nodes, dim) containing the gridded locations of the nodes on the boundary of the mesh. Returns ------- (n_boundary_nodes, dim) numpy.ndarray of float Gridded boundary node locations """ self._error_if_not_finalized("boundary_nodes") nodes = self.nodes x0, xF = self._xs[0], self._xs[-1] y0, yF = self._ys[0], self._ys[-1] is_boundary = ( (nodes[:, 0] == x0) | (nodes[:, 0] == xF) | (nodes[:, 1] == y0) | (nodes[:, 1] == yF) ) if self.dim > 2: z0, zF = self._zs[0], self._zs[-1] is_boundary |= ( (nodes[:, 2] == z0) | (nodes[:, 2] == zF) ) return nodes[is_boundary] @property def h_gridded(self): """Gridded cell dimensions. This property returns a numpy array of shape (n_cells, dim) containing the dimensions of the cells along each axis direction in order. E.g. the columns of *h_gridded* for a 3D tree mesh would be ordered [hx,hy,hz]. Returns ------- (n_cells, dim) numpy.ndarray of float Gridded cell dimensions """ self._error_if_not_finalized("h_gridded") if self._h_gridded is not None: return self._h_gridded cdef np.float64_t[:, :] gridCH cdef np.int64_t ii, ind, dim cdef np.float64_t len cdef int epc = 4 if self._dim==3 else 2 dim = self._dim self._h_gridded = np.empty((self.n_cells, dim), dtype=np.float64) gridCH = self._h_gridded for cell in self.tree.cells: ind = cell.index for ii in range(dim): gridCH[ind, ii] = cell.edges[ii*epc].length return self._h_gridded @property def edges_x(self): """Gridded locations of non-hanging x-edges. This property returns a numpy array of shape (n_edges_x, dim) containing gridded locations for all non-hanging x-edges. Returns ------- (n_edges_x, dim) numpy.ndarray of float Gridded locations of all non-hanging x-edges """ self._error_if_not_finalized("edges_x") cdef np.float64_t[:, :] gridEx cdef Edge *edge cdef np.int64_t ii, ind, dim if self._edges_x is None: dim = self._dim self._edges_x = np.empty((self.n_edges_x, dim), dtype=np.float64) gridEx = self._edges_x for it in self.tree.edges_x: edge = it.second if not edge.hanging: ind = edge.index for ii in range(dim): gridEx[ind, ii] = edge.location[ii] return self._edges_x @property def hanging_edges_x(self): """Gridded locations of hanging x-edges. This property returns a numpy array of shape (n_hanging_edges_x, dim) containing gridded locations for all hanging x-edges. Returns ------- (n_hanging_edges_x, dim) numpy.ndarray of float Gridded locations of all hanging x-edges """ self._error_if_not_finalized("hanging_edges_x") cdef np.float64_t[:, :] gridhEx cdef Edge *edge cdef np.int64_t ii, ind, dim if self._hanging_edges_x is None: dim = self._dim self._hanging_edges_x = np.empty((self.n_hanging_edges_x, dim), dtype=np.float64) gridhEx = self._hanging_edges_x for edge in self.tree.hanging_edges_x: ind = edge.index-self.n_edges_x for ii in range(dim): gridhEx[ind, ii] = edge.location[ii] return self._hanging_edges_x @property def edges_y(self): """Gridded locations of non-hanging y-edges. This property returns a numpy array of shape (n_edges_y, dim) containing gridded locations for all non-hanging y-edges. Returns ------- (n_edges_y, dim) numpy.ndarray of float Gridded locations of all non-hanging y-edges """ self._error_if_not_finalized("edges_y") cdef np.float64_t[:, :] gridEy cdef Edge *edge cdef np.int64_t ii, ind, dim if self._edges_y is None: dim = self._dim self._edges_y = np.empty((self.n_edges_y, dim), dtype=np.float64) gridEy = self._edges_y for it in self.tree.edges_y: edge = it.second if not edge.hanging: ind = edge.index for ii in range(dim): gridEy[ind, ii] = edge.location[ii] return self._edges_y @property def hanging_edges_y(self): """Gridded locations of hanging y-edges. This property returns a numpy array of shape (n_haning_edges_y, dim) containing gridded locations for all hanging y-edges. Returns ------- (n_haning_edges_y, dim) numpy.ndarray of float Gridded locations of all hanging y-edges """ self._error_if_not_finalized("hanging_edges_y") cdef np.float64_t[:, :] gridhEy cdef Edge *edge cdef np.int64_t ii, ind, dim if self._hanging_edges_y is None: dim = self._dim self._hanging_edges_y = np.empty((self.n_hanging_edges_y, dim), dtype=np.float64) gridhEy = self._hanging_edges_y for edge in self.tree.hanging_edges_y: ind = edge.index-self.n_edges_y for ii in range(dim): gridhEy[ind, ii] = edge.location[ii] return self._hanging_edges_y @property def edges_z(self): """Gridded locations of non-hanging z-edges. This property returns a numpy array of shape (n_edges_z, dim) containing gridded locations for all non-hanging z-edges. Returns ------- (n_edges_z, dim) numpy.ndarray of float Gridded locations of all non-hanging z-edges """ self._error_if_not_finalized("edges_z") cdef np.float64_t[:, :] gridEz cdef Edge *edge cdef np.int64_t ii, ind, dim if self._edges_z is None: dim = self._dim self._edges_z = np.empty((self.n_edges_z, dim), dtype=np.float64) gridEz = self._edges_z for it in self.tree.edges_z: edge = it.second if not edge.hanging: ind = edge.index for ii in range(dim): gridEz[ind, ii] = edge.location[ii] return self._edges_z @property def hanging_edges_z(self): """Gridded locations of hanging z-edges. This property returns a numpy array of shape (n_hanging_edges_z, dim) containing gridded locations for all hanging z-edges. Returns ------- (n_hanging_edges_z, dim) numpy.ndarray of float Gridded locations of all hanging z-edges """ self._error_if_not_finalized("hanging_edges_z") cdef np.float64_t[:, :] gridhEz cdef Edge *edge cdef np.int64_t ii, ind, dim if self._hanging_edges_z is None: dim = self._dim self._hanging_edges_z = np.empty((self.n_hanging_edges_z, dim), dtype=np.float64) gridhEz = self._hanging_edges_z for edge in self.tree.hanging_edges_z: ind = edge.index-self.n_edges_z for ii in range(dim): gridhEz[ind, ii] = edge.location[ii] return self._hanging_edges_z @property def boundary_edges(self): """Gridded boundary edge locations. This property returns a numpy array of shape (n_boundary_edges, dim) containing the gridded locations of the edges on the boundary of the mesh. The returned quantity is organized *np.r_[edges_x, edges_y, edges_z]* . Returns ------- (n_boundary_edges, dim) numpy.ndarray of float Gridded boundary edge locations """ self._error_if_not_finalized("boundary_edges") edges_x = self.edges_x edges_y = self.edges_y x0, xF = self._xs[0], self._xs[-1] y0, yF = self._ys[0], self._ys[-1] is_boundary_x = (edges_x[:, 1] == y0) | (edges_x[:, 1] == yF) is_boundary_y = (edges_y[:, 0] == x0) | (edges_y[:, 0] == xF) if self.dim > 2: z0, zF = self._zs[0], self._zs[-1] edges_z = self.edges_z is_boundary_x |= (edges_x[:, 2] == z0) | (edges_x[:, 2] == zF) is_boundary_y |= (edges_y[:, 2] == z0) | (edges_y[:, 2] == zF) is_boundary_z = ( (edges_z[:, 0] == x0) | (edges_z[:, 0] == xF) | (edges_z[:, 1] == y0) | (edges_z[:, 1] == yF) ) return np.r_[ edges_x[is_boundary_x], edges_y[is_boundary_y], edges_z[is_boundary_z] ] else: return np.r_[edges_x[is_boundary_x], edges_y[is_boundary_y]] @property def faces_x(self): """Gridded locations of non-hanging x-faces. This property returns a numpy array of shape (n_faces_x, dim) containing gridded locations for all non-hanging x-faces. Returns ------- (n_faces_x, dim) numpy.ndarray of float Gridded locations of all non-hanging x-faces """ self._error_if_not_finalized("faces_x") if(self._dim == 2): return self.edges_y cdef np.float64_t[:, :] gridFx cdef Face *face cdef np.int64_t ii, ind, dim if self._faces_x is None: dim = self._dim self._faces_x = np.empty((self.n_faces_x, dim), dtype=np.float64) gridFx = self._faces_x for it in self.tree.faces_x: face = it.second if not face.hanging: ind = face.index for ii in range(dim): gridFx[ind, ii] = face.location[ii] return self._faces_x @property def faces_y(self): """Gridded locations of non-hanging y-faces. This property returns a numpy array of shape (n_faces_y, dim) containing gridded locations for all non-hanging y-faces. Returns ------- (n_faces_y, dim) numpy.ndarray of float Gridded locations of all non-hanging y-faces """ self._error_if_not_finalized("faces_y") if(self._dim == 2): return self.edges_x cdef np.float64_t[:, :] gridFy cdef Face *face cdef np.int64_t ii, ind, dim if self._faces_y is None: dim = self._dim self._faces_y = np.empty((self.n_faces_y, dim), dtype=np.float64) gridFy = self._faces_y for it in self.tree.faces_y: face = it.second if not face.hanging: ind = face.index for ii in range(dim): gridFy[ind, ii] = face.location[ii] return self._faces_y @property def faces_z(self): """Gridded locations of non-hanging z-faces. This property returns a numpy array of shape (n_faces_z, dim) containing gridded locations for all non-hanging z-faces. Returns ------- (n_faces_z, dim) numpy.ndarray of float Gridded locations of all non-hanging z-faces """ self._error_if_not_finalized("faces_z") if(self._dim == 2): return self.cell_centers cdef np.float64_t[:, :] gridFz cdef Face *face cdef np.int64_t ii, ind, dim if self._faces_z is None: dim = self._dim self._faces_z = np.empty((self.n_faces_z, dim), dtype=np.float64) gridFz = self._faces_z for it in self.tree.faces_z: face = it.second if not face.hanging: ind = face.index for ii in range(dim): gridFz[ind, ii] = face.location[ii] return self._faces_z @property def hanging_faces_x(self): """Gridded locations of hanging x-faces. This property returns a numpy array of shape (n_hanging_faces_x, dim) containing gridded locations for all hanging x-faces. Returns ------- (n_hanging_faces_x, dim) numpy.ndarray of float Gridded locations of all hanging x-faces """ self._error_if_not_finalized("hanging_faces_x") if(self._dim == 2): return self.hanging_edges_y cdef np.float64_t[:, :] gridFx cdef Face *face cdef np.int64_t ii, ind, dim if self._hanging_faces_x is None: dim = self._dim self._hanging_faces_x = np.empty((self.n_hanging_faces_x, dim), dtype=np.float64) gridhFx = self._hanging_faces_x for face in self.tree.hanging_faces_x: ind = face.index-self.n_faces_x for ii in range(dim): gridhFx[ind, ii] = face.location[ii] return self._hanging_faces_x @property def hanging_faces_y(self): """Gridded locations of hanging y-faces. This property returns a numpy array of shape (n_hanging_faces_y, dim) containing gridded locations for all hanging y-faces. Returns ------- (n_hanging_faces_y, dim) numpy.ndarray of float Gridded locations of all hanging y-faces """ self._error_if_not_finalized("hanging_faces_y") if(self._dim == 2): return self.hanging_edges_x cdef np.float64_t[:, :] gridhFy cdef Face *face cdef np.int64_t ii, ind, dim if self._hanging_faces_y is None: dim = self._dim self._hanging_faces_y = np.empty((self.n_hanging_faces_y, dim), dtype=np.float64) gridhFy = self._hanging_faces_y for face in self.tree.hanging_faces_y: ind = face.index-self.n_faces_y for ii in range(dim): gridhFy[ind, ii] = face.location[ii] return self._hanging_faces_y @property def hanging_faces_z(self): """Gridded locations of hanging z-faces. This property returns a numpy array of shape (n_hanging_faces_z, dim) containing gridded locations for all hanging z-faces. Returns ------- (n_hanging_faces_z, dim) numpy.ndarray of float Gridded locations of all hanging z-faces """ self._error_if_not_finalized("hanging_faces_z") if(self._dim == 2): return np.array([]) cdef np.float64_t[:, :] gridhFz cdef Face *face cdef np.int64_t ii, ind, dim if self._hanging_faces_z is None: dim = self._dim self._hanging_faces_z = np.empty((self.n_hanging_faces_z, dim), dtype=np.float64) gridhFz = self._hanging_faces_z for face in self.tree.hanging_faces_z: ind = face.index-self.n_faces_z for ii in range(dim): gridhFz[ind, ii] = face.location[ii] return self._hanging_faces_z @property def boundary_faces(self): """Gridded boundary face locations. This property returns a numpy array of shape (n_boundary_faces, dim) containing the gridded locations of the faces on the boundary of the mesh. The returned quantity is organized *np.r_[faces_x, faces_y, faces_z]* . Returns ------- (n_boundary_faces, dim) numpy.ndarray of float Gridded boundary face locations """ self._error_if_not_finalized("boundary_faces") faces_x = self.faces_x faces_y = self.faces_y x0, xF = self._xs[0], self._xs[-1] y0, yF = self._ys[0], self._ys[-1] is_boundary_x = (faces_x[:, 0] == x0) | (faces_x[:, 0] == xF) is_boundary_y = (faces_y[:, 1] == y0) | (faces_y[:, 1] == yF) boundary_faces = np.r_[faces_x[is_boundary_x], faces_y[is_boundary_y]] if self.dim > 2: z0, zF = self._zs[0], self._zs[-1] faces_z = self.faces_z is_boundary_z = (faces_z[:, 2] == z0) | (faces_z[:, 2] == zF) boundary_faces = np.r_[boundary_faces, faces_z[is_boundary_z]] return boundary_faces @property def boundary_face_outward_normals(self): """Outward normals of boundary faces. For all boundary faces in the mesh, this property returns the unit vectors denoting the outward normals to the boundary. Returns ------- (n_boundary_faces, dim) numpy.ndarray of float Outward normals of boundary faces """ self._error_if_not_finalized("boundary_face_outward_normals") faces_x = self.faces_x faces_y = self.faces_y x0, xF = self._xs[0], self._xs[-1] y0, yF = self._ys[0], self._ys[-1] is_bxm = faces_x[:, 0] == x0 is_boundary_x = is_bxm | (faces_x[:, 0] == xF) is_bym = faces_y[:, 1] == y0 is_boundary_y = is_bym | (faces_y[:, 1] == yF) is_boundary = np.r_[is_boundary_x, is_boundary_y] switch = np.r_[is_bxm, is_bym] if self.dim > 2: z0, zF = self._zs[0], self._zs[-1] faces_z = self.faces_z is_bzm = faces_z[:, 2] == z0 is_boundary_z = is_bzm | (faces_z[:, 2] == zF) is_boundary = np.r_[is_boundary, is_boundary_z] switch = np.r_[switch, is_bzm] face_normals = self.face_normals.copy() face_normals[switch] *= -1 return face_normals[is_boundary] @property def cell_volumes(self): """Return cell volumes. Calling this property will compute and return a 1D array containing the volumes of mesh cells. Returns ------- (n_cells) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *2D:* Returns the cell areas - *3D:* Returns the cell volumes """ self._error_if_not_finalized("cell_volumes") cdef np.float64_t[:] vol if self._cell_volumes is None: self._cell_volumes = np.empty(self.n_cells, dtype=np.float64) vol = self._cell_volumes for cell in self.tree.cells: vol[cell.index] = cell.volume return self._cell_volumes @property def face_areas(self): """Returns the areas of all cell faces. Calling this property will compute and return the areas of all mesh faces as a 1D numpy array. Returns ------- (n_faces) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *2D:* returns the x-face and y-face areas; i.e. y-edge and x-edge lengths, respectively - *3D:* returns the x, y and z-face areas in order """ self._error_if_not_finalized("face_areas") if self._dim == 2 and self._face_areas is None: self._face_areas = np.r_[self.edge_lengths[self.n_edges_x:], self.edge_lengths[:self.n_edges_x]] cdef np.float64_t[:] area cdef int_t ind, offset = 0 cdef Face *face if self._face_areas is None: self._face_areas = np.empty(self.n_faces, dtype=np.float64) area = self._face_areas for it in self.tree.faces_x: face = it.second if face.hanging: continue area[face.index] = face.area offset = self.n_faces_x for it in self.tree.faces_y: face = it.second if face.hanging: continue area[face.index + offset] = face.area offset = self.n_faces_x + self.n_faces_y for it in self.tree.faces_z: face = it.second if face.hanging: continue area[face.index + offset] = face.area return self._face_areas @property def edge_lengths(self): """Returns the lengths of all edges in the mesh. Calling this property will compute and return the lengths of all edges in the mesh. Returns ------- (n_edges) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *2D:* returns the x-edge and y-edge lengths in order - *3D:* returns the x, y and z-edge lengths in order """ self._error_if_not_finalized("edge_lengths") cdef np.float64_t[:] edge_l cdef Edge *edge cdef int_t ind, offset if self._edge_lengths is None: self._edge_lengths = np.empty(self.n_edges, dtype=np.float64) edge_l = self._edge_lengths for it in self.tree.edges_x: edge = it.second if edge.hanging: continue edge_l[edge.index] = edge.length offset = self.n_edges_x for it in self.tree.edges_y: edge = it.second if edge.hanging: continue edge_l[edge.index + offset] = edge.length if self._dim > 2: offset = self.n_edges_x + self.n_edges_y for it in self.tree.edges_z: edge = it.second if edge.hanging: continue edge_l[edge.index + offset] = edge.length return self._edge_lengths @property def cell_boundary_indices(self): """Returns the indices of the x, y (and z) boundary cells. This property returns the indices of the cells on the x, y (and z) boundaries, respectively. Note that each axis direction will have both a lower and upper boundary. The property will return the indices corresponding to the lower and upper boundaries separately. E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total). In this case, the return is a list of length 4 organized [ind_Bx1, ind_Bx2, ind_By1, ind_By2]:: By2 + ------------- + | | | | Bx1 | | Bx2 | | | | + ------------- + By1 Returns ------- ind_bx1 ind_bx2, ind_by1, ind_by2 : numpy.ndarray of int The length of each array in the list is equal to the number of faces on that particular boundary. ind_bz1, ind_bz2 : numpy.ndarray of int, optional Returned if `dim` is 3. Examples -------- Here, we construct a small 2D tree mesh and return the indices of the x and y-boundary cells. >>> from discretize import TreeMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> hx = np.ones(16) >>> hy = np.ones(16) >>> mesh = TreeMesh([hx, hy]) >>> mesh.refine_ball([4.0,4.0], [4.0], [4]) >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.cell_boundary_indices >>> ax = plt.subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(*mesh.cell_centers[ind_Bx1].T) >>> plt.show() """ cdef np.int64_t[:] indxu, indxd, indyu, indyd, indzu, indzd indxu = np.empty(self.n_cells, dtype=np.int64) indxd = np.empty(self.n_cells, dtype=np.int64) indyu = np.empty(self.n_cells, dtype=np.int64) indyd = np.empty(self.n_cells, dtype=np.int64) if self._dim == 3: indzu = np.empty(self.n_cells, dtype=np.int64) indzd = np.empty(self.n_cells, dtype=np.int64) cdef int_t nxu, nxd, nyu, nyd, nzu, nzd nxu = 0 nxd = 0 nyu = 0 nyd = 0 nzu = 0 nzd = 0 for cell in self.tree.cells: if cell.neighbors[0] == NULL: indxd[nxd] = cell.index nxd += 1 if cell.neighbors[1] == NULL: indxu[nxu] = cell.index nxu += 1 if cell.neighbors[2] == NULL: indyd[nyd] = cell.index nyd += 1 if cell.neighbors[3] == NULL: indyu[nyu] = cell.index nyu += 1 if self._dim == 3: if cell.neighbors[4] == NULL: indzd[nzd] = cell.index nzd += 1 if cell.neighbors[5] == NULL: indzu[nzu] = cell.index nzu += 1 ixd = np.array(indxd)[:nxd] ixu = np.array(indxu)[:nxu] iyd = np.array(indyd)[:nyd] iyu = np.array(indyu)[:nyu] if self._dim == 3: izd = np.array(indzd)[:nzd] izu = np.array(indzu)[:nzu] return ixd, ixu, iyd, iyu, izd, izu else: return ixd, ixu, iyd, iyu @property def face_boundary_indices(self): """Returns the indices of the x, y (and z) boundary faces. This property returns the indices of the faces on the x, y (and z) boundaries, respectively. Note that each axis direction will have both a lower and upper boundary. The property will return the indices corresponding to the lower and upper boundaries separately. E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total). In this case, the return is a list of length 4 organized [ind_Bx1, ind_Bx2, ind_By1, ind_By2]:: By2 + ------------- + | | | | Bx1 | | Bx2 | | | | + ------------- + By1 Returns ------- ind_bx1 ind_bx2, ind_by1, ind_by2 : numpy.ndarray of int The length of each array in the list is equal to the number of faces on that particular boundary. ind_bz1, ind_bz2 : numpy.ndarray of int, optional Examples -------- Here, we construct a small 2D tree mesh and return the indices of the x and y-boundary faces. >>> from discretize import TreeMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> hx = np.ones(16) >>> hy = np.ones(16) >>> mesh = TreeMesh([hx, hy]) >>> mesh.refine_ball([4.0,4.0], [4.0], [4]) >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.face_boundary_indices >>> ax = plt.subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(*mesh.faces_x[ind_Bx1].T) >>> plt.show() """ cell_boundary_inds = self.cell_boundary_indices cdef np.int64_t[:] c_indxu, c_indxd, c_indyu, c_indyd, c_indzu, c_indzd cdef np.int64_t[:] f_indxu, f_indxd, f_indyu, f_indyd, f_indzu, f_indzd if self._dim == 2: c_indxd, c_indxu, c_indyd, c_indyu = cell_boundary_inds else: c_indxd, c_indxu, c_indyd, c_indyu, c_indzd, c_indzu = cell_boundary_inds f_indxd = np.empty(c_indxd.shape[0], dtype=np.int64) f_indxu = np.empty(c_indxu.shape[0], dtype=np.int64) f_indyd = np.empty(c_indyd.shape[0], dtype=np.int64) f_indyu = np.empty(c_indyu.shape[0], dtype=np.int64) if self._dim == 2: for i in range(f_indxd.shape[0]): f_indxd[i] = self.tree.cells[c_indxd[i]].edges[2].index for i in range(f_indxu.shape[0]): f_indxu[i] = self.tree.cells[c_indxu[i]].edges[3].index for i in range(f_indyd.shape[0]): f_indyd[i] = self.tree.cells[c_indyd[i]].edges[0].index for i in range(f_indyu.shape[0]): f_indyu[i] = self.tree.cells[c_indyu[i]].edges[1].index if self._dim == 3: f_indzd = np.empty(c_indzd.shape[0], dtype=np.int64) f_indzu = np.empty(c_indzu.shape[0], dtype=np.int64) for i in range(f_indxd.shape[0]): f_indxd[i] = self.tree.cells[c_indxd[i]].faces[0].index for i in range(f_indxu.shape[0]): f_indxu[i] = self.tree.cells[c_indxu[i]].faces[1].index for i in range(f_indyd.shape[0]): f_indyd[i] = self.tree.cells[c_indyd[i]].faces[2].index for i in range(f_indyu.shape[0]): f_indyu[i] = self.tree.cells[c_indyu[i]].faces[3].index for i in range(f_indzd.shape[0]): f_indzd[i] = self.tree.cells[c_indzd[i]].faces[4].index for i in range(f_indzu.shape[0]): f_indzu[i] = self.tree.cells[c_indzu[i]].faces[5].index ixd = np.array(f_indxd) ixu = np.array(f_indxu) iyd = np.array(f_indyd) iyu = np.array(f_indyu) if self._dim == 3: izd = np.array(f_indzd) izu = np.array(f_indzu) return ixd, ixu, iyd, iyu, izd, izu else: return ixd, ixu, iyd, iyu def get_boundary_cells(self, active_ind=None, direction='zu'): """Return the indices of boundary cells in a given direction given an active index array. Parameters ---------- active_ind : array_like of bool, optional If not None, then this must show which cells are active direction: {'zu', 'zd', 'xu', 'xd', 'yu', 'yd'} The requested direction to return Returns ------- numpy.ndarray of int Array of indices for the boundary cells in the requested direction """ direction = direction.lower() if direction[0] == 'z' and self._dim == 2: dir_str = 'y'+direction[1] else: dir_str = direction cdef int_t dir_ind = {'xd':0, 'xu':1, 'yd':2, 'yu':3, 'zd':4, 'zu':5}[dir_str] if active_ind is None: return self.cell_boundary_indices[dir_ind] active_ind = np.require(active_ind, dtype=np.int8, requirements='C') cdef np.int8_t[:] act = active_ind cdef np.int8_t[:] is_on_boundary = np.zeros(self.n_cells, dtype=np.int8) cdef c_Cell *cell cdef c_Cell *neighbor for cell in self.tree.cells: if not act[cell.index]: continue is_bound = 0 neighbor = cell.neighbors[dir_ind] if neighbor is NULL: is_bound = 1 elif neighbor.is_leaf(): is_bound = not act[neighbor.index] else: if dir_ind == 1 or dir_ind == 3 or dir_ind == 5: is_bound = is_bound or (not act[neighbor.children[0].index]) if dir_ind == 0 or dir_ind == 3 or dir_ind == 5: is_bound = is_bound or (not act[neighbor.children[1].index]) if dir_ind == 1 or dir_ind == 2 or dir_ind == 5: is_bound = is_bound or (not act[neighbor.children[2].index]) if dir_ind == 0 or dir_ind == 2 or dir_ind == 5: is_bound = is_bound or (not act[neighbor.children[3].index]) if self._dim == 3: if dir_ind == 1 or dir_ind == 3 or dir_ind == 4: is_bound = is_bound or (not act[neighbor.children[4].index]) if dir_ind == 0 or dir_ind == 3 or dir_ind == 4: is_bound = is_bound or (not act[neighbor.children[5].index]) if dir_ind == 1 or dir_ind == 2 or dir_ind == 4: is_bound = is_bound or (not act[neighbor.children[6].index]) if dir_ind == 0 or dir_ind == 2 or dir_ind == 4: is_bound = is_bound or (not act[neighbor.children[7].index]) is_on_boundary[cell.index] = is_bound return np.where(is_on_boundary) @cython.cdivision(True) def get_cells_along_line(self, x0, x1): """Find the cells in order along a line segment. Parameters ---------- x0,x1 : (dim) array_like Begining and ending point of the line segment. Returns ------- list of int Indices for cells that contain the a line defined by the two input points, ordered in the direction of the line. """ cdef np.float64_t ax, ay, az, bx, by, bz cdef int dim = self.dim ax = x0[0] ay = x0[1] az = x0[2] if dim==3 else 0 bx = x1[0] by = x1[1] bz = x1[2] if dim==3 else 0 cdef vector[long long int] cell_indexes; #find initial cell cdef c_Cell *cur_cell = self.tree.containing_cell(ax, ay, az) cell_indexes.push_back(cur_cell.index) #find last cell cdef c_Cell *last_cell = self.tree.containing_cell(bx, by, bz) cdef c_Cell *next_cell cdef int ix, iy, iz cdef double tx, ty, tz, ipx, ipy, ipz if dim==3: last_point = 7 else: last_point = 3 cdef int iter = 0 while cur_cell.index != last_cell.index: #find which direction to look: p0 = cur_cell.points[0].location pF = cur_cell.points[last_point].location if ax>bx: tx = (p0[0]-ax)/(bx-ax) elif axby: ty = (p0[1]-ay)/(by-ay) elif aybz: tz = (p0[2]-az)/(bz-az) elif az= 1: # then the segment ended in the current cell. # do not bother checking anymore. break #intersection point ipx = (bx-ax)*t+ax ipy = (by-ay)*t+ay ipz = (bz-az)*t+az next_cell = cur_cell if t == tx: # step in x direction if ax>bx: # go -x next_cell = next_cell.neighbors[0] else: # go +x next_cell = next_cell.neighbors[1] if next_cell is NULL: break if t == ty: # step in y direction if ay>by: # go -y next_cell = next_cell.neighbors[2] else: # go +y next_cell = next_cell.neighbors[3] if next_cell is NULL: break if dim==3 and t == tz: # step in z direction if az>bz: # go -z next_cell = next_cell.neighbors[4] else: # go +z next_cell = next_cell.neighbors[5] if next_cell is NULL: break # check if next_cell is not a leaf # (if so need to traverse down the children and find the closest leaf cell) while not next_cell.is_leaf(): # should be able to use cp to check which cell to go to cp = next_cell.children[0].points[last_point].location # this basically finds the child cell closest to the intersection point ix = ipx>cp[0] or (ipx==cp[0] and axcp[1] or (ipy==cp[1] and aycp[2] or (ipz==cp[2] and az2): for it in self.tree.edges_z: edge = it.second if edge.hanging: continue ii = edge.index + offset2 I[ii*2 : ii*2 + 2] = ii J[ii*2 ] = edge.points[0].index J[ii*2 + 1] = edge.points[1].index length = edge.length V[ii*2 ] = -1.0/length V[ii*2 + 1] = 1.0/length Rn = self._deflate_nodes() G = sp.csr_matrix((V, (I, J)), shape=(self.n_edges, self.n_total_nodes)) self._nodal_gradient = G*Rn return self._nodal_gradient @property def nodal_laplacian(self): """Not implemented on the TreeMesh.""" raise NotImplementedError('Nodal Laplacian has not been implemented for TreeMesh') @cython.boundscheck(False) def average_cell_to_total_face_x(self): """Average matrix for cell center to total (including hanging) x faces. This property constructs an averaging operator that maps scalar quantities from cell centers to face. This averaging operator is used when a discrete scalar quantity defined cell centers must be projected to faces. Returns ------- (n_total_faces_x, n_cells) scipy.sparse.csr_matrix The scalar averaging operator from faces to cell centers """ cdef np.int64_t[:] I = np.zeros(2*self.n_total_faces_x, dtype=np.int64) cdef np.int64_t[:] J = np.zeros(2*self.n_total_faces_x, dtype=np.int64) cdef np.float64_t[:] V = np.zeros(2*self.n_total_faces_x, dtype=np.float64) cdef int dim = self._dim cdef int_t ind for cell in self.tree.cells : next_cell = cell.neighbors[1] if next_cell == NULL: continue if dim == 2: if next_cell.is_leaf(): ind = cell.edges[3].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 else: for i in range(2): # two neighbors in +x direction ind = next_cell.children[2*i].edges[2].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[2*i].index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 else: if cell.neighbors[1].is_leaf(): ind = cell.faces[1].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 else: for i in range(4): # four neighbors in +x direction ind = next_cell.children[2*i].faces[0].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[2*i].index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 return sp.csr_matrix((V, (I,J)), shape=(self.n_total_faces_x, self.n_cells)) @cython.boundscheck(False) def average_cell_to_total_face_y(self): """Average matrix for cell center to total (including hanging) y faces. This property constructs an averaging operator that maps scalar quantities from cell centers to face. This averaging operator is used when a discrete scalar quantity defined cell centers must be projected to faces. Returns ------- (n_total_faces_y, n_cells) scipy.sparse.csr_matrix The scalar averaging operator from faces to cell centers """ cdef np.int64_t[:] I = np.zeros(2*self.n_total_faces_y, dtype=np.int64) cdef np.int64_t[:] J = np.zeros(2*self.n_total_faces_y, dtype=np.int64) cdef np.float64_t[:] V = np.zeros(2*self.n_total_faces_y, dtype=np.float64) cdef int dim = self._dim cdef int_t ind for cell in self.tree.cells : next_cell = cell.neighbors[3] if next_cell==NULL: continue if dim==2: if next_cell.is_leaf(): ind = cell.edges[1].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 else: for i in range(2): # two neighbors in +y direction ind = next_cell.children[i].edges[0].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[i].index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 else: if next_cell.is_leaf(): ind = cell.faces[3].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 else: for i in range(4): # four neighbors in +y direction ind = next_cell.children[(i>>1)*4 + i%2].faces[2].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[(i>>1)*4 + i%2].index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 return sp.csr_matrix((V, (I,J)), shape=(self.n_total_faces_y, self.n_cells)) @cython.boundscheck(False) def average_cell_to_total_face_z(self): """Average matrix for cell center to total (including hanging) z faces. This property constructs an averaging operator that maps scalar quantities from cell centers to face. This averaging operator is used when a discrete scalar quantity defined cell centers must be projected to faces. Returns ------- (n_total_faces_z, n_cells) scipy.sparse.csr_matrix The scalar averaging operator from faces to cell centers """ cdef np.int64_t[:] I = np.zeros(2*self.n_total_faces_z, dtype=np.int64) cdef np.int64_t[:] J = np.zeros(2*self.n_total_faces_z, dtype=np.int64) cdef np.float64_t[:] V = np.zeros(2*self.n_total_faces_z, dtype=np.float64) cdef int_t ind for cell in self.tree.cells : next_cell = cell.neighbors[5] if next_cell==NULL: continue if next_cell.is_leaf(): ind = cell.faces[5].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 else: for i in range(4): # four neighbors in +z direction ind = next_cell.children[i].faces[4].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[i].index V[2*ind ] = 0.5 V[2*ind + 1] = 0.5 return sp.csr_matrix((V, (I,J)), shape=(self.n_total_faces_z, self.n_cells)) @property @cython.boundscheck(False) def stencil_cell_gradient_x(self): r"""Differencing operator along x-direction to total (including hanging) x faces. This property constructs a differencing operator along the x-axis that acts on cell centered quantities; i.e. the stencil for the x-component of the cell gradient. The operator computes the differences between the values at adjacent cell centers along the x-direction, and places the result on the x-faces. The operator is a sparse matrix :math:`\mathbf{G_x}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: diff_phi_x = Gx @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Returns ------- (n_total_faces_x, n_cells) scipy.sparse.csr_matrix The stencil for the x-component of the cell gradient """ self._error_if_not_finalized("stencil_cell_gradient_x") if getattr(self, '_stencil_cell_gradient_x', None) is not None: return self._stencil_cell_gradient_x cdef np.int64_t[:] I = np.zeros(2*self.n_total_faces_x, dtype=np.int64) cdef np.int64_t[:] J = np.zeros(2*self.n_total_faces_x, dtype=np.int64) cdef np.float64_t[:] V = np.zeros(2*self.n_total_faces_x, dtype=np.float64) cdef int dim = self._dim cdef int_t ind for cell in self.tree.cells : next_cell = cell.neighbors[1] if next_cell == NULL: continue if dim == 2: if next_cell.is_leaf(): ind = cell.edges[3].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 else: for i in range(2): # two neighbors in +x direction ind = next_cell.children[2*i].edges[2].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[2*i].index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 else: if cell.neighbors[1].is_leaf(): ind = cell.faces[1].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 else: for i in range(4): # four neighbors in +x direction ind = next_cell.children[2*i].faces[0].index #0 2 4 6 I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[2*i].index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 self._stencil_cell_gradient_x = ( sp.csr_matrix((V, (I,J)), shape=(self.n_total_faces_x, self.n_cells)) ) return self._stencil_cell_gradient_x @property @cython.boundscheck(False) def stencil_cell_gradient_y(self): r"""Differencing operator along y-direction to total (including hanging) y faces. This property constructs a differencing operator along the y-axis that acts on cell centered quantities; i.e. the stencil for the y-component of the cell gradient. The operator computes the differences between the values at adjacent cell centers along the y-direction, and places the result on the y-faces. The operator is a sparse matrix :math:`\mathbf{G_y}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: diff_phi_y = Gy @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Returns ------- (n_total_faces_y, n_cells) scipy.sparse.csr_matrix The stencil for the y-component of the cell gradient """ self._error_if_not_finalized("stencil_cell_gradient_y") if getattr(self, '_stencil_cell_gradient_y', None) is not None: return self._stencil_cell_gradient_y cdef np.int64_t[:] I = np.zeros(2*self.n_total_faces_y, dtype=np.int64) cdef np.int64_t[:] J = np.zeros(2*self.n_total_faces_y, dtype=np.int64) cdef np.float64_t[:] V = np.zeros(2*self.n_total_faces_y, dtype=np.float64) cdef int dim = self._dim cdef int_t ind for cell in self.tree.cells : next_cell = cell.neighbors[3] if next_cell == NULL: continue if dim==2: if next_cell.is_leaf(): ind = cell.edges[1].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 else: for i in range(2): # two neighbors in +y direction ind = next_cell.children[i].edges[0].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[i].index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 else: if next_cell.is_leaf(): ind = cell.faces[3].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 else: for i in range(4): # four neighbors in +y direction ind = next_cell.children[(i>>1)*4 + i%2].faces[2].index #0, 1, 4, 5 I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[(i>>1)*4 + i%2].index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 self._stencil_cell_gradient_y = ( sp.csr_matrix((V, (I,J)), shape=(self.n_total_faces_y, self.n_cells)) ) return self._stencil_cell_gradient_y @property @cython.boundscheck(False) def stencil_cell_gradient_z(self): r"""Differencing operator along z-direction to total (including hanging) z faces. This property constructs a differencing operator along the z-axis that acts on cell centered quantities; i.e. the stencil for the z-component of the cell gradient. The operator computes the differences between the values at adjacent cell centers along the z-direction, and places the result on the z-faces. The operator is a sparse matrix :math:`\mathbf{G_z}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: diff_phi_z = Gz @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Returns ------- (n_total_faces_z, n_cells) scipy.sparse.csr_matrix The stencil for the z-component of the cell gradient """ self._error_if_not_finalized("stencil_cell_gradient_z") if getattr(self, '_stencil_cell_gradient_z', None) is not None: return self._stencil_cell_gradient_z cdef np.int64_t[:] I = np.zeros(2*self.n_total_faces_z, dtype=np.int64) cdef np.int64_t[:] J = np.zeros(2*self.n_total_faces_z, dtype=np.int64) cdef np.float64_t[:] V = np.zeros(2*self.n_total_faces_z, dtype=np.float64) cdef int_t ind for cell in self.tree.cells : next_cell = cell.neighbors[5] if next_cell==NULL: continue if next_cell.is_leaf(): ind = cell.faces[5].index I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 else: for i in range(4): # four neighbors in +z direction ind = next_cell.children[i].faces[4].index #0, 1, 2, 3 I[2*ind ] = ind I[2*ind + 1] = ind J[2*ind ] = cell.index J[2*ind + 1] = next_cell.children[i].index V[2*ind ] = -1.0 V[2*ind + 1] = 1.0 self._stencil_cell_gradient_z = ( sp.csr_matrix((V, (I,J)), shape=(self.n_total_faces_z, self.n_cells)) ) return self._stencil_cell_gradient_z @cython.boundscheck(False) def _deflate_edges_x(self): #I is output index (with hanging) #J is input index (without hanging) cdef np.int64_t[:] I = np.empty(2*self.n_total_edges_x, dtype=np.int64) cdef np.int64_t[:] J = np.empty(2*self.n_total_edges_x, dtype=np.int64) cdef np.float64_t[:] V = np.empty(2*self.n_total_edges_x, dtype=np.float64) cdef Edge *edge cdef np.int64_t ii #x edges: for it in self.tree.edges_x: edge = it.second ii = edge.index I[2*ii ] = ii I[2*ii + 1] = ii if edge.hanging: J[2*ii ] = edge.parents[0].index J[2*ii + 1] = edge.parents[1].index else: J[2*ii ] = ii J[2*ii + 1] = ii V[2*ii ] = 0.5 V[2*ii + 1] = 0.5 Rh = sp.csr_matrix((V, (I, J)), shape=(self.n_total_edges_x, self.n_total_edges_x)) # Test if it needs to be deflated again, (if any parents were also hanging) last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_edges_x) while(last_ind > self.n_edges_x): Rh = Rh*Rh last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_edges_x) Rh = Rh[:, : last_ind] return Rh @cython.boundscheck(False) def _deflate_edges_y(self): #I is output index (with hanging) #J is input index (without hanging) cdef int_t dim = self._dim cdef np.int64_t[:] I = np.empty(2*self.n_total_edges_y, dtype=np.int64) cdef np.int64_t[:] J = np.empty(2*self.n_total_edges_y, dtype=np.int64) cdef np.float64_t[:] V = np.empty(2*self.n_total_edges_y, dtype=np.float64) cdef Edge *edge cdef np.int64_t ii #x edges: for it in self.tree.edges_y: edge = it.second ii = edge.index I[2*ii ] = ii I[2*ii + 1] = ii if edge.hanging: J[2*ii ] = edge.parents[0].index J[2*ii + 1] = edge.parents[1].index else: J[2*ii ] = ii J[2*ii + 1] = ii V[2*ii ] = 0.5 V[2*ii + 1] = 0.5 Rh = sp.csr_matrix((V, (I, J)), shape=(self.n_total_edges_y, self.n_total_edges_y)) # Test if it needs to be deflated again, (if any parents were also hanging) last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_edges_y) while(last_ind > self.n_edges_y): Rh = Rh*Rh last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_edges_y) Rh = Rh[:, : last_ind] return Rh @cython.boundscheck(False) def _deflate_edges_z(self): #I is output index (with hanging) #J is input index (without hanging) cdef int_t dim = self._dim cdef np.int64_t[:] I = np.empty(2*self.n_total_edges_z, dtype=np.int64) cdef np.int64_t[:] J = np.empty(2*self.n_total_edges_z, dtype=np.int64) cdef np.float64_t[:] V = np.empty(2*self.n_total_edges_z, dtype=np.float64) cdef Edge *edge cdef np.int64_t ii #x edges: for it in self.tree.edges_z: edge = it.second ii = edge.index I[2*ii ] = ii I[2*ii + 1] = ii if edge.hanging: J[2*ii ] = edge.parents[0].index J[2*ii + 1] = edge.parents[1].index else: J[2*ii ] = ii J[2*ii + 1] = ii V[2*ii ] = 0.5 V[2*ii + 1] = 0.5 Rh = sp.csr_matrix((V, (I, J)), shape=(self.n_total_edges_z, self.n_total_edges_z)) # Test if it needs to be deflated again, (if any parents were also hanging) last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_edges_z) while(last_ind > self.n_edges_z): Rh = Rh*Rh last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_edges_z) Rh = Rh[:, : last_ind] return Rh def _deflate_edges(self): """Return a matrix to remove hanging edges. A hanging edge can either have 1 or 2 parents. If a hanging edge has a single parent, it's value is the same as the parent If a hanging edge has 2 parents, it's an average of the two parents """ if self._dim == 2: Rx = self._deflate_edges_x() Ry = self._deflate_edges_y() return sp.block_diag((Rx, Ry)) else: Rx = self._deflate_edges_x() Ry = self._deflate_edges_y() Rz = self._deflate_edges_z() return sp.block_diag((Rx, Ry, Rz)) def _deflate_faces(self): """Return a matrix that removes hanging faces. The operation assigns the hanging face the value of its parent. A hanging face will only ever have 1 parent. """ if(self._dim == 2): Rx = self._deflate_edges_x() Ry = self._deflate_edges_y() return sp.block_diag((Ry, Rx)) else: Rx = self._deflate_faces_x() Ry = self._deflate_faces_y() Rz = self._deflate_faces_z() return sp.block_diag((Rx, Ry, Rz)) @cython.boundscheck(False) def _deflate_faces_x(self): #I is output index (with hanging) #J is input index (without hanging) cdef np.int64_t[:] I = np.empty(self.n_total_faces_x, dtype=np.int64) cdef np.int64_t[:] J = np.empty(self.n_total_faces_x, dtype=np.int64) cdef np.float64_t[:] V = np.empty(self.n_total_faces_x, dtype=np.float64) cdef Face *face cdef np.int64_t ii; for it in self.tree.faces_x: face = it.second ii = face.index I[ii] = ii if face.hanging: J[ii] = face.parent.index else: J[ii] = ii V[ii] = 1.0 return sp.csr_matrix((V, (I, J))) @cython.boundscheck(False) def _deflate_faces_y(self): #I is output index (with hanging) #J is input index (without hanging) cdef np.int64_t[:] I = np.empty(self.n_total_faces_y, dtype=np.int64) cdef np.int64_t[:] J = np.empty(self.n_total_faces_y, dtype=np.int64) cdef np.float64_t[:] V = np.empty(self.n_total_faces_y, dtype=np.float64) cdef Face *face cdef np.int64_t ii; for it in self.tree.faces_y: face = it.second ii = face.index I[ii] = ii if face.hanging: J[ii] = face.parent.index else: J[ii] = ii V[ii] = 1.0 return sp.csr_matrix((V, (I, J))) @cython.boundscheck(False) def _deflate_faces_z(self): #I is output index (with hanging) #J is input index (without hanging) cdef np.int64_t[:] I = np.empty(self.n_total_faces_z, dtype=np.int64) cdef np.int64_t[:] J = np.empty(self.n_total_faces_z, dtype=np.int64) cdef np.float64_t[:] V = np.empty(self.n_total_faces_z, dtype=np.float64) cdef Face *face cdef np.int64_t ii; for it in self.tree.faces_z: face = it.second ii = face.index I[ii] = ii if face.hanging: J[ii] = face.parent.index else: J[ii] = ii V[ii] = 1.0 return sp.csr_matrix((V, (I, J))) @cython.boundscheck(False) def _deflate_nodes(self): """Return a matrix that removes hanging faces. A hanging node will have 2 parents in 2D or 2 or 4 parents in 3D. This matrix assigns the hanging node the average value of its parents. """ cdef np.int64_t[:] I = np.empty(4*self.n_total_nodes, dtype=np.int64) cdef np.int64_t[:] J = np.empty(4*self.n_total_nodes, dtype=np.int64) cdef np.float64_t[:] V = np.empty(4*self.n_total_nodes, dtype=np.float64) # I is output index # J is input index cdef Node *node cdef np.int64_t ii, i, offset offset = self.n_nodes cdef double[4] weights for it in self.tree.nodes: node = it.second ii = node.index I[4*ii:4*ii + 4] = ii if node.hanging: J[4*ii ] = node.parents[0].index J[4*ii + 1] = node.parents[1].index J[4*ii + 2] = node.parents[2].index J[4*ii + 3] = node.parents[3].index else: J[4*ii : 4*ii + 4] = ii V[4*ii : 4*ii + 4] = 0.25; Rh = sp.csr_matrix((V, (I, J)), shape=(self.n_total_nodes, self.n_total_nodes)) # Test if it needs to be deflated again, (if any parents were also hanging) last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_nodes) while(last_ind > self.n_nodes): Rh = Rh*Rh last_ind = max(np.nonzero(Rh.getnnz(0)>0)[0][-1], self.n_nodes) Rh = Rh[:, : last_ind] return Rh @property @cython.boundscheck(False) def average_edge_x_to_cell(self): r"""Averaging operator from x-edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from x-edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on x-edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges_x) scipy.sparse.csr_matrix The scalar averaging operator from x-edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_x}` be a discrete scalar quantity that lives on x-edges. **average_edge_x_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{xc}}` that projects :math:`\boldsymbol{\phi_x}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{xc}} \, \boldsymbol{\phi_x} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its x-edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Axc @ phi_x """ if self._average_edge_x_to_cell is not None: return self._average_edge_x_to_cell cdef np.int64_t[:] I,J cdef np.float64_t[:] V cdef np.int64_t ind, ii, n_epc cdef double scale n_epc = 2*(self._dim-1) I = np.empty(self.n_cells*n_epc, dtype=np.int64) J = np.empty(self.n_cells*n_epc, dtype=np.int64) V = np.empty(self.n_cells*n_epc, dtype=np.float64) scale = 1.0/n_epc for cell in self.tree.cells: ind = cell.index for ii in range(n_epc): I[ind*n_epc + ii] = ind J[ind*n_epc + ii] = cell.edges[ii].index V[ind*n_epc + ii] = scale Rex = self._deflate_edges_x() self._average_edge_x_to_cell = sp.csr_matrix((V, (I, J)))*Rex return self._average_edge_x_to_cell @property @cython.boundscheck(False) def average_edge_y_to_cell(self): r"""Averaging operator from y-edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from y-edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on y-edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges_y) scipy.sparse.csr_matrix The scalar averaging operator from y-edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_y}` be a discrete scalar quantity that lives on y-edges. **average_edge_y_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{yc}}` that projects :math:`\boldsymbol{\phi_y}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{yc}} \, \boldsymbol{\phi_y} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its y-edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Ayc @ phi_y """ if self._average_edge_y_to_cell is not None: return self._average_edge_y_to_cell cdef np.int64_t[:] I,J cdef np.float64_t[:] V cdef np.int64_t ind, ii, n_epc cdef double scale n_epc = 2*(self._dim-1) I = np.empty(self.n_cells*n_epc, dtype=np.int64) J = np.empty(self.n_cells*n_epc, dtype=np.int64) V = np.empty(self.n_cells*n_epc, dtype=np.float64) scale = 1.0/n_epc for cell in self.tree.cells: ind = cell.index for ii in range(n_epc): I[ind*n_epc + ii] = ind J[ind*n_epc + ii] = cell.edges[n_epc + ii].index #y edges V[ind*n_epc + ii] = scale Rey = self._deflate_edges_y() self._average_edge_y_to_cell = sp.csr_matrix((V, (I, J)))*Rey return self._average_edge_y_to_cell @property @cython.boundscheck(False) def average_edge_z_to_cell(self): r"""Averaging operator from z-edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from z-edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on z-edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges_z) scipy.sparse.csr_matrix The scalar averaging operator from z-edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_z}` be a discrete scalar quantity that lives on z-edges. **average_edge_z_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{zc}}` that projects :math:`\boldsymbol{\phi_z}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{zc}} \, \boldsymbol{\phi_z} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its z-edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Azc @ phi_z """ if self._average_edge_z_to_cell is not None: return self._average_edge_z_to_cell if self._dim == 2: raise Exception('There are no z-edges in 2D') cdef np.int64_t[:] I,J cdef np.float64_t[:] V cdef np.int64_t ind, ii, n_epc cdef double scale n_epc = 2*(self._dim-1) I = np.empty(self.n_cells*n_epc, dtype=np.int64) J = np.empty(self.n_cells*n_epc, dtype=np.int64) V = np.empty(self.n_cells*n_epc, dtype=np.float64) scale = 1.0/n_epc for cell in self.tree.cells: ind = cell.index for ii in range(n_epc): I[ind*n_epc + ii] = ind J[ind*n_epc + ii] = cell.edges[ii + 2*n_epc].index V[ind*n_epc + ii] = scale Rez = self._deflate_edges_z() self._average_edge_z_to_cell = sp.csr_matrix((V, (I, J)))*Rez return self._average_edge_z_to_cell @property def average_edge_to_cell(self): r"""Averaging operator from edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on mesh edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges) scipy.sparse.csr_matrix The scalar averaging operator from edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_e}` be a discrete scalar quantity that lives on mesh edges. **average_edge_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{ec}}` that projects :math:`\boldsymbol{\phi_e}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{ec}} \, \boldsymbol{\phi_e} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Aec @ phi_e """ if self._average_edge_to_cell is None: stacks = [self.average_edge_x_to_cell, self.average_edge_y_to_cell] if self._dim == 3: stacks += [self.average_edge_z_to_cell] self._average_edge_to_cell = 1.0/self._dim * sp.hstack(stacks).tocsr() return self._average_edge_to_cell @property def average_edge_to_cell_vector(self): r"""Averaging operator from edges to cell centers (vector quantities). This property constructs the averaging operator that independently maps the Cartesian components of vector quantities from edges to cell centers. This averaging operators is used when a discrete vector quantity defined on mesh edges must be approximated at cell centers. Once constructed, the operator is stored permanently as a property of the mesh. Be aware that the Cartesian components of the original vector are defined on their respective edges; e.g. the x-component lives on x-edges. However, the x, y and z components are being averaged separately to cell centers. The operation is implemented as a matrix vector product, i.e.:: u_c = Aec @ u_e Returns ------- (dim * n_cells, n_edges) scipy.sparse.csr_matrix The vector averaging operator from edges to cell centers. Since we are averaging a vector quantity to cell centers, the first dimension of the operator is the mesh dimension times the number of cells. Notes ----- Let :math:`\mathbf{u_e}` be the discrete representation of a vector quantity whose Cartesian components are defined on their respective edges; e.g. :math:`u_x` is defined on x-edges. **average_edge_to_cell_vector** constructs a discrete linear operator :math:`\mathbf{A_{ec}}` that projects each Cartesian component of :math:`\mathbf{u_e}` independently to cell centers, i.e.: .. math:: \mathbf{u_c} = \mathbf{A_{ec}} \, \mathbf{u_e} where :math:`\mathbf{u_c}` is a discrete vector quantity whose Cartesian components defined at the cell centers and organized into a 1D array of the form np.r_[ux, uy, uz]. For each cell, and for each Cartesian component, we are simply taking the average of the values defined on the cell's corresponding edges and placing the result at the cell's center. """ if self._average_edge_to_cell_vector is None: stacks = [self.average_edge_x_to_cell, self.average_edge_y_to_cell] if self._dim == 3: stacks += [self.average_edge_z_to_cell] self._average_edge_to_cell_vector = sp.block_diag(stacks).tocsr() return self._average_edge_to_cell_vector @property def average_edge_to_face(self): r"""Averaging operator from edges to faces. This property constructs the averaging operator that maps uantities from edges to faces. This averaging operators is used when a discrete quantity defined on mesh edges must be approximated at faces. The operation is implemented as a matrix vector product, i.e.:: u_f = Aef @ u_e Once constructed, the operator is stored permanently as a property of the mesh. Returns ------- (n_faces, n_edges) scipy.sparse.csr_matrix The averaging operator from edges to faces. Notes ----- Let :math:`\mathbf{u_e}` be the discrete representation of aquantity whose that is defined on the edges. **average_edge_to_face** constructs a discrete linear operator :math:`\mathbf{A_{ef}}` that projects :math:`\mathbf{u_e}` to its corresponding face, i.e.: .. math:: \mathbf{u_f} = \mathbf{A_{ef}} \, \mathbf{u_e} where :math:`\mathbf{u_f}` is a quantity defined on the respective faces. """ if self.dim == 2: return sp.diags( [1, 1], [-self.n_faces_x, self.n_faces_y], shape=(self.n_faces, self.n_edges) ) if self._average_edge_to_face is not None: return self._average_edge_to_face cdef: int_t dim = self._dim np.int64_t[:] I = np.empty(4*self.n_faces, dtype=np.int64) np.int64_t[:] J = np.empty(4*self.n_faces, dtype=np.int64) np.float64_t[:] V = np.full(4*self.n_faces, 0.25, dtype=np.float64) Face *face int_t ii int_t face_offset_y = self.n_faces_x int_t face_offset_z = self.n_faces_x + self.n_faces_y int_t edge_offset_y = self.n_total_edges_x int_t edge_offset_z = self.n_total_edges_x + self.n_total_edges_y double area for it in self.tree.faces_x: face = it.second if face.hanging: continue ii = face.index I[4*ii : 4*ii + 4] = ii J[4*ii ] = face.edges[0].index + edge_offset_z J[4*ii + 1] = face.edges[1].index + edge_offset_y J[4*ii + 2] = face.edges[2].index + edge_offset_z J[4*ii + 3] = face.edges[3].index + edge_offset_y for it in self.tree.faces_y: face = it.second if face.hanging: continue ii = face.index + face_offset_y I[4*ii : 4*ii + 4] = ii J[4*ii ] = face.edges[0].index + edge_offset_z J[4*ii + 1] = face.edges[1].index J[4*ii + 2] = face.edges[2].index + edge_offset_z J[4*ii + 3] = face.edges[3].index for it in self.tree.faces_z: face = it.second if face.hanging: continue ii = face.index + face_offset_z I[4*ii : 4*ii + 4] = ii J[4*ii ] = face.edges[0].index + edge_offset_y J[4*ii + 1] = face.edges[1].index J[4*ii + 2] = face.edges[2].index + edge_offset_y J[4*ii + 3] = face.edges[3].index Av = sp.csr_matrix((V, (I, J)),shape=(self.n_faces, self.n_total_edges)) R = self._deflate_edges() self._average_edge_to_face = Av @ R return self._average_edge_to_face @property @cython.boundscheck(False) def average_face_x_to_cell(self): r"""Averaging operator from x-faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from x-faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on x-faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. Returns ------- (n_cells, n_faces_x) scipy.sparse.csr_matrix The scalar averaging operator from x-faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_x}` be a discrete scalar quantity that lives on x-faces. **average_face_x_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{xc}}` that projects :math:`\boldsymbol{\phi_x}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{xc}} \, \boldsymbol{\phi_x} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its x-faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Axc @ phi_x """ if self._average_face_x_to_cell is not None: return self._average_face_x_to_cell if self._dim == 2: return self.average_edge_y_to_cell cdef np.int64_t[:] I,J cdef np.float64_t[:] V cdef Face *face1 cdef Face *face2 cdef np.int64_t ii I = np.empty(self.n_cells*2, dtype=np.int64) J = np.empty(self.n_cells*2, dtype=np.int64) V = np.empty(self.n_cells*2, dtype=np.float64) for cell in self.tree.cells: face1 = cell.faces[0] # x face face2 = cell.faces[1] # x face ii = cell.index I[ii*2 : ii*2 + 2] = ii J[ii*2 ] = face1.index J[ii*2 + 1] = face2.index V[ii*2 : ii*2 + 2] = 0.5 Rfx = self._deflate_faces_x() self._average_face_x_to_cell = sp.csr_matrix((V, (I, J)))*Rfx return self._average_face_x_to_cell @property @cython.boundscheck(False) def average_face_y_to_cell(self): r"""Averaging operator from y-faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from y-faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on x-faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_faces_y) scipy.sparse.csr_matrix The scalar averaging operator from y-faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_y}` be a discrete scalar quantity that lives on y-faces. **average_face_y_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{yc}}` that projects :math:`\boldsymbol{\phi_y}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{yc}} \, \boldsymbol{\phi_y} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its y-faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Ayc @ phi_y """ if self._average_face_y_to_cell is not None: return self._average_face_y_to_cell if self._dim == 2: return self.average_edge_x_to_cell cdef np.int64_t[:] I,J cdef np.float64_t[:] V cdef Face *face1 cdef Face *face2 cdef np.int64_t ii I = np.empty(self.n_cells*2, dtype=np.int64) J = np.empty(self.n_cells*2, dtype=np.int64) V = np.empty(self.n_cells*2, dtype=np.float64) for cell in self.tree.cells: face1 = cell.faces[2] # y face face2 = cell.faces[3] # y face ii = cell.index I[ii*2 : ii*2 + 2] = ii J[ii*2 ] = face1.index J[ii*2 + 1] = face2.index V[ii*2 : ii*2 + 2] = 0.5 Rfy = self._deflate_faces_y() self._average_face_y_to_cell = sp.csr_matrix((V, (I, J)))*Rfy return self._average_face_y_to_cell @property @cython.boundscheck(False) def average_face_z_to_cell(self): r"""Averaging operator from z-faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from z-faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on z-faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_faces_z) scipy.sparse.csr_matrix The scalar averaging operator from z-faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_z}` be a discrete scalar quantity that lives on z-faces. **average_face_z_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{zc}}` that projects :math:`\boldsymbol{\phi_z}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{zc}} \, \boldsymbol{\phi_z} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its z-faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Azc @ phi_z """ if self._average_face_z_to_cell is not None: return self._average_face_z_to_cell if self._dim == 2: raise Exception('There are no z-faces in 2D') cdef np.int64_t[:] I,J cdef np.float64_t[:] V cdef Face *face1 cdef Face *face2 cdef np.int64_t ii I = np.empty(self.n_cells*2, dtype=np.int64) J = np.empty(self.n_cells*2, dtype=np.int64) V = np.empty(self.n_cells*2, dtype=np.float64) for cell in self.tree.cells: face1 = cell.faces[4] face2 = cell.faces[5] ii = cell.index I[ii*2 : ii*2 + 2] = ii J[ii*2 ] = face1.index J[ii*2 + 1] = face2.index V[ii*2 : ii*2 + 2] = 0.5 Rfy = self._deflate_faces_z() self._average_face_z_to_cell = sp.csr_matrix((V, (I, J)))*Rfy return self._average_face_z_to_cell @property def average_face_to_cell(self): r"""Averaging operator from faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on mesh faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_faces) scipy.sparse.csr_matrix The scalar averaging operator from faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_f}` be a discrete scalar quantity that lives on mesh faces. **average_face_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{fc}}` that projects :math:`\boldsymbol{\phi_f}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{fc}} \, \boldsymbol{\phi_f} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Afc @ phi_f """ if self._average_face_to_cell is None: stacks = [self.average_face_x_to_cell, self.aveFy2CC] if self._dim == 3: stacks += [self.average_face_z_to_cell] self._average_face_to_cell = 1./self._dim*sp.hstack(stacks).tocsr() return self._average_face_to_cell @property def average_face_to_cell_vector(self): r"""Averaging operator from faces to cell centers (vector quantities). This property constructs the averaging operator that independently maps the Cartesian components of vector quantities from faces to cell centers. This averaging operators is used when a discrete vector quantity defined on mesh faces must be approximated at cell centers. Once constructed, the operator is stored permanently as a property of the mesh. Be aware that the Cartesian components of the original vector are defined on their respective faces; e.g. the x-component lives on x-faces. However, the x, y and z components are being averaged separately to cell centers. The operation is implemented as a matrix vector product, i.e.:: u_c = Afc @ u_f Returns ------- (dim * n_cells, n_faces) scipy.sparse.csr_matrix The vector averaging operator from faces to cell centers. Since we are averaging a vector quantity to cell centers, the first dimension of the operator is the mesh dimension times the number of cells. Notes ----- Let :math:`\mathbf{u_f}` be the discrete representation of a vector quantity whose Cartesian components are defined on their respective faces; e.g. :math:`u_x` is defined on x-faces. **average_face_to_cell_vector** constructs a discrete linear operator :math:`\mathbf{A_{fc}}` that projects each Cartesian component of :math:`\mathbf{u_f}` independently to cell centers, i.e.: .. math:: \mathbf{u_c} = \mathbf{A_{fc}} \, \mathbf{u_f} where :math:`\mathbf{u_c}` is a discrete vector quantity whose Cartesian components defined at the cell centers and organized into a 1D array of the form np.r_[ux, uy, uz]. For each cell, and for each Cartesian component, we are simply taking the average of the values defined on the cell's corresponding faces and placing the result at the cell's center. """ if self._average_face_to_cell_vector is None: stacks = [self.average_face_x_to_cell, self.aveFy2CC] if self._dim == 3: stacks += [self.average_face_z_to_cell] self._average_face_to_cell_vector = sp.block_diag(stacks).tocsr() return self._average_face_to_cell_vector @property @cython.boundscheck(False) def average_node_to_cell(self): r"""Averaging operator from nodes to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from nodes to cell centers. This averaging operator is used when a discrete scalar quantity defined on mesh nodes must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_nodes) scipy.sparse.csr_matrix The scalar averaging operator from nodes to cell centers Notes ----- Let :math:`\boldsymbol{\phi_n}` be a discrete scalar quantity that lives on mesh nodes. **average_node_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{nc}}` that projects :math:`\boldsymbol{\phi_f}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{nc}} \, \boldsymbol{\phi_n} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its nodes. The operation is implemented as a matrix vector product, i.e.:: phi_c = Anc @ phi_n """ cdef np.int64_t[:] I, J cdef np.float64_t[:] V cdef np.int64_t ii, id, n_ppc cdef double scale if self._average_node_to_cell is None: n_ppc = 1<>1)*4 + i%2] #0 1 4 5 ind = child.faces[2].index I[2*ind ] = ind_parent I[2*ind + 1] = ind_parent J[2*ind ] = cell.index J[2*ind + 1] = child.index V[2*ind ] = w/children_per_parent V[2*ind + 1] = (1.0-w)/children_per_parent self._average_cell_to_face_y = sp.csr_matrix((V, (I,J)), shape=(self.n_faces_y, self.n_cells)) return self._average_cell_to_face_y @property def average_cell_to_face_z(self): """Averaging operator from cell centers to z faces (scalar quantities). This property constructs an averaging operator that maps scalar quantities from cell centers to face. This averaging operator is used when a discrete scalar quantity defined cell centers must be projected to faces. Returns ------- (n_faces_z, n_cells) scipy.sparse.csr_matrix The scalar averaging operator from cell centers to z faces """ self._error_if_not_finalized("average_cell_to_face_z") if self.dim == 2: raise Exception('TreeMesh has no z-faces in 2D') if self._average_cell_to_face_z is not None: return self._average_cell_to_face_z cdef np.int64_t[:] I = np.zeros(2*self.n_total_faces_z, dtype=np.int64) cdef np.int64_t[:] J = np.zeros(2*self.n_total_faces_z, dtype=np.int64) cdef np.float64_t[:] V = np.zeros(2*self.n_total_faces_z, dtype=np.float64) cdef int dim = self._dim cdef int_t ind, ind_parent cdef int_t children_per_parent = (dim-1)*2 cdef c_Cell* child cdef c_Cell* next_cell cdef c_Cell* prev_cell cdef double w for cell in self.tree.cells : next_cell = cell.neighbors[5] prev_cell = cell.neighbors[4] # handle extrapolation to boundary faces if next_cell == NULL: ind = cell.faces[5].index # +z face I[2*ind ] = ind J[2*ind ] = cell.index V[2*ind ] = 1.0 continue if prev_cell == NULL: ind = cell.faces[4].index # -z face I[2*ind ] = ind J[2*ind ] = cell.index V[2*ind ] = 1.0 if next_cell.is_leaf(): if next_cell.level == cell.level: #I am on the same level and easy to interpolate ind = cell.faces[5].index w = (next_cell.location[2] - cell.faces[5].location[2])/( next_cell.location[2] - cell.location[2]) I[2*ind ] = ind I[2*ind+1] = ind J[2*ind ] = cell.index J[2*ind+1] = next_cell.index V[2*ind ] = w V[2*ind+1] = (1.0-w) else: # if next cell is a level larger than i am ind = cell.faces[5].index ind_parent = cell.faces[5].parent.index w = (next_cell.location[2] - cell.faces[5].location[2])/( next_cell.location[2] - cell.location[2]) I[2*ind ] = ind_parent I[2*ind+1] = ind_parent J[2*ind ] = cell.index J[2*ind+1] = next_cell.index V[2*ind ] = w/children_per_parent V[2*ind+1] = (1.0-w)/children_per_parent else: #should mean next cell is not a leaf so need to loop over children ind_parent = cell.faces[5].index w = (next_cell.children[0].location[2] - cell.faces[5].location[2])/( next_cell.children[0].location[2] - cell.location[2]) for i in range(4): # four neighbors in +x direction child = next_cell.children[i] ind = child.faces[4].index #0 1 2 3 I[2*ind ] = ind_parent I[2*ind + 1] = ind_parent J[2*ind ] = cell.index J[2*ind + 1] = child.index V[2*ind ] = w/children_per_parent V[2*ind + 1] = (1.0-w)/children_per_parent self._average_cell_to_face_z = sp.csr_matrix((V, (I,J)), shape=(self.n_faces_z, self.n_cells)) return self._average_cell_to_face_z @property def project_face_to_boundary_face(self): r"""Projection matrix from all faces to boundary faces. Constructs and returns a matrix :math:`\mathbf{P}` that projects from all mesh faces to boundary faces. That is, for a discrete vector :math:`\mathbf{u}` that lives on the faces, the values on the boundary faces :math:`\mathbf{u_b}` can be extracted via the following matrix-vector product:: ub = P @ u Returns ------- scipy.sparse.csr_matrix (n_boundary_faces, n_faces) Projection matrix with shape """ self._error_if_not_finalized("project_face_to_boundary_face") faces_x = self.faces_x faces_y = self.faces_y x0, xF = self._xs[0], self._xs[-1] y0, yF = self._ys[0], self._ys[-1] is_b = np.r_[ (faces_x[:, 0] == x0) | (faces_x[:, 0] == xF), (faces_y[:, 1] == y0) | (faces_y[:, 1] == yF) ] if self.dim == 3: faces_z = self.faces_z z0, zF = self._zs[0], self._zs[-1] is_b = np.r_[ is_b, (faces_z[:, 2] == z0) | (faces_z[:, 2] == zF) ] return sp.eye(self.n_faces, format='csr')[is_b] @property def project_edge_to_boundary_edge(self): r"""Projection matrix from all edges to boundary edges. Constructs and returns a matrix :math:`\mathbf{P}` that projects from all mesh edges to boundary edges. That is, for a discrete vector :math:`\mathbf{u}` that lives on the edges, the values on the boundary edges :math:`\mathbf{u_b}` can be extracted via the following matrix-vector product:: ub = P @ u Returns ------- (n_boundary_edges, n_edges) scipy.sparse.csr_matrix Projection matrix with shape """ self._error_if_not_finalized("project_edge_to_boundary_edge") edges_x = self.edges_x edges_y = self.edges_y x0, xF = self._xs[0], self._xs[-1] y0, yF = self._ys[0], self._ys[-1] is_bx = (edges_x[:, 1] == y0) | (edges_x[:, 1] == yF) is_by = (edges_y[:, 0] == x0) | (edges_y[:, 0] == xF) if self.dim == 3: z0, zF = self._zs[0], self._zs[-1] edges_z = self.edges_z is_bx |= (edges_x[:, 2] == z0) | (edges_x[:, 2] == zF) is_by |= (edges_y[:, 2] == z0) | (edges_y[:, 2] == zF) is_bz = ( (edges_z[:, 0] == x0) | (edges_z[:, 0] == xF) | (edges_z[:, 1] == y0) | (edges_z[:, 1] == yF) ) is_b = np.r_[is_bx, is_by, is_bz] else: is_b = np.r_[is_bx, is_by] return sp.eye(self.n_edges, format='csr')[is_b] @property def project_node_to_boundary_node(self): r"""Projection matrix from all nodes to boundary nodes. Constructs and returns a matrix :math:`\mathbf{P}` that projects from all mesh nodes to boundary nodes. That is, for a discrete scalar :math:`\mathbf{u}` that lives on the nodes, the values on the boundary nodes :math:`\mathbf{u_b}` can be extracted via the following matrix-vector product:: ub = P @ u Returns ------- (n_boundary_nodes, n_nodes) scipy.sparse.csr_matrix Projection matrix with shape """ self._error_if_not_finalized("project_node_to_boundary_node") nodes = self.nodes x0, xF = self._xs[0], self._xs[-1] y0, yF = self._ys[0], self._ys[-1] is_b = ( (nodes[:, 0] == x0) | (nodes[:, 0] == xF) | (nodes[:, 1] == y0) | (nodes[:, 1] == yF) ) if self.dim > 2: z0, zF = self._zs[0], self._zs[-1] is_b |= (nodes[:, 2] == z0) | (nodes[:, 2] == zF) return sp.eye(self.n_nodes, format='csr')[is_b] def _count_cells_per_index(self): cdef np.int64_t[:] counts = np.zeros(self.max_level+1, dtype=np.int64) for cell in self.tree.cells: counts[cell.level] += 1 return np.array(counts) def _cell_levels_by_indexes(self, index=None): cdef np.int64_t[:] inds cdef bool do_all = index is None cdef int_t n_cells if not do_all: index = np.require(np.atleast_1d(index), dtype=np.int64, requirements='C') inds = index n_cells = inds.shape[0] else: n_cells = self.n_cells cdef np.int64_t[:] levels = np.empty(n_cells, dtype=np.int64) cdef int_t ii for i in range(n_cells): ii = i if do_all else inds[i] levels[i] = self.tree.cells[ii].level if n_cells == 1: return levels[0] else: return np.array(levels) def _getFaceP(self, xFace, yFace, zFace): cdef int dim = self._dim cdef int_t ind, id cdef np.int64_t[:] I, J, J1, J2, J3 cdef np.float64_t[:] V J1 = np.empty(self.n_cells, dtype=np.int64) J2 = np.empty(self.n_cells, dtype=np.int64) if dim==3: J3 = np.empty(self.n_cells, dtype=np.int64) cdef int[3] faces cdef np.int64_t[:] offsets = np.empty(self._dim, dtype=np.int64) faces[0] = (xFace == 'fXp') faces[1] = (yFace == 'fYp') if dim == 3: faces[2] = (zFace == 'fZp') if dim == 2: offsets[0] = 0 offsets[1] = self.n_total_faces_x else: offsets[0] = 0 offsets[1] = self.n_total_faces_x offsets[2] = self.n_total_faces_x + self.n_total_faces_y for cell in self.tree.cells: ind = cell.index if dim==2: J1[ind] = cell.edges[2 + faces[0]].index J2[ind] = cell.edges[ faces[1]].index + offsets[1] else: J1[ind] = cell.faces[ faces[0]].index J2[ind] = cell.faces[2 + faces[1]].index + offsets[1] J3[ind] = cell.faces[4 + faces[2]].index + offsets[2] I = np.arange(dim*self.n_cells, dtype=np.int64) if dim==2: J = np.r_[J1, J2] else: J = np.r_[J1, J2, J3] V = np.ones(self.n_cells*dim, dtype=np.float64) P = sp.csr_matrix((V, (I, J)), shape=(self._dim*self.n_cells, self.n_total_faces)) Rf = self._deflate_faces() return P*Rf def _getFacePxx(self): def Pxx(xFace, yFace): return self._getFaceP(xFace, yFace, None) return Pxx def _getFacePxxx(self): def Pxxx(xFace, yFace, zFace): return self._getFaceP(xFace, yFace, zFace) return Pxxx def _getEdgeP(self, xEdge, yEdge, zEdge): cdef int dim = self._dim cdef int_t ind, id cdef int epc = 1<<(dim-1) #edges per cell 2/4 cdef np.int64_t[:] I, J, J1, J2, J3 cdef np.float64_t[:] V J1 = np.empty(self.n_cells, dtype=np.int64) J2 = np.empty(self.n_cells, dtype=np.int64) if dim == 3: J3 = np.empty(self.n_cells, dtype=np.int64) cdef int[3] edges cdef np.int64_t[:] offsets = np.empty(self._dim, dtype=np.int64) try: edges[0] = int(xEdge[-1]) #0, 1, 2, 3 edges[1] = int(yEdge[-1]) #0, 1, 2, 3 if dim == 3: edges[2] = int(zEdge[-1]) #0, 1, 2, 3 except ValueError: raise Exception('Last character of edge string must be 0, 1, 2, or 3') offsets[0] = 0 offsets[1] = self.n_total_edges_x if dim==3: offsets[2] = self.n_total_edges_x + self.n_total_edges_y for cell in self.tree.cells: ind = cell.index J1[ind] = cell.edges[0*epc + edges[0]].index + offsets[0] J2[ind] = cell.edges[1*epc + edges[1]].index + offsets[1] if dim==3: J3[ind] = cell.edges[2*epc + edges[2]].index + offsets[2] I = np.arange(dim*self.n_cells, dtype=np.int64) if dim==2: J = np.r_[J1, J2] else: J = np.r_[J1, J2, J3] V = np.ones(self.n_cells*dim, dtype=np.float64) P = sp.csr_matrix((V, (I, J)), shape=(self._dim*self.n_cells, self.n_total_edges)) Rf = self._deflate_edges() return P*Rf def _getEdgePxx(self): def Pxx(xEdge, yEdge): return self._getEdgeP(xEdge, yEdge, None) return Pxx def _getEdgePxxx(self): def Pxxx(xEdge, yEdge, zEdge): return self._getEdgeP(xEdge, yEdge, zEdge) return Pxxx def _getEdgeIntMat(self, locs, zeros_outside, direction): cdef: double[:, :] locations = locs int_t dir, dir1, dir2 int_t dim = self._dim int_t n_loc = locs.shape[0] int_t n_edges = 2 if self._dim == 2 else 4 np.int64_t[:] indptr = 2 * n_edges * np.arange(n_loc+1, dtype=np.int64) np.int64_t[:] indices = np.empty(n_loc * 2 * n_edges, dtype=np.int64) np.float64_t[:] data = np.empty(n_loc * 2 * n_edges, dtype=np.float64) np.int64_t[:] row_inds np.float64_t[:] row_data int_t ii, i, j, offset c_Cell *cell c_Cell *i0 c_Cell *i1 Edge *i000 Edge *i001 Edge *i010 Edge *i011 Edge *i100 Edge *i101 Edge *i110 Edge *i111 double x, y, z double w1, w2, w3 double eps = 100*np.finfo(float).eps int zeros_out = zeros_outside if direction == 'x': dir, dir1, dir2 = 0, 1, 2 offset = 0 elif direction == 'y': dir, dir1, dir2 = 1, 0, 2 offset = self.n_total_edges_x elif direction == 'z': dir, dir1, dir2 = 2, 0, 1 offset = self.n_total_edges_x + self.n_total_edges_y else: raise ValueError('Invalid direction, must be x, y, or z') for i in range(n_loc): x = locations[i, 0] y = locations[i, 1] z = locations[i, 2] if dim==3 else 0.0 # get containing (or closest) cell cell = self.tree.containing_cell(x, y, z) row_inds = indices[indptr[i]:indptr[i+1]] row_data = data[indptr[i]:indptr[i+1]] was_outside = False if zeros_out: if x < cell.points[0].location[0]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif x > cell.points[3].location[0]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif y < cell.points[0].location[1]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif y > cell.points[3].location[1]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif dim == 3 and z < cell.points[0].location[2]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif dim == 3 and z > cell.points[7].location[2]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True if not was_outside: # look + dir and - dir away if ( locations[i, dir] < cell.location[dir] and cell.neighbors[2*dir] != NULL and cell.neighbors[2*dir].is_leaf() and cell.neighbors[2*dir].level == cell.level ): i0 = cell.neighbors[2*dir] i1 = cell elif( locations[i, dir] > cell.location[dir] and cell.neighbors[2*dir + 1] != NULL and cell.neighbors[2*dir + 1].is_leaf() and cell.neighbors[2*dir + 1].level == cell.level ): i0 = cell i1 = cell.neighbors[2*dir + 1] else: i0 = cell i1 = cell i000 = i0.edges[n_edges * dir] i001 = i0.edges[n_edges * dir + 1] w1 = ((i001.location[dir1] - locations[i, dir1])/ (i001.location[dir1] - i000.location[dir1])) i010 = i1.edges[n_edges*dir] i011 = i1.edges[n_edges*dir + 1] if i0.index != i1.index: w2 = ((i010.location[dir] - locations[i, dir])/ (i010.location[dir] - i000.location[dir])) else: w2 = 1.0 if dim == 3: i100 = i0.edges[n_edges * dir + 2] i101 = i0.edges[n_edges * dir + 3] i110 = i1.edges[n_edges * dir + 2] i111 = i1.edges[n_edges * dir + 3] w3 = ((i100.location[dir2] - locations[i, dir2])/ (i100.location[dir2] - i000.location[dir2])) else: w3 = 1.0 w1 = _clip01(w1) w2 = _clip01(w2) w3 = _clip01(w3) row_data[0] = w1 * w2 * w3 row_data[1] = (1 - w1) * w2 * w3 row_data[2] = w1 * (1 - w2) * w3 row_data[3] = (1 - w1) * (1 - w2) * w3 row_inds[0] = i000.index + offset row_inds[1] = i001.index + offset row_inds[2] = i010.index + offset row_inds[3] = i011.index + offset if dim==3: row_data[4] = w1 * w2 * (1 - w3) row_data[5] = (1 - w1) * w2 * (1 - w3) row_data[6] = w1 * (1 - w2) * (1 - w3) row_data[7] = (1 - w1) * (1 - w2) * (1 - w3) row_inds[4] = i100.index + offset row_inds[5] = i101.index + offset row_inds[6] = i110.index + offset row_inds[7] = i111.index + offset Re = self._deflate_edges() A = sp.csr_matrix((data, indices, indptr), shape=(locs.shape[0], self.n_total_edges)) return A*Re def _getFaceIntMat(self, locs, zeros_outside, direction): cdef: double[:, :] locations = locs int_t dir, dir1, dir2, temp int_t dim = self._dim int_t n_loc = locs.shape[0] int_t n_faces = 4 if dim == 2 else 8 np.int64_t[:] indptr = n_faces * np.arange(n_loc + 1, dtype=np.int64) np.int64_t[:] indices = np.empty(n_loc*n_faces, dtype=np.int64) np.float64_t[:] data = np.empty(n_loc*n_faces, dtype=np.float64) int_t ii, i, offset c_Cell *cell c_Cell *i00 c_Cell *i01 c_Cell *i10 c_Cell *i11 Face *f000 Face *f001 Face *f010 Face *f011 Face *f100 Face *f101 Face *f110 Face *f111 Edge *e00 Edge *e01 Edge *e10 Edge *e11 double x, y, z double w1, w2, w3 double eps = 100*np.finfo(float).eps int zeros_out = zeros_outside if direction == 'x': dir = 0 dir1 = 1 dir2 = 2 offset = 0 elif direction == 'y': dir = 1 dir1 = 0 dir2 = 2 offset = self.n_total_faces_x elif direction == 'z': dir = 2 dir1 = 0 dir2 = 1 offset = self.n_total_faces_x + self.n_total_faces_y else: raise ValueError('Invalid direction, must be x, y, or z') for i in range(n_loc): x = locations[i, 0] y = locations[i, 1] z = locations[i, 2] if dim==3 else 0.0 #get containing (or closest) cell cell = self.tree.containing_cell(x, y, z) row_inds = indices[indptr[i]:indptr[i+1]] row_data = data[indptr[i]:indptr[i+1]] was_outside = False if zeros_out: if x < cell.points[0].location[0]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif x > cell.points[3].location[0]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif y < cell.points[0].location[1]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif y > cell.points[3].location[1]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif dim == 3 and z < cell.points[0].location[2]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif dim == 3 and z > cell.points[7].location[2]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True if not was_outside: # Find containing cells # Decide order to search based on which face it is closest to if dim == 3: if ( abs(locations[i, dir1] - cell.location[dir1]) < abs(locations[i, dir2] - cell.location[dir2]) ): temp = dir1 dir1 = dir2 dir2 = temp # look in dir1 direction if ( locations[i, dir1] < cell.location[dir1] and cell.neighbors[2*dir1] != NULL and cell.neighbors[2*dir1].is_leaf() and cell.neighbors[2*dir1].level == cell.level ): i00 = cell.neighbors[2*dir1] i01 = cell elif ( locations[i, dir1] > cell.location[dir1] and cell.neighbors[2*dir1 + 1] != NULL and cell.neighbors[2*dir1 + 1].is_leaf() and cell.neighbors[2*dir1 + 1].level == cell.level ): i00 = cell i01 = cell.neighbors[2*dir1 + 1] else: i00 = i01 = cell if dim == 2: e00 = i00.edges[2 * dir1] e01 = i00.edges[2 * dir1 + 1] e10 = i01.edges[2 * dir1] e11 = i01.edges[2 * dir1 + 1] w1 = ((e01.location[dir] - locations[i, dir])/ (e01.location[dir] - e00.location[dir])) if i00.index != i01.index: w2 = ((e10.location[dir1] - locations[i, dir1])/ (e10.location[dir1] - e00.location[dir1])) else: w2 = 1.0 w1 = _clip01(w1) w2 = _clip01(w2) row_data[0] = w1 * w2 row_data[1] = (1 - w1) * w2 row_data[2] = w1 * (1 - w2) row_data[3] = (1 - w1) * (1 - w2) row_inds[0] = e00.index + offset row_inds[1] = e01.index + offset row_inds[2] = e10.index + offset row_inds[3] = e11.index + offset else: # Look dir2 from previous two cells if ( locations[i, dir2] < cell.location[dir2] and i00.neighbors[2*dir2] != NULL and i01.neighbors[2*dir2] != NULL and i00.neighbors[2*dir2].is_leaf() and i01.neighbors[2*dir2].is_leaf() and i00.neighbors[2*dir2].level == i00.level and i01.neighbors[2*dir2].level == i01.level ): i10 = i00 i11 = i01 i00 = i00.neighbors[2*dir2] i01 = i01.neighbors[2*dir2] elif ( locations[i, dir2] > cell.location[dir2] and i00.neighbors[2*dir2 + 1] != NULL and i01.neighbors[2*dir2 + 1] != NULL and i00.neighbors[2*dir2 + 1].is_leaf() and i01.neighbors[2*dir2 + 1].is_leaf() and i00.neighbors[2*dir2 + 1].level == i00.level and i01.neighbors[2*dir2 + 1].level == i01.level ): i10 = i00.neighbors[2*dir2 + 1] i11 = i01.neighbors[2*dir2 + 1] else: i10 = i00 i11 = i01 f000 = i00.faces[dir * 2] f001 = i00.faces[dir * 2 + 1] f010 = i01.faces[dir * 2] f011 = i01.faces[dir * 2 + 1] f100 = i10.faces[dir * 2] f101 = i10.faces[dir * 2 + 1] f110 = i11.faces[dir * 2] f111 = i11.faces[dir * 2 + 1] w1 = ((f001.location[dir] - locations[i, dir])/ (f001.location[dir] - f000.location[dir])) if i00.index != i01.index: w2 = ((f010.location[dir1] - locations[i, dir1])/ (f010.location[dir1] - f000.location[dir1])) else: w2 = 1.0 if i10.index != i00.index: w3 = ((f100.location[dir2] - locations[i, dir2])/ (f100.location[dir2] - f000.location[dir2])) else: w3 = 1.0 w1 = _clip01(w1) w2 = _clip01(w2) w3 = _clip01(w3) row_data[0] = w1 * w2 * w3 row_data[1] = (1 - w1) * w2 * w3 row_data[2] = w1 * (1 - w2) * w3 row_data[3] = (1 - w1) * (1 - w2) * w3 row_data[4] = w1 * w2 * (1 - w3) row_data[5] = (1 - w1) * w2 * (1 - w3) row_data[6] = w1 * (1 - w2) * (1 - w3) row_data[7] = (1 - w1) * (1 - w2) * (1 - w3) row_inds[0] = f000.index + offset row_inds[1] = f001.index + offset row_inds[2] = f010.index + offset row_inds[3] = f011.index + offset row_inds[4] = f100.index + offset row_inds[5] = f101.index + offset row_inds[6] = f110.index + offset row_inds[7] = f111.index + offset Rf = self._deflate_faces() return sp.csr_matrix((data, indices, indptr), shape=(locs.shape[0], self.n_total_faces))*Rf def _getNodeIntMat(self, locs, zeros_outside): cdef: double[:, :] locations = locs int_t dim = self._dim int_t n_loc = locs.shape[0] int_t n_nodes = 1<1 + eps or wy > 1 + eps or wz > 1 + eps): for ii in range(n_nodes): J[n_nodes*i + ii] = 0 V[n_nodes*i + ii] = 0.0 continue wx = _clip01(wx) wy = _clip01(wy) wz = _clip01(wz) for ii in range(n_nodes): J[n_nodes*i + ii] = cell.points[ii].index V[n_nodes*i ] = wx*wy*wz V[n_nodes*i + 1] = (1 - wx)*wy*wz V[n_nodes*i + 2] = wx*(1 - wy)*wz V[n_nodes*i + 3] = (1 - wx)*(1 - wy)*wz if dim==3: V[n_nodes*i + 4] = wx*wy*(1 - wz) V[n_nodes*i + 5] = (1 - wx)*wy*(1 - wz) V[n_nodes*i + 6] = wx*(1 - wy)*(1 - wz) V[n_nodes*i + 7] = (1 - wx)*(1 - wy)*(1 - wz) Rn = self._deflate_nodes() return sp.csr_matrix((V, (I, J)), shape=(locs.shape[0],self.n_total_nodes))*Rn def _getCellIntMat(self, locs, zeros_outside): cdef: double[:, :] locations = locs int_t dim = self._dim int_t dir0, dir1, dir2, temp int_t n_loc = locations.shape[0] int_t n_cells = 4 if dim == 2 else 8 np.int64_t[:] indptr = n_cells * np.arange(n_loc+1, dtype=np.int64) np.int64_t[:] indices = np.empty(n_cells * n_loc, dtype=np.int64) np.float64_t[:] data = np.ones(n_cells * n_loc, dtype=np.float64) np.int64_t[:] row_inds np.float64_t[:] row_data np.float64_t w0, w1, w2 int_t ii, i c_Cell *i000 c_Cell *i001 c_Cell *i010 c_Cell *i011 c_Cell *i100 c_Cell *i101 c_Cell *i110 c_Cell *i111 c_Cell *cell double x, y, z double eps = 100*np.finfo(float).eps int zeros_out = zeros_outside dir0 = 0 dir1 = 1 dir2 = 2 for i in range(n_loc): x = locations[i, 0] y = locations[i, 1] z = locations[i, 2] if dim==3 else 0.0 # get containing (or closest) cell cell = self.tree.containing_cell(x, y, z) row_inds = indices[indptr[i]:indptr[i + 1]] row_data = data[indptr[i]:indptr[i + 1]] was_outside = False if zeros_out: if x < cell.points[0].location[0]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif x > cell.points[3].location[0]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif y < cell.points[0].location[1]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif y > cell.points[3].location[1]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif dim == 3 and z < cell.points[0].location[2]-eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True elif dim == 3 and z > cell.points[7].location[2]+eps: row_data[:] = 0.0 row_inds[:] = 0 was_outside = True if not was_outside: # decide order to search based on distance to each faces # if ( abs(locations[i, dir0] - cell.location[dir0]) < abs(locations[i, dir1] - cell.location[dir1]) ): temp = dir0 dir0 = dir1 dir1 = temp if dim == 3: if ( abs(locations[i, dir1] - cell.location[dir1]) < abs(locations[i, dir2] - cell.location[dir2]) ): temp = dir1 dir1 = dir2 dir2 = temp if ( abs(locations[i, dir0] - cell.location[dir0]) < abs(locations[i, dir1] - cell.location[dir1]) ): temp = dir0 dir0 = dir1 dir0 = temp # Look -dir0 and +dir0 from current cell if ( locations[i, dir0] < cell.location[dir0] and cell.neighbors[2 * dir0] != NULL and cell.neighbors[2 * dir0].is_leaf() and cell.neighbors[2 * dir0].level == cell.level ): i000 = cell.neighbors[2 * dir0] i001 = cell elif ( locations[i, dir0] > cell.location[dir0] and cell.neighbors[2 * dir0 + 1] != NULL and cell.neighbors[2 * dir0 + 1].is_leaf() and cell.neighbors[2 * dir0 + 1].level == cell.level ): i000 = cell i001 = cell.neighbors[2 * dir0 + 1] else: i000 = i001 = cell # Look -y and +y from previous two cells if ( locations[i, dir1] < cell.location[dir1] and i000.neighbors[2 * dir1] != NULL and i001.neighbors[2 * dir1] != NULL and i000.neighbors[2 * dir1].is_leaf() and i001.neighbors[2 * dir1].is_leaf() and i000.neighbors[2 * dir1].level == i000.level and i001.neighbors[2 * dir1].level == i001.level ): i010 = i000 i011 = i001 i000 = i000.neighbors[2 * dir1] i001 = i001.neighbors[2 * dir1] elif ( locations[i, dir1] > cell.location[dir1] and i000.neighbors[2 * dir1 + 1] != NULL and i001.neighbors[2 * dir1 + 1] != NULL and i000.neighbors[2 * dir1 + 1].is_leaf() and i001.neighbors[2 * dir1 + 1].is_leaf() and i000.neighbors[2 * dir1 + 1].level == i000.level and i001.neighbors[2 * dir1 + 1].level == i001.level ): i010 = i000.neighbors[2 * dir1 + 1] i011 = i001.neighbors[2 * dir1 + 1] else: i010 = i000 i011 = i001 w1 = 1.0 # Look -z and +z from previous four cells if ( dim == 3 and locations[i, dir2] < cell.location[dir2] and i000.neighbors[2 * dir2] != NULL and i001.neighbors[2 * dir2] != NULL and i010.neighbors[2 * dir2] != NULL and i011.neighbors[2 * dir2] != NULL and i000.neighbors[2 * dir2].is_leaf() and i001.neighbors[2 * dir2].is_leaf() and i010.neighbors[2 * dir2].is_leaf() and i011.neighbors[2 * dir2].is_leaf() and i000.neighbors[2 * dir2].level == i000.level and i001.neighbors[2 * dir2].level == i001.level and i010.neighbors[2 * dir2].level == i010.level and i011.neighbors[2 * dir2].level == i011.level ): i100 = i000 i101 = i001 i110 = i010 i111 = i011 i000 = i000.neighbors[2 * dir2] i001 = i001.neighbors[2 * dir2] i010 = i010.neighbors[2 * dir2] i011 = i011.neighbors[2 * dir2] elif ( dim == 3 and locations[i, dir2] > cell.location[dir2] and i000.neighbors[2 * dir2 + 1] != NULL and i001.neighbors[2 * dir2 + 1] != NULL and i010.neighbors[2 * dir2 + 1] != NULL and i011.neighbors[2 * dir2 + 1] != NULL and i000.neighbors[2 * dir2 + 1].is_leaf() and i001.neighbors[2 * dir2 + 1].is_leaf() and i010.neighbors[2 * dir2 + 1].is_leaf() and i011.neighbors[2 * dir2 + 1].is_leaf() and i000.neighbors[2 * dir2 + 1].level == i000.level and i001.neighbors[2 * dir2 + 1].level == i001.level and i010.neighbors[2 * dir2 + 1].level == i010.level and i011.neighbors[2 * dir2 + 1].level == i011.level ): i100 = i000.neighbors[2 * dir2 + 1] i101 = i001.neighbors[2 * dir2 + 1] i110 = i010.neighbors[2 * dir2 + 1] i111 = i011.neighbors[2 * dir2 + 1] else: i100 = i000 i101 = i001 i110 = i010 i111 = i011 if i001.index != i000.index: w1 = ((i001.location[dir0] - locations[i, dir0])/ (i001.location[dir0] - i000.location[dir0])) else: w1 = 1.0 if i010.index != i000.index: w2 = ((i010.location[dir1] - locations[i, dir1])/ (i010.location[dir1] - i000.location[dir1])) else: w2 = 1.0 if dim == 3 and i100.index != i000.index: w3 = ((i100.location[dir2] - locations[i, dir2])/ (i100.location[dir2] - i000.location[dir2])) else: w3 = 1.0 w1 = _clip01(w1) w2 = _clip01(w2) w3 = _clip01(w3) row_data[0] = w1 * w2 * w3 row_data[1] = (1 - w1) * w2 * w3 row_data[2] = w1 * (1 - w2) * w3 row_data[3] = (1 - w1) * (1 - w2) * w3 row_inds[0] = i000.index row_inds[1] = i001.index row_inds[2] = i010.index row_inds[3] = i011.index if dim==3: row_data[4] = w1 * w2 * (1 - w3) row_data[5] = (1 - w1) * w2 * (1 - w3) row_data[6] = w1 * (1 - w2) * (1 - w3) row_data[7] = (1 - w1) * (1 - w2) * (1 - w3) row_inds[4] = i100.index row_inds[5] = i101.index row_inds[6] = i110.index row_inds[7] = i111.index return sp.csr_matrix((data, indices, indptr), shape=(locs.shape[0],self.n_cells)) @property def cell_nodes(self): """The index of all nodes for each cell. These indices point to non-hanging and hanging nodes. Returns ------- numpy.ndarray of int Index array of shape (n_cells, 4) if 2D, or (n_cells, 8) if 3D See also -------- TreeMesh.total_nodes """ cdef int_t npc = 4 if self.dim == 2 else 8 inds = np.empty((self.n_cells, npc), dtype=np.int64) cdef np.int64_t[:, :] node_index = inds cdef int_t i for cell in self.tree.cells: for i in range(npc): node_index[cell.index, i] = cell.points[i].index return inds @property def edge_nodes(self): """The index of nodes for every edge. The index of the nodes at each end of every (including hanging) edge. Returns ------- (dim) tuple of numpy.ndarray of int One numpy array for each edge type (x, y, (z)) for this mesh. Notes ----- These arrays will also index into the hanging nodes. """ inds_x = np.empty((self.n_total_edges_x, 2), dtype=np.int64) inds_y = np.empty((self.n_total_edges_y, 2), dtype=np.int64) cdef np.int64_t[:, :] edge_inds edge_inds = inds_x for it in self.tree.edges_x: edge = it.second edge_inds[edge.index, 0] = edge.points[0].index edge_inds[edge.index, 1] = edge.points[1].index edge_inds = inds_y for it in self.tree.edges_y: edge = it.second edge_inds[edge.index, 0] = edge.points[0].index edge_inds[edge.index, 1] = edge.points[1].index if self.dim == 2: return inds_x, inds_y inds_z = np.empty((self.n_total_edges_z, 2), dtype=np.int64) edge_inds = inds_z for it in self.tree.edges_z: edge = it.second edge_inds[edge.index, 0] = edge.points[0].index edge_inds[edge.index, 1] = edge.points[1].index return inds_x, inds_y, inds_z def __getstate__(self): """Get the current state of the TreeMesh.""" cdef int id, dim = self._dim indArr = np.empty((self.n_cells, dim), dtype=np.int64) levels = np.empty((self.n_cells), dtype=np.int32) cdef np.int64_t[:, :] _indArr = indArr cdef np.int32_t[:] _levels = levels for cell in self.tree.cells: for id in range(dim): _indArr[cell.index, id] = cell.location_ind[id] _levels[cell.index] = cell.level return indArr, levels def __setstate__(self, state): """Set the current state of the TreeMesh.""" indArr, levels = state indArr = np.asarray(indArr) levels = np.asarray(levels) xs = np.array(self._xs) ys = np.array(self._ys) if self._dim == 3: zs = np.array(self._zs) points = np.column_stack((xs[indArr[:, 0]], ys[indArr[:, 1]], zs[indArr[:, 2]])) else: points = np.column_stack((xs[indArr[:, 0]], ys[indArr[:, 1]])) # Set diagonal balance as false. If the state itself came from a diagonally # balanced tree, those cells will naturally be included in the state information # itself (no need to re-enforce that balancing). This then also allows # us to support reading in older TreeMesh that are not diagonally balanced when # we switch the default to be a diagonally balanced tree. self.insert_cells(points, levels, diagonal_balance=False) def __getitem__(self, key): """Get a TreeCell or cells. Each item of the TreeMesh is a TreeCell that contains information about the Returns ------- discretize.tree_mesh.TreeCell """ if isinstance(key, slice): # Get the start, stop, and step from the slice return [self[ii] for ii in range(*key.indices(len(self)))] elif isinstance(key, (int, np.integer)): if key < 0: # Handle negative indices key += len(self) if key >= len(self): raise IndexError( "The index ({0:d}) is out of range.".format(key) ) pycell = TreeCell() pycell._set(self.tree.cells[key]) return pycell else: raise TypeError("Invalid argument type.") @property def _ubc_indArr(self): if self.__ubc_indArr is not None: return self.__ubc_indArr indArr, levels = self.__getstate__() max_level = self.tree.max_level levels = 1<<(max_level - levels) if self.dim == 2: indArr[:, -1] = (self._ys.shape[0]-1) - indArr[:, -1] else: indArr[:, -1] = (self._zs.shape[0]-1) - indArr[:, -1] indArr = (indArr - levels[:, None])//2 indArr += 1 self.__ubc_indArr = (indArr, levels) return self.__ubc_indArr @property def _ubc_order(self): if self.__ubc_order is not None: return self.__ubc_order indArr, _ = self._ubc_indArr if self.dim == 2: self.__ubc_order = np.lexsort((indArr[:, 0], indArr[:, 1])) else: self.__ubc_order = np.lexsort((indArr[:, 0], indArr[:, 1], indArr[:, 2])) return self.__ubc_order def __dealloc__(self): del self.tree del self.wrapper @cython.boundscheck(False) @cython.cdivision(True) def _vol_avg_from_tree(self, _TreeMesh meshin, values=None, output=None): # first check if they have the same tensor base, as it makes it a lot easier... cdef int_t same_base try: same_base = ( np.allclose(self.nodes_x, meshin.nodes_x) and np.allclose(self.nodes_y, meshin.nodes_y) and (self.dim == 2 or np.allclose(self.nodes_z, meshin.nodes_z)) ) except ValueError: same_base = False cdef c_Cell * out_cell cdef c_Cell * in_cell cdef np.float64_t[:] vals cdef np.float64_t[:] outs cdef int_t build_mat = 1 if values is not None: vals = values if output is None: output = np.empty(self.n_cells, dtype=np.float64) else: output = np.require(output, dtype=np.float64, requirements=['A', 'W']) output[:] = 0 outs = output build_mat = 0 cdef vector[int_t] row_inds, col_inds cdef vector[int_t] indptr cdef vector[double] all_weights cdef vector[int_t] *overlapping_cells cdef double *weights cdef double over_lap_vol cdef double x1m[3] cdef double x1p[3] cdef double x2m[3] cdef double x2p[33] cdef double[:] origin = meshin._origin cdef double[:] xF if self.dim == 2: xF = np.array([meshin._xs[-1], meshin._ys[-1]]) else: xF = np.array([meshin._xs[-1], meshin._ys[-1], meshin._zs[-1]]) cdef int_t nnz_counter = 0 cdef int_t nnz_row = 0 if build_mat: indptr.push_back(0) cdef int_t i, in_cell_ind cdef int_t n_overlap cdef double weight_sum cdef double weight cdef vector[int_t] out_visited cdef int_t n_unvisited # easier path if they share the same base: if same_base: if build_mat: all_weights.resize(meshin.n_cells, 0.0) row_inds.resize(meshin.n_cells, 0) out_visited.resize(self.n_cells, 0) for in_cell in meshin.tree.cells: # for each input cell find containing output cell out_cell = self.tree.containing_cell( in_cell.location[0], in_cell.location[1], in_cell.location[2] ) # if containing output cell is lower level (larger) than input cell: # contribution is related to difference of levels (aka ratio of volumes) # else: # contribution is 1.0 if out_cell.level < in_cell.level: out_visited[out_cell.index] = 1 weight = in_cell.volume/out_cell.volume if not build_mat: outs[out_cell.index] += weight*vals[in_cell.index] else: all_weights[in_cell.index] = weight row_inds[in_cell.index] = out_cell.index if build_mat: P = sp.csr_matrix((all_weights, (row_inds, np.arange(meshin.n_cells))), shape=(self.n_cells, meshin.n_cells)) n_unvisited = self.n_cells - np.sum(out_visited) row_inds.resize(n_unvisited, 0) col_inds.resize(n_unvisited, 0) i = 0 # assign weights of 1 to unvisited output cells and find their containing cell for out_cell in self.tree.cells: if not out_visited[out_cell.index]: in_cell = meshin.tree.containing_cell( out_cell.location[0], out_cell.location[1], out_cell.location[2] ) if not build_mat: outs[out_cell.index] = vals[in_cell.index] else: row_inds[i] = out_cell.index col_inds[i] = in_cell.index i += 1 if build_mat and n_unvisited > 0: P += sp.csr_matrix( (np.ones(n_unvisited), (row_inds, col_inds)), shape=(self.n_cells, meshin.n_cells) ) if not build_mat: return output return P cdef int_t last_point_ind = 7 if self._dim==3 else 3 for cell in self.tree.cells: for i_d in range(self._dim): x1m[i_d] = min(cell.min_node().location[i_d], xF[i_d]) x1p[i_d] = max(cell.max_node().location[i_d], origin[i_d]) box = geom.Box(self._dim, x1m, x1p) overlapping_cell_inds = meshin.tree.find_cells_geom(box) n_overlap = overlapping_cell_inds.size() weights = malloc(n_overlap*sizeof(double)) i = 0 weight_sum = 0.0 nnz_row = 0 for in_cell_ind in overlapping_cell_inds: in_cell = meshin.tree.cells[in_cell_ind] x2m = in_cell.min_node().location x2p = in_cell.max_node().location over_lap_vol = 1.0 for i_d in range(self._dim): if x1m[i_d]< xF[i_d] and x1p[i_d] > origin[i_d]: over_lap_vol *= min(x1p[i_d], x2p[i_d]) - max(x1m[i_d], x2m[i_d]) weights[i] = over_lap_vol if build_mat and weights[i] != 0.0: nnz_row += 1 row_inds.push_back(in_cell_ind) weight_sum += weights[i] i += 1 if weight_sum > 0: for i in range(n_overlap): weights[i] /= weight_sum if build_mat and weights[i] != 0.0: all_weights.push_back(weights[i]) if not build_mat: for i in range(n_overlap): outs[cell.index] += vals[overlapping_cell_inds[i]]*weights[i] else: nnz_counter += nnz_row indptr.push_back(nnz_counter) free(weights) overlapping_cell_inds.clear() if not build_mat: return output return sp.csr_matrix((all_weights, row_inds, indptr), shape=(self.n_cells, meshin.n_cells)) @cython.boundscheck(False) @cython.cdivision(True) def _vol_avg_to_tens(self, out_tens_mesh, values=None, output=None): cdef vector[int_t] *overlapping_cells cdef double *weights cdef double over_lap_vol cdef double x1m[3] cdef double x1p[3] cdef double x2m[3] cdef double x2p[3] cdef double[:] origin cdef double[:] xF # first check if they have the same tensor base, as it makes it a lot easier... cdef int_t same_base try: same_base = ( np.allclose(self.nodes_x, out_tens_mesh.nodes_x) and np.allclose(self.nodes_y, out_tens_mesh.nodes_y) and (self.dim == 2 or np.allclose(self.nodes_z, out_tens_mesh.nodes_z)) ) except ValueError: same_base = False if same_base: in_cell_inds = self.get_containing_cells(out_tens_mesh.cell_centers) # Every cell input cell is gauranteed to be a lower level than the output tenser mesh # therefore all weights a 1.0 if values is not None: if output is None: output = np.empty(out_tens_mesh.n_cells) output[:] = values[in_cell_inds] return output return sp.csr_matrix( (np.ones(out_tens_mesh.n_cells), (np.arange(out_tens_mesh.n_cells), in_cell_inds)), shape=(out_tens_mesh.n_cells, self.n_cells) ) if self.dim == 2: origin = np.r_[self.origin, 0.0] xF = np.array([self._xs[-1], self._ys[-1], 0.0]) else: origin = self._origin xF = np.array([self._xs[-1], self._ys[-1], self._zs[-1]]) cdef c_Cell * in_cell cdef np.float64_t[:] vals = np.array([]) cdef np.float64_t[::1, :, :] outs = np.array([[[]]]) cdef vector[int_t] row_inds cdef vector[int_t] indptr cdef vector[double] all_weights cdef int_t nnz_row = 0 cdef int_t nnz_counter = 0 cdef double[:] nodes_x = out_tens_mesh.nodes_x cdef double[:] nodes_y = out_tens_mesh.nodes_y cdef double[:] nodes_z = np.array([0.0, 0.0]) if self._dim==3: nodes_z = out_tens_mesh.nodes_z cdef int_t nx = len(nodes_x)-1 cdef int_t ny = len(nodes_y)-1 cdef int_t nz = len(nodes_z)-1 cdef int_t build_mat = 1 if values is not None: vals = values if output is None: output = np.empty(out_tens_mesh.n_cells, dtype=np.float64) else: output = np.require(output, dtype=np.float64, requirements=['A', 'W']) output[:] = 0 outs = output.reshape((nx, ny, nz), order='F') build_mat = 0 if build_mat: indptr.push_back(0) cdef int_t ix, iy, iz, in_cell_ind, i, i_dim cdef int_t n_overlap cdef double weight_sum #for cell in self.tree.cells: for iz in range(nz): x1m[2] = min(nodes_z[iz], xF[2]) x1p[2] = max(nodes_z[iz+1], origin[2]) for iy in range(ny): x1m[1] = min(nodes_y[iy], xF[1]) x1p[1] = max(nodes_y[iy+1], origin[1]) for ix in range(nx): x1m[0] = min(nodes_x[ix], xF[0]) x1p[0] = max(nodes_x[ix+1], origin[0]) box = geom.Box(self._dim, x1m, x1p) overlapping_cell_inds = self.tree.find_cells_geom(box) n_overlap = overlapping_cell_inds.size() weights = malloc(n_overlap*sizeof(double)) i = 0 weight_sum = 0.0 nnz_row = 0 for in_cell_ind in overlapping_cell_inds: in_cell = self.tree.cells[in_cell_ind] x2m = in_cell.min_node().location x2p = in_cell.max_node().location over_lap_vol = 1.0 for i_d in range(self._dim): if x1m[i_d]< xF[i_d] and x1p[i_d] > origin[i_d]: over_lap_vol *= min(x1p[i_d], x2p[i_d]) - max(x1m[i_d], x2m[i_d]) weights[i] = over_lap_vol if build_mat and weights[i] != 0.0: nnz_row += 1 row_inds.push_back(in_cell_ind) weight_sum += weights[i] i += 1 if weight_sum > 0: for i in range(n_overlap): weights[i] /= weight_sum if build_mat and weights[i] != 0.0: all_weights.push_back(weights[i]) if not build_mat: for i in range(n_overlap): outs[ix, iy, iz] += vals[overlapping_cell_inds[i]]*weights[i] else: nnz_counter += nnz_row indptr.push_back(nnz_counter) free(weights) overlapping_cell_inds.clear() if not build_mat: return output return sp.csr_matrix((all_weights, row_inds, indptr), shape=(out_tens_mesh.n_cells, self.n_cells)) @cython.boundscheck(False) @cython.cdivision(True) def _vol_avg_from_tens(self, in_tens_mesh, values=None, output=None): cdef double *weights cdef double over_lap_vol cdef double x1m, x1p, y1m, y1p, z1m, z1p cdef double x2m, x2p, y2m, y2p, z2m, z2p cdef int_t ix, ix1, ix2, iy, iy1, iy2, iz, iz1, iz2 cdef double[:] origin = in_tens_mesh.origin cdef double[:] xF # first check if they have the same tensor base, as it makes it a lot easier... cdef int_t same_base try: same_base = ( np.allclose(self.nodes_x, in_tens_mesh.nodes_x) and np.allclose(self.nodes_y, in_tens_mesh.nodes_y) and (self.dim == 2 or np.allclose(self.nodes_z, in_tens_mesh.nodes_z)) ) except ValueError: same_base = False if same_base: out_cell_inds = self.get_containing_cells(in_tens_mesh.cell_centers) ws = in_tens_mesh.cell_volumes/self.cell_volumes[out_cell_inds] if values is not None: if output is None: output = np.empty(self.n_cells) output[:] = np.bincount(out_cell_inds, ws*values) return output return sp.csr_matrix( (ws, (out_cell_inds, np.arange(in_tens_mesh.n_cells))), shape=(self.n_cells, in_tens_mesh.n_cells) ) cdef np.float64_t[:] nodes_x = in_tens_mesh.nodes_x cdef np.float64_t[:] nodes_y = in_tens_mesh.nodes_y cdef np.float64_t[:] nodes_z = np.array([0.0, 0.0]) if self._dim == 3: nodes_z = in_tens_mesh.nodes_z cdef int_t nx = len(nodes_x)-1 cdef int_t ny = len(nodes_y)-1 cdef int_t nz = len(nodes_z)-1 cdef double * dx cdef double * dy cdef double * dz if self.dim == 2: xF = np.array([nodes_x[-1], nodes_y[-1]]) else: xF = np.array([nodes_x[-1], nodes_y[-1], nodes_z[-1]]) cdef np.float64_t[::1, :, :] vals cdef np.float64_t[:] outs cdef int_t build_mat = 1 if values is not None: vals = values.reshape((nx, ny, nz), order='F') if output is None: output = np.empty(self.n_cells, dtype=np.float64) else: output = np.require(output, dtype=np.float64, requirements=['A', 'W']) output[:] = 0 outs = output build_mat = 0 cdef vector[int_t] row_inds cdef vector[double] all_weights cdef np.int64_t[:] indptr = np.zeros(self.n_cells+1, dtype=np.int64) cdef int_t nnz_counter = 0 cdef int_t nnz_row = 0 cdef int_t nx_overlap, ny_overlap, nz_overlap, n_overlap cdef int_t i cdef double weight_sum for cell in self.tree.cells: x1m = min(cell.points[0].location[0], xF[0]) y1m = min(cell.points[0].location[1], xF[1]) x1p = max(cell.points[3].location[0], origin[0]) y1p = max(cell.points[3].location[1], origin[1]) if self._dim==3: z1m = min(cell.points[0].location[2], xF[2]) z1p = max(cell.points[7].location[2], origin[2]) # then need to find overlapping cells of TensorMesh... ix1 = max(_bisect_left(nodes_x, x1m) - 1, 0) ix2 = min(_bisect_right(nodes_x, x1p), nx) iy1 = max(_bisect_left(nodes_y, y1m) - 1, 0) iy2 = min(_bisect_right(nodes_y, y1p), ny) if self._dim==3: iz1 = max(_bisect_left(nodes_z, z1m) - 1, 0) iz2 = min(_bisect_right(nodes_z, z1p), nz) else: iz1 = 0 iz2 = 1 nx_overlap = ix2-ix1 ny_overlap = iy2-iy1 nz_overlap = iz2-iz1 n_overlap = nx_overlap*ny_overlap*nz_overlap weights = malloc(n_overlap*sizeof(double)) dx = malloc(nx_overlap*sizeof(double)) for ix in range(ix1, ix2): x2m = nodes_x[ix] x2p = nodes_x[ix+1] if x1m == xF[0] or x1p == origin[0]: dx[ix-ix1] = 1.0 else: dx[ix-ix1] = min(x1p, x2p) - max(x1m, x2m) dy = malloc(ny_overlap*sizeof(double)) for iy in range(iy1, iy2): y2m = nodes_y[iy] y2p = nodes_y[iy+1] if y1m == xF[1] or y1p == origin[1]: dy[iy-iy1] = 1.0 else: dy[iy-iy1] = min(y1p, y2p) - max(y1m, y2m) dz = malloc(nz_overlap*sizeof(double)) for iz in range(iz1, iz2): z2m = nodes_z[iz] z2p = nodes_z[iz+1] if self._dim==3: if z1m == xF[2] or z1p == origin[2]: dz[iz-iz1] = 1.0 else: dz[iz-iz1] = min(z1p, z2p) - max(z1m, z2m) else: dz[iz-iz1] = 1.0 i = 0 weight_sum = 0.0 nnz_row = 0 for iz in range(iz1, iz2): for iy in range(iy1, iy2): for ix in range(ix1, ix2): in_cell_ind = ix + (iy + iz*ny)*nx weights[i] = dx[ix-ix1]*dy[iy-iy1]*dz[iz-iz1] if build_mat and weights[i] != 0.0: nnz_row += 1 row_inds.push_back(in_cell_ind) weight_sum += weights[i] i += 1 for i in range(n_overlap): weights[i] /= weight_sum if build_mat and weights[i] != 0.0: all_weights.push_back(weights[i]) if not build_mat: i = 0 for iz in range(iz1, iz2): for iy in range(iy1, iy2): for ix in range(ix1, ix2): outs[cell.index] += vals[ix, iy, iz]*weights[i] i += 1 else: nnz_counter += nnz_row indptr[cell.index+1] = nnz_counter free(weights) free(dx) free(dy) free(dz) if not build_mat: return output return sp.csr_matrix((all_weights, row_inds, indptr), shape=(self.n_cells, in_tens_mesh.n_cells)) def get_overlapping_cells(self, rectangle): """Find the indicis of cells that overlap the given rectangle Parameters ---------- rectangle: (2 * dim) array_like array ordered ``[x_min, x_max, y_min, y_max, (z_min, z_max)]`` describing the axis aligned rectangle of interest. Returns ------- list of int The indices of cells which overlap the axis aligned rectangle. """ return self.get_cells_in_aabb(*rectangle.reshape(self.dim, 2).T) def _error_if_not_finalized(self, method: str): """ Raise error if mesh is not finalized. """ if not self.finalized: msg = ( f"`{type(self).__name__}.{method}` requires a finalized mesh. " "Use the `finalize()` method to finalize it." ) raise TreeMeshNotFinalizedError(msg) def _require_ndarray_with_dim(self, name, arr, ndim=1, dtype=None, requirements=None): """Returns an ndarray that has dim along it's last dimension, with ndim dims, Parameters ---------- name : str name of the parameter for raised error arr : array_like ndim : {1, 2, 3} dtype, optional dtype input to np.requires requirements, optional requirements input to np.requires, defaults to 'C'. Returns ------- numpy.ndarray validated array """ if requirements is None: requirements = 'C' if ndim == 1: arr = np.atleast_1d(arr) elif ndim > 1: arr = np.atleast_2d(arr) if ndim == 3 and arr.ndim != 3: arr = arr[None, ...] else: arr = np.asarray(arr) if arr.ndim != ndim: raise ValueError(f"{name} must have at most {ndim} dimensions.") if arr.shape[-1] != self.dim: raise ValueError( f"Expected the last dimension of {name}.shape={arr.shape} to be {self.dim}." ) return np.require(arr, dtype=dtype, requirements=requirements) def _check_first_dim_broadcast(**kwargs): """Perform a check to make sure that the first dimensions of the inputs will broadcast.""" n_items = 1 err = False for key, arr in kwargs.items(): test_len = arr.shape[0] if test_len != 1: if n_items == 1: n_items = test_len elif test_len != n_items: err = True break if err: message = "First dimensions of" for key, arr in kwargs.items(): message += f" {key}: {arr.shape}," message = message[:-1] message += " do not broadcast." raise ValueError(message) return n_items cdef inline double _clip01(double x) nogil: return min(1, max(x, 0)) cdef inline int _wrap_levels(int l, int max_level): if l < 0: l = (max_level + 1) - (abs(l) % (max_level + 1)) return l ================================================ FILE: discretize/_extensions/triplet.h ================================================ #ifndef __TRIPLET_H #define __TRIPLET_H #include #include #include template struct triplet{ T v1{}; U v2{}; V v3{}; triplet(){} triplet(T first, U second, V third){ v1 = first; v2 = second; v3 = third; } bool operator==(const triplet &other) const{ return (v1 == other.v1 && v2 == other.v2 && v3 == other.v3); } bool operator<(const triplet &other) const{ return ! ( (v1 > other.v1) || (v2 > other.v2) || (v3 > other.v3) ); } }; namespace std { template struct hash > { std::size_t operator()(const triplet& k) const { using std::hash; // Compute individual hash values for first, // second and third and combine them using XOR // and bit shifting: return ((hash()(k.v1) ^ (hash()(k.v2) << 1)) >> 1) ^ (hash()(k.v3) << 1); } }; template struct hash > { size_t operator()(const pair& k) const { using std::hash; return ((hash()(k.first) ^ (hash()(k.second) << 1)) >> 1); } }; } #endif ================================================ FILE: discretize/base/__init__.py ================================================ """ ================================== Base Mesh (:mod:`discretize.base`) ================================== .. currentmodule:: discretize.base The ``base`` sub-package houses the fundamental classes for all meshes in ``discretize``. Base Mesh Class --------------- .. autosummary:: :toctree: generated/ BaseMesh BaseRegularMesh BaseRectangularMesh BaseTensorMesh """ from discretize.base.base_mesh import BaseMesh from discretize.base.base_regular_mesh import BaseRegularMesh, BaseRectangularMesh from discretize.base.base_tensor_mesh import BaseTensorMesh ================================================ FILE: discretize/base/base_mesh.py ================================================ """Module for the base ``discretize`` mesh.""" import numpy as np import scipy.sparse as sp import os import json from scipy.spatial import KDTree from discretize.utils import is_scalar, mkvc, sdiag, sdinv from discretize.utils.code_utils import ( deprecate_property, deprecate_method, as_array_n_by_dim, ) class BaseMesh: """ Base mesh class for the ``discretize`` package. This class contains the basic structure of properties and methods that should be supported on all discretize meshes. """ _aliases = { "nC": "n_cells", "nN": "n_nodes", "nE": "n_edges", "nF": "n_faces", "serialize": "to_dict", "gridCC": "cell_centers", "gridN": "nodes", "aveF2CC": "average_face_to_cell", "aveF2CCV": "average_face_to_cell_vector", "aveCC2F": "average_cell_to_face", "aveCCV2F": "average_cell_vector_to_face", "aveE2CC": "average_edge_to_cell", "aveE2CCV": "average_edge_to_cell_vector", "aveN2CC": "average_node_to_cell", "aveN2E": "average_node_to_edge", "aveN2F": "average_node_to_face", } def __getattr__(self, name): """Reimplement get attribute to allow for aliases.""" if name == "_aliases": raise AttributeError name = self._aliases.get(name, name) return super().__getattribute__(name) def to_dict(self): """Represent the mesh's attributes as a dictionary. The dictionary representation of the mesh class necessary to reconstruct the object. This is useful for serialization. All of the attributes returned in this dictionary will be JSON serializable. The mesh class is also stored in the dictionary as strings under the `__module__` and `__class__` keys. Returns ------- dict Dictionary of {attribute: value} for the attributes of this mesh. """ cls = type(self) out = { "__module__": cls.__module__, "__class__": cls.__name__, } for item in self._items: attr = getattr(self, item, None) if attr is not None: if isinstance(attr, np.ndarray): attr = attr.tolist() elif isinstance(attr, tuple): # change to a list and make sure inner items are not numpy arrays attr = list(attr) for i, thing in enumerate(attr): if isinstance(thing, np.ndarray): attr[i] = thing.tolist() out[item] = attr return out def equals(self, other_mesh): """Compare the current mesh with another mesh to determine if they are identical. This method compares all the properties of the current mesh to *other_mesh* and determines if both meshes are identical. If so, this function returns a boolean value of *True* . Otherwise, the function returns *False* . Parameters ---------- other_mesh : discretize.base.BaseMesh An instance of any discretize mesh class. Returns ------- bool *True* if meshes are identical and *False* otherwise. """ if type(self) is not type(other_mesh): return False for item in self._items: my_attr = getattr(self, item, None) other_mesh_attr = getattr(other_mesh, item, None) if isinstance(my_attr, np.ndarray): is_equal = np.allclose(my_attr, other_mesh_attr, rtol=0, atol=0) elif isinstance(my_attr, tuple): is_equal = len(my_attr) == len(other_mesh_attr) if is_equal: for thing1, thing2 in zip(my_attr, other_mesh_attr): if isinstance(thing1, np.ndarray): is_equal = np.allclose(thing1, thing2, rtol=0, atol=0) else: try: is_equal = thing1 == thing2 except Exception: is_equal = False if not is_equal: return is_equal else: try: is_equal = my_attr == other_mesh_attr except Exception: is_equal = False if not is_equal: return is_equal return is_equal def serialize(self): """Represent the mesh's attributes as a dictionary. An alias for :py:meth:`~.BaseMesh.to_dict` See Also -------- to_dict """ return self.to_dict() @classmethod def deserialize(cls, items, **kwargs): """Create this mesh from a dictionary of attributes. Parameters ---------- items : dict dictionary of {attribute : value} pairs that will be passed to this class's initialization method as keyword arguments. **kwargs This is used to catch (and ignore) keyword arguments that used to be used. """ items.pop("__module__", None) items.pop("__class__", None) return cls(**items) def save(self, file_name="mesh.json", verbose=False, **kwargs): """Save the mesh to json. This method is used to save a mesh by writing its properties to a .json file. To load a mesh you have previously saved, see :py:func:`~discretize.utils.load_mesh`. Parameters ---------- file_name : str, optional File name for saving the mesh properties verbose : bool, optional If *True*, the path of the json file is printed """ if "filename" in kwargs: raise TypeError( "The filename keyword argument has been removed, please use file_name. " "This will be removed in discretize 1.0.0" ) f = os.path.abspath(file_name) # make sure we are working with abs path with open(f, "w") as outfile: json.dump(self.to_dict(), outfile) if verbose: print("Saved {}".format(f)) return f def copy(self): """Make a copy of the current mesh. Returns ------- type(mesh) A copy of this mesh. """ cls = type(self) items = self.to_dict() items.pop("__module__", None) items.pop("__class__", None) return cls(**items) def validate(self): """Return the validation state of the mesh. This mesh is valid immediately upon initialization Returns ------- bool : True """ return True # Counting dim, n_cells, n_nodes, n_edges, n_faces @property def dim(self): """The dimension of the mesh (1, 2, or 3). The dimension is an integer denoting whether the mesh is 1D, 2D or 3D. Returns ------- int Dimension of the mesh; i.e. 1, 2 or 3 """ raise NotImplementedError(f"dim not implemented for {type(self)}") @property def n_cells(self): """Total number of cells in the mesh. Returns ------- int Number of cells in the mesh Notes ----- Property also accessible as using the shorthand **nC** """ raise NotImplementedError(f"n_cells not implemented for {type(self)}") def __len__(self): """Total number of cells in the mesh. Essentially this is an alias for :py:attr:`~.BaseMesh.n_cells`. Returns ------- int Number of cells in the mesh """ return self.n_cells @property def n_nodes(self): """Total number of nodes in the mesh. Returns ------- int Number of nodes in the mesh Notes ----- Property also accessible as using the shorthand **nN** """ raise NotImplementedError(f"n_nodes not implemented for {type(self)}") @property def n_edges(self): """Total number of edges in the mesh. Returns ------- int Total number of edges in the mesh Notes ----- Property also accessible as using the shorthand **nE** """ raise NotImplementedError(f"n_edges not implemented for {type(self)}") @property def n_faces(self): """Total number of faces in the mesh. Returns ------- int Total number of faces in the mesh Notes ----- Property also accessible as using the shorthand **nF** """ raise NotImplementedError(f"n_faces not implemented for {type(self)}") # grid locations @property def cell_centers(self): """Return gridded cell center locations. This property returns a numpy array of shape (n_cells, dim) containing gridded cell center locations for all cells in the mesh. The cells are ordered along the x, then y, then z directions. Returns ------- (n_cells, dim) numpy.ndarray of float Gridded cell center locations Examples -------- The following is a 1D example. >>> from discretize import TensorMesh >>> hx = np.ones(5) >>> mesh_1D = TensorMesh([hx], '0') >>> mesh_1D.cell_centers array([0.5, 1.5, 2.5, 3.5, 4.5]) The following is a 3D example. >>> hx, hy, hz = np.ones(2), 2*np.ones(2), 3*np.ones(2) >>> mesh_3D = TensorMesh([hx, hy, hz], '000') >>> mesh_3D.cell_centers array([[0.5, 1. , 1.5], [1.5, 1. , 1.5], [0.5, 3. , 1.5], [1.5, 3. , 1.5], [0.5, 1. , 4.5], [1.5, 1. , 4.5], [0.5, 3. , 4.5], [1.5, 3. , 4.5]]) """ raise NotImplementedError(f"cell_centers not implemented for {type(self)}") @property def nodes(self): """Return gridded node locations. This property returns a numpy array of shape (n_nodes, dim) containing gridded node locations for all nodes in the mesh. The nodes are ordered along the x, then y, then z directions. Returns ------- (n_nodes, dim) numpy.ndarray of float Gridded node locations Examples -------- The following is a 1D example. >>> from discretize import TensorMesh >>> hx = np.ones(5) >>> mesh_1D = TensorMesh([hx], '0') >>> mesh_1D.nodes array([0., 1., 2., 3., 4., 5.]) The following is a 3D example. >>> hx, hy, hz = np.ones(2), 2*np.ones(2), 3*np.ones(2) >>> mesh_3D = TensorMesh([hx, hy, hz], '000') >>> mesh_3D.nodes array([[0., 0., 0.], [1., 0., 0.], [2., 0., 0.], [0., 2., 0.], [1., 2., 0.], [2., 2., 0.], [0., 4., 0.], [1., 4., 0.], [2., 4., 0.], [0., 0., 3.], [1., 0., 3.], [2., 0., 3.], [0., 2., 3.], [1., 2., 3.], [2., 2., 3.], [0., 4., 3.], [1., 4., 3.], [2., 4., 3.], [0., 0., 6.], [1., 0., 6.], [2., 0., 6.], [0., 2., 6.], [1., 2., 6.], [2., 2., 6.], [0., 4., 6.], [1., 4., 6.], [2., 4., 6.]]) """ raise NotImplementedError(f"nodes not implemented for {type(self)}") @property def boundary_nodes(self): """Boundary node locations. This property returns the locations of the nodes on the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary nodes by the dimension of the mesh. Returns ------- (n_boundary_nodes, dim) numpy.ndarray of float Boundary node locations """ raise NotImplementedError(f"boundary_nodes not implemented for {type(self)}") @property def faces(self): """Gridded face locations. This property returns a numpy array of shape (n_faces, dim) containing gridded locations for all faces in the mesh. For structued meshes, the first row corresponds to the bottom-front-leftmost x-face. The output array returns the x-faces, then the y-faces, then the z-faces; i.e. *mesh.faces* is equivalent to *np.r_[mesh.faces_x, mesh.faces_y, mesh.face_z]* . For each face type, the locations are ordered along the x, then y, then z directions. Returns ------- (n_faces, dim) numpy.ndarray of float Gridded face locations Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x and y-faces have normal vectors that are primarily along the x and y-directions, respectively. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> faces = mesh1.faces >>> x_faces = faces[:mesh1.n_faces_x] >>> y_faces = faces[mesh1.n_faces_x:] >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax1.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'g') >>> ax1.legend(['Mesh', 'X-faces', 'Y-faces'], fontsize=16) >>> plt.show() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-faces are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> faces = mesh2.faces >>> x_faces = faces[:mesh2.n_faces_x] >>> y_faces = faces[mesh2.n_faces_x:] >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax2.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'g') >>> ax2.legend(['Mesh', 'X-faces', 'Y-faces'], fontsize=16) >>> plt.show() """ raise NotImplementedError(f"faces not implemented for {type(self)}") @property def boundary_faces(self): """Boundary face locations. This property returns the locations of the faces on the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary faces by the dimension of the mesh. Returns ------- (n_boundary_faces, dim) numpy.ndarray of float Boundary faces locations """ raise NotImplementedError(f"boundary_faces not implemented for {type(self)}") @property def edges(self): """Gridded edge locations. This property returns a numpy array of shape (n_edges, dim) containing gridded locations for all edges in the mesh. For structured meshes, the first row corresponds to the bottom-front-leftmost x-edge. The output array returns the x-edges, then the y-edges, then the z-edges; i.e. *mesh.edges* is equivalent to *np.r_[mesh.edges_x, mesh.edges_y, mesh.edges_z]* . For each edge type, the locations are ordered along the x, then y, then z directions. Returns ------- (n_edges, dim) numpy.ndarray of float Gridded edge locations Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x and y-edges have normal vectors that are primarily along the x and y-directions, respectively. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> edges = mesh1.edges >>> x_edges = edges[:mesh1.n_edges_x] >>> y_edges = edges[mesh1.n_edges_x:] >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax1.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'g') >>> ax1.legend(['Mesh', 'X-edges', 'Y-edges'], fontsize=16) >>> plt.show() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-edges are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> edges = mesh2.edges >>> x_edges = edges[:mesh2.n_edges_x] >>> y_edges = edges[mesh2.n_edges_x:] >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax2.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'g') >>> ax2.legend(['Mesh', 'X-edges', 'Y-edges'], fontsize=16) >>> plt.show() """ raise NotImplementedError(f"edges not implemented for {type(self)}") @property def boundary_edges(self): """Boundary edge locations. This property returns the locations of the edges on the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary edges by the dimension of the mesh. Returns ------- (n_boundary_edges, dim) numpy.ndarray of float Boundary edge locations """ raise NotImplementedError(f"boundary_edges not implemented for {type(self)}") # unit directions @property def face_normals(self): """Unit normal vectors for all mesh faces. The unit normal vector defines the direction that is perpendicular to a surface. Calling *face_normals* returns a numpy.ndarray containing the unit normal vectors for all faces in the mesh. For a 3D mesh, the array would have shape (n_faces, dim). The rows of the output are organized by x-faces, then y-faces, then z-faces vectors. Returns ------- (n_faces, dim) numpy.ndarray of float Unit normal vectors for all mesh faces """ raise NotImplementedError(f"face_normals not implemented for {type(self)}") @property def edge_tangents(self): """Unit tangent vectors for all mesh edges. For a given edge, the unit tangent vector defines the path direction one would take if traveling along that edge. Calling *edge_tangents* returns a numpy.ndarray containing the unit tangent vectors for all edges in the mesh. For a 3D mesh, the array would have shape (n_edges, dim). The rows of the output are organized by x-edges, then y-edges, then z-edges vectors. Returns ------- (n_edges, dim) numpy.ndarray of float Unit tangent vectors for all mesh edges """ raise NotImplementedError(f"edge_tangents not implemented for {type(self)}") @property def boundary_face_outward_normals(self): """Outward normal vectors of boundary faces. This property returns the outward normal vectors of faces the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary faces by the dimension of the mesh. Returns ------- (n_boundary_faces, dim) numpy.ndarray of float Outward normal vectors of boundary faces """ raise NotImplementedError( f"boundary_face_outward_normals not implemented for {type(self)}" ) def project_face_vector(self, face_vectors): """Project vectors onto the faces of the mesh. Consider a numpy array *face_vectors* whose rows provide a vector for each face in the mesh. For each face, *project_face_vector* computes the dot product between a vector and the corresponding face's unit normal vector. That is, *project_face_vector* projects the vectors in *face_vectors* to the faces of the mesh. Parameters ---------- face_vectors : (n_faces, dim) numpy.ndarray Numpy array containing the vectors that will be projected to the mesh faces Returns ------- (n_faces) numpy.ndarray of float Dot product between each vector and the unit normal vector of the corresponding face """ if not isinstance(face_vectors, np.ndarray): raise Exception("face_vectors must be an ndarray") if not ( len(face_vectors.shape) == 2 and face_vectors.shape[0] == self.n_faces and face_vectors.shape[1] == self.dim ): raise Exception("face_vectors must be an ndarray of shape (n_faces, dim)") return np.sum(face_vectors * self.face_normals, 1) def project_edge_vector(self, edge_vectors): """Project vectors to the edges of the mesh. Consider a numpy array *edge_vectors* whose rows provide a vector for each edge in the mesh. For each edge, *project_edge_vector* computes the dot product between a vector and the corresponding edge's unit tangent vector. That is, *project_edge_vector* projects the vectors in *edge_vectors* to the edges of the mesh. Parameters ---------- edge_vectors : (n_edges, dim) numpy.ndarray Numpy array containing the vectors that will be projected to the mesh edges Returns ------- (n_edges) numpy.ndarray of float Dot product between each vector and the unit tangent vector of the corresponding edge """ if not isinstance(edge_vectors, np.ndarray): raise Exception("edge_vectors must be an ndarray") if not ( len(edge_vectors.shape) == 2 and edge_vectors.shape[0] == self.n_edges and edge_vectors.shape[1] == self.dim ): raise Exception("edge_vectors must be an ndarray of shape (nE, dim)") return np.sum(edge_vectors * self.edge_tangents, 1) # Mesh properties @property def cell_volumes(self): """Return cell volumes. Calling this property will compute and return a 1D array containing the volumes of mesh cells. Returns ------- (n_cells) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* Returns the cell widths - *2D:* Returns the cell areas - *3D:* Returns the cell volumes """ raise NotImplementedError(f"cell_volumes not implemented for {type(self)}") @property def face_areas(self): """Return areas of all faces in the mesh. Calling this property will compute and return the areas of all faces as a 1D numpy array. For structured meshes, the returned quantity is ordered x-face areas, then y-face areas, then z-face areas. Returns ------- (n_faces) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *1D:* returns the x-face areas - *2D:* returns the x-face and y-face areas in order; i.e. y-edge and x-edge lengths, respectively - *3D:* returns the x, y and z-face areas in order """ raise NotImplementedError(f"face_areas not implemented for {type(self)}") @property def edge_lengths(self): """Return lengths of all edges in the mesh. Calling this property will compute and return the lengths of all edges in the mesh. For structured meshes, the returned quantity is ordered x-edge lengths, then y-edge lengths, then z-edge lengths. Returns ------- (n_edges) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *1D:* returns the x-edge lengths - *2D:* returns the x-edge and y-edge lengths in order - *3D:* returns the x, y and z-edge lengths in order """ raise NotImplementedError(f"edge_lengths not implemented for {type(self)}") # Differential Operators @property def face_divergence(self): r"""Face divergence operator (faces to cell-centres). This property constructs the 2nd order numerical divergence operator that maps from faces to cell centers. The operator is a sparse matrix :math:`\mathbf{D_f}` that can be applied as a matrix-vector product to a discrete vector :math:`\mathbf{u}` that lives on mesh faces; i.e.:: div_u = Df @ u Once constructed, the operator is stored permanently as a property of the mesh. *See notes for additional details.* Returns ------- (n_cells, n_faces) scipy.sparse.csr_matrix The numerical divergence operator from faces to cell centers Notes ----- In continuous space, the divergence operator is defined as: .. math:: \phi = \nabla \cdot \vec{u} = \frac{\partial u_x}{\partial x} + \frac{\partial u_y}{\partial y} + \frac{\partial u_z}{\partial z} Where :math:`\mathbf{u}` is the discrete representation of the continuous variable :math:`\vec{u}` on cell faces and :math:`\boldsymbol{\phi}` is the discrete representation of :math:`\phi` at cell centers, **face_divergence** constructs a discrete linear operator :math:`\mathbf{D_f}` such that: .. math:: \boldsymbol{\phi} = \mathbf{D_f \, u} For each cell, the computation of the face divergence can be expressed according to the integral form below. For cell :math:`i` whose corresponding faces are indexed as a subset :math:`K` from the set of all mesh faces: .. math:: \phi_i = \frac{1}{V_i} \sum_{k \in K} A_k \, \vec{u}_k \cdot \hat{n}_k where :math:`V_i` is the volume of cell :math:`i`, :math:`A_k` is the surface area of face *k*, :math:`\vec{u}_k` is the value of :math:`\vec{u}` on face *k*, and :math:`\hat{n}_k` represents the outward normal vector of face *k* for cell *i*. Examples -------- Below, we demonstrate 1) how to apply the face divergence operator to a discrete vector and 2) the mapping of the face divergence operator and its sparsity. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl Define a 2D mesh >>> h = np.ones(20) >>> mesh = TensorMesh([h, h], "CC") Create a discrete vector on mesh faces >>> faces_x = mesh.faces_x >>> faces_y = mesh.faces_y >>> ux = (faces_x[:, 0] / np.sqrt(np.sum(faces_x ** 2, axis=1))) * np.exp( ... -(faces_x[:, 0] ** 2 + faces_x[:, 1] ** 2) / 6 ** 2 ... ) >>> uy = (faces_y[:, 1] / np.sqrt(np.sum(faces_y ** 2, axis=1))) * np.exp( ... -(faces_y[:, 0] ** 2 + faces_y[:, 1] ** 2) / 6 ** 2 ... ) >>> u = np.r_[ux, uy] Construct the divergence operator and apply to face-vector >>> Df = mesh.face_divergence >>> div_u = Df @ u Plot the original face-vector and its divergence >>> fig = plt.figure(figsize=(13, 6)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image( ... u, ax=ax1, v_type="F", view="vec", stream_opts={"color": "w", "density": 1.0} ... ) >>> ax1.set_title("Vector at cell faces", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(div_u, ax=ax2) >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Divergence at cell centers", fontsize=14) >>> plt.show() The discrete divergence operator is a sparse matrix that maps from faces to cell centers. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements in the original discrete quantity :math:`\mathbf{u}` and its discrete divergence :math:`\boldsymbol{\phi}` as well as a spy plot. >>> mesh = TensorMesh([[(1, 6)], [(1, 3)]]) >>> fig = plt.figure(figsize=(10, 10)) >>> ax1 = fig.add_subplot(211) >>> mesh.plot_grid(ax=ax1) >>> ax1.plot( ... mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8 ... ) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0]+0.05, loc[1]+0.02, "{0:d}".format(ii), color="r") >>> ax1.plot( ... mesh.faces_x[:, 0], mesh.faces_x[:, 1], "g>", markersize=8 ... ) >>> for ii, loc in zip(range(mesh.nFx), mesh.faces_x): ... ax1.text(loc[0]+0.05, loc[1]+0.02, "{0:d}".format(ii), color="g") >>> ax1.plot( ... mesh.faces_y[:, 0], mesh.faces_y[:, 1], "g^", markersize=8 ... ) >>> for ii, loc in zip(range(mesh.nFy), mesh.faces_y): ... ax1.text(loc[0] + 0.05, loc[1] + 0.1, "{0:d}".format((ii + mesh.nFx)), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.set_title("Mapping of Face Divergence", fontsize=14, pad=15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (centers)', r'$\mathbf{u}$ (faces)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(212) >>> ax2.spy(mesh.face_divergence) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Cell Index", fontsize=12) >>> ax2.set_xlabel("Face Index", fontsize=12) >>> plt.show() """ raise NotImplementedError(f"face_divergence not implemented for {type(self)}") @property def nodal_gradient(self): r"""Nodal gradient operator (nodes to edges). This property constructs the 2nd order numerical gradient operator that maps from nodes to edges. The operator is a sparse matrix :math:`\mathbf{G_n}` that can be applied as a matrix-vector product to a discrete scalar quantity :math:`\boldsymbol{\phi}` that lives on the nodes, i.e.:: grad_phi = Gn @ phi Once constructed, the operator is stored permanently as a property of the mesh. Returns ------- (n_edges, n_nodes) scipy.sparse.csr_matrix The numerical gradient operator from nodes to edges Notes ----- In continuous space, the gradient operator is defined as: .. math:: \vec{u} = \nabla \phi = \frac{\partial \phi}{\partial x}\hat{x} + \frac{\partial \phi}{\partial y}\hat{y} + \frac{\partial \phi}{\partial z}\hat{z} Where :math:`\boldsymbol{\phi}` is the discrete representation of the continuous variable :math:`\phi` on the nodes and :math:`\mathbf{u}` is the discrete representation of :math:`\vec{u}` on the edges, **nodal_gradient** constructs a discrete linear operator :math:`\mathbf{G_n}` such that: .. math:: \mathbf{u} = \mathbf{G_n} \, \boldsymbol{\phi} The Cartesian components of :math:`\vec{u}` are defined on their corresponding edges (x, y or z) as follows; e.g. the x-component of the gradient is defined on x-edges. For edge :math:`i` which defines a straight path of length :math:`h_i` between adjacent nodes :math:`n_1` and :math:`n_2`: .. math:: u_i = \frac{\phi_{n_2} - \phi_{n_1}}{h_i} Note that :math:`u_i \in \mathbf{u}` may correspond to a value on an x, y or z edge. See the example below. Examples -------- Below, we demonstrate 1) how to apply the nodal gradient operator to a discrete scalar quantity, and 2) the mapping of the nodal gradient operator and its sparsity. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl For a discrete scalar quantity defined on the nodes, we take the gradient by constructing the gradient operator and multiplying as a matrix-vector product. Create a uniform grid >>> h = np.ones(20) >>> mesh = TensorMesh([h, h], "CC") Create a discrete scalar on nodes >>> nodes = mesh.nodes >>> phi = np.exp(-(nodes[:, 0] ** 2 + nodes[:, 1] ** 2) / 4 ** 2) Construct the gradient operator and apply to vector >>> Gn = mesh.nodal_gradient >>> grad_phi = Gn @ phi Plot the original function and the gradient >>> fig = plt.figure(figsize=(13, 6)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi, v_type="N", ax=ax1) >>> ax1.set_title("Scalar at nodes", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image( ... grad_phi, ax=ax2, v_type="E", view="vec", ... stream_opts={"color": "w", "density": 1.0} ... ) >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Gradient at edges", fontsize=14) >>> plt.show() The nodal gradient operator is a sparse matrix that maps from nodes to edges. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements in the original discrete quantity :math:`\boldsymbol{\phi}` and its discrete gradient as well as a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 6)]]) >>> fig = plt.figure(figsize=(12, 10)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_grid(ax=ax1) >>> ax1.set_title("Mapping of Gradient Operator", fontsize=14, pad=15) >>> ax1.plot(mesh.nodes[:, 0], mesh.nodes[:, 1], "ro", markersize=8) >>> for ii, loc in zip(range(mesh.nN), mesh.nodes): >>> ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") >>> ax1.plot(mesh.edges_x[:, 0], mesh.edges_x[:, 1], "g>", markersize=8) >>> for ii, loc in zip(range(mesh.nEx), mesh.edges_x): >>> ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="g") >>> ax1.plot(mesh.edges_y[:, 0], mesh.edges_y[:, 1], "g^", markersize=8) >>> for ii, loc in zip(range(mesh.nEy), mesh.edges_y): >>> ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format((ii + mesh.nEx)), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.legend( >>> ['Mesh', r'$\mathbf{\phi}$ (nodes)', r'$\mathbf{u}$ (edges)'], >>> loc='upper right', fontsize=14 >>> ) >>> ax2 = fig.add_subplot(122) >>> ax2.spy(mesh.nodal_gradient) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Edge Index", fontsize=12) >>> ax2.set_xlabel("Node Index", fontsize=12) >>> plt.show() """ raise NotImplementedError(f"nodal_gradient not implemented for {type(self)}") @property def edge_curl(self): r"""Edge curl operator (edges to faces). This property constructs the 2nd order numerical curl operator that maps from edges to faces. The operator is a sparse matrix :math:`\mathbf{C_e}` that can be applied as a matrix-vector product to a discrete vector quantity **u** that lives on the edges; i.e.:: curl_u = Ce @ u Once constructed, the operator is stored permanently as a property of the mesh. Returns ------- (n_faces, n_edges) scipy.sparse.csr_matrix The numerical curl operator from edges to faces Notes ----- In continuous space, the curl operator is defined as: .. math:: \vec{w} = \nabla \times \vec{u} = \begin{vmatrix} \hat{x} & \hat{y} & \hat{z} \\ \partial_x & \partial_y & \partial_z \\ u_x & u_y & u_z \end{vmatrix} Where :math:`\mathbf{u}` is the discrete representation of the continuous variable :math:`\vec{u}` on cell edges and :math:`\mathbf{w}` is the discrete representation of the curl on the faces, **edge_curl** constructs a discrete linear operator :math:`\mathbf{C_e}` such that: .. math:: \mathbf{w} = \mathbf{C_e \, u} The computation of the curl on mesh faces can be expressed according to the integral form below. For face :math:`i` bordered by a set of edges indexed by subset :math:`K`: .. math:: w_i = \frac{1}{A_i} \sum_{k \in K} \vec{u}_k \cdot \vec{\ell}_k where :math:`A_i` is the surface area of face *i*, :math:`u_k` is the value of :math:`\vec{u}` on face *k*, and \vec{\ell}_k is the path along edge *k*. Examples -------- Below, we demonstrate the mapping and sparsity of the edge curl for a 3D tensor mesh. We choose a the index for a single face, and illustrate which edges are used to compute the curl on that face. >>> from discretize import TensorMesh >>> from discretize.utils import mkvc >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import matplotlib as mpl >>> import mpl_toolkits.mplot3d as mp3d >>> mpl.rcParams.update({'font.size': 14}) Create a simple tensor mesh, and grab the **edge_curl** operator: >>> mesh = TensorMesh([[(1, 2)], [(1, 2)], [(1, 2)]]) >>> Ce = mesh.edge_curl Then we choose a *face* for illustration purposes: >>> face_ind = 2 # Index of a face in the mesh (could be x, y or z) >>> edge_ind = np.where( ... np.sum((mesh.edges-mesh.faces[face_ind, :])**2, axis=1) <= 0.5 + 1e-6 ... )[0] >>> face = mesh.faces[face_ind, :] >>> face_norm = mesh.face_normals[face_ind, :] >>> edges = mesh.edges[edge_ind, :] >>> edge_tan = mesh.edge_tangents[edge_ind, :] >>> node = np.min(edges, axis=0) >>> min_edges = np.min(edges, axis=0) >>> max_edges = np.max(edges, axis=0) >>> if face_norm[0] == 1: ... k = (edges[:, 1] == min_edges[1]) | (edges[:, 2] == max_edges[2]) ... poly = node + np.c_[np.r_[0, 0, 0, 0], np.r_[0, 1, 1, 0], np.r_[0, 0, 1, 1]] ... ds = [0.07, -0.07, -0.07] ... elif face_norm[1] == 1: ... k = (edges[:, 0] == max_edges[0]) | (edges[:, 2] == min_edges[2]) ... poly = node + np.c_[np.r_[0, 1, 1, 0], np.r_[0, 0, 0, 0], np.r_[0, 0, 1, 1]] ... ds = [0.07, -0.09, -0.07] ... elif face_norm[2] == 1: ... k = (edges[:, 0] == min_edges[0]) | (edges[:, 1] == max_edges[1]) ... poly = node + np.c_[np.r_[0, 1, 1, 0], np.r_[0, 0, 1, 1], np.r_[0, 0, 0, 0]] ... ds = [0.07, -0.09, -0.07] >>> edge_tan[k, :] *= -1 Plot the curve and its mapping for a single face. >>> fig = plt.figure(figsize=(10, 15)) >>> ax1 = fig.add_axes([0, 0.35, 1, 0.6], projection='3d', elev=25, azim=-60) >>> mesh.plot_grid(ax=ax1) >>> ax1.plot( ... mesh.edges[edge_ind, 0], mesh.edges[edge_ind, 1], mesh.edges[edge_ind, 2], ... "go", markersize=10 ... ) >>> ax1.plot( ... mesh.faces[face_ind, 0], mesh.faces[face_ind, 1], mesh.faces[face_ind, 2], ... "rs", markersize=10 ... ) >>> poly = mp3d.art3d.Poly3DCollection( ... [poly], alpha=0.1, facecolor='r', linewidth=None ... ) >>> ax1.add_collection(poly) >>> ax1.quiver( ... edges[:, 0], edges[:, 1], edges[:, 2], ... 0.5*edge_tan[:, 0], 0.5*edge_tan[:, 1], 0.5*edge_tan[:, 2], ... edgecolor='g', pivot='middle', linewidth=4, arrow_length_ratio=0.25 ... ) >>> ax1.text(face[0]+ds[0], face[1]+ds[1], face[2]+ds[2], "{0:d}".format(face_ind), color="r") >>> for ii, loc in zip(range(len(edge_ind)), edges): ... ax1.text(loc[0]+ds[0], loc[1]+ds[1], loc[2]+ds[2], "{0:d}".format(edge_ind[ii]), color="g") >>> ax1.legend( ... ['Mesh', r'$\mathbf{u}$ (edges)', r'$\mathbf{w}$ (face)'], ... loc='upper right', fontsize=14 ... ) Manually make axis properties invisible >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.set_zticks([]) >>> ax1.xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.xaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.yaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.zaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.set_xlabel('X', labelpad=-15, fontsize=16) >>> ax1.set_ylabel('Y', labelpad=-20, fontsize=16) >>> ax1.set_zlabel('Z', labelpad=-20, fontsize=16) >>> ax1.set_title("Mapping for a Single Face", fontsize=16, pad=-15) >>> ax2 = fig.add_axes([0.05, 0.05, 0.9, 0.3]) >>> ax2.spy(Ce) >>> ax2.set_title("Spy Plot", fontsize=16, pad=5) >>> ax2.set_ylabel("Face Index", fontsize=12) >>> ax2.set_xlabel("Edge Index", fontsize=12) >>> plt.show() """ raise NotImplementedError(f"edge_curl not implemented for {type(self)}") @property def boundary_face_scalar_integral(self): r"""Represent the operation of integrating a scalar function on the boundary. This matrix represents the boundary surface integral of a scalar function multiplied with a finite volume test function on the mesh. Returns ------- (n_faces, n_boundary_faces) scipy.sparse.csr_matrix Notes ----- The integral we are representing on the boundary of the mesh is .. math:: \int_{\Omega} u\vec{w} \cdot \hat{n} \partial \Omega In discrete form this is: .. math:: w^T * P @ u_b where `w` is defined on all faces, and `u_b` is defined on boundary faces. """ raise NotImplementedError( f"boundary_face_scalar_integral not implemented for {type(self)}" ) @property def boundary_edge_vector_integral(self): r"""Represent the operation of integrating a vector function on the boundary. This matrix represents the boundary surface integral of a vector function multiplied with a finite volume test function on the mesh. In 1D and 2D, the operation assumes that the right array contains only a single component of the vector ``u``. In 3D, however, we must assume that ``u`` will contain each of the three vector components, and it must be ordered as, ``[edges_1_x, ... ,edge_N_x, edge_1_y, ..., edge_N_y, edge_1_z, ..., edge_N_z]`` , where ``N`` is the number of boundary edges. Returns ------- scipy.sparse.csr_matrix Sparse matrix of shape (n_edges, n_boundary_edges) for 1D or 2D mesh, (n_edges, 3*n_boundary_edges) for a 3D mesh. Notes ----- The integral we are representing on the boundary of the mesh is .. math:: \int_{\Omega} \vec{w} \cdot (\vec{u} \times \hat{n}) \partial \Omega In discrete form this is: .. math:: w^T * P @ u_b where `w` is defined on all edges, and `u_b` is all three components defined on boundary edges. """ raise NotImplementedError( f"boundary_edge_vector_integral not implemented for {type(self)}" ) @property def boundary_node_vector_integral(self): r"""Represent the operation of integrating a vector function dotted with the boundary normal. This matrix represents the boundary surface integral of a vector function dotted with the boundary normal and multiplied with a scalar finite volume test function on the mesh. Returns ------- (n_nodes, dim * n_boundary_nodes) scipy.sparse.csr_matrix Sparse matrix of shape. Notes ----- The integral we are representing on the boundary of the mesh is .. math:: \int_{\Omega} (w \vec{u}) \cdot \hat{n} \partial \Omega In discrete form this is: .. math:: w^T * P @ u_b where `w` is defined on all nodes, and `u_b` is all three components defined on boundary nodes. """ raise NotImplementedError( f"boundary_node_vector_integral not implemented for {type(self)}" ) @property def nodal_laplacian(self): r"""Nodal scalar Laplacian operator (nodes to nodes). This property constructs the 2nd order scalar Laplacian operator that maps from nodes to nodes. The operator is a sparse matrix :math:`\mathbf{L_n}` that can be applied as a matrix-vector product to a discrete scalar quantity :math:`\boldsymbol{\phi}` that lives on the nodes, i.e.:: laplace_phi = Ln @ phi The operator ``*`` assumes a zero Neumann boundary condition for the discrete scalar quantity. Once constructed, the operator is stored permanently as a property of the mesh. Returns ------- (n_nodes, n_nodes) scipy.sparse.csr_matrix The numerical Laplacian operator from nodes to nodes Notes ----- In continuous space, the scalar Laplacian operator is defined as: .. math:: \psi = \nabla^2 \phi = \frac{\partial^2 \phi}{\partial x^2} + \frac{\partial^2 \phi}{\partial y^2} + \frac{\partial^2 \phi}{\partial z^2} Where :math:`\boldsymbol{\phi}` is the discrete representation of the continuous variable :math:`\phi` on the nodes, and :math:`\boldsymbol{\psi}` is the discrete representation of its scalar Laplacian on the nodes, **nodal_laplacian** constructs a discrete linear operator :math:`\mathbf{L_n}` such that: .. math:: \boldsymbol{\psi} = \mathbf{L_n} \, \boldsymbol{\phi} """ # EXAMPLE INACTIVE BECAUSE OPERATOR IS BROKEN # # Examples # -------- # Below, we demonstrate how to apply the nodal Laplacian operator to # a discrete scalar quantity, the mapping of the nodal Laplacian operator and # its sparsity. Our example is carried out on a 2D mesh but it can # be done equivalently for a 3D mesh. # We start by importing the necessary packages and modules. # >>> from discretize import TensorMesh # >>> import numpy as np # >>> import matplotlib.pyplot as plt # >>> import matplotlib as mpl # For a discrete scalar quantity defined on the nodes, we take the # Laplacian by constructing the operator and multiplying # as a matrix-vector product. # >>> # Create a uniform grid # >>> h = np.ones(20) # >>> mesh = TensorMesh([h, h], "CC") # >>> # >>> # Create a discrete scalar on nodes. The scalar MUST # >>> # respect the zero Neumann boundary condition. # >>> nodes = mesh.nodes # >>> phi = np.exp(-(nodes[:, 0] ** 2 + nodes[:, 1] ** 2) / 4 ** 2) # >>> # >>> # Construct the Laplacian operator and apply to vector # >>> Ln = mesh.nodal_laplacian # >>> laplacian_phi = Ln @ phi # >>> # >>> # Plot # >>> fig = plt.figure(figsize=(13, 6)) # >>> ax1 = fig.add_subplot(121) # >>> mesh.plot_image(phi, ax=ax1) # >>> ax1.set_title("Scalar at nodes", fontsize=14) # >>> ax2 = fig.add_subplot(122) # >>> mesh.plot_image(laplacian_phi, ax=ax1) # >>> ax2.set_yticks([]) # >>> ax2.set_ylabel("") # >>> ax2.set_title("Laplacian at nodes", fontsize=14) # >>> plt.show() # The nodal Laplacian operator is a sparse matrix that maps # from nodes to nodes. To demonstrate this, we construct # a small 2D mesh. We then show the ordering of the nodes # and a spy plot illustrating the sparsity of the operator. # >>> mesh = TensorMesh([[(1, 4)], [(1, 4)]]) # >>> fig = plt.figure(figsize=(12, 6)) # >>> ax1 = fig.add_subplot(211) # >>> mesh.plot_grid(ax=ax1) # >>> ax1.set_title("Ordering of the Nodes", fontsize=14, pad=15) # >>> ax1.plot(mesh.nodes[:, 0], mesh.nodes[:, 1], "ro", markersize=8) # >>> for ii, loc in zip(range(mesh.nN), mesh.nodes): # ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") # >>> ax1.set_xticks([]) # >>> ax1.set_yticks([]) # >>> ax1.spines['bottom'].set_color('white') # >>> ax1.spines['top'].set_color('white') # >>> ax1.spines['left'].set_color('white') # >>> ax1.spines['right'].set_color('white') # >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) # >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) # >>> ax1.legend( # ... ['Mesh', r'$\mathbf{\phi}$ (nodes)'], # ... loc='upper right', fontsize=14 # ... ) # >>> ax2 = fig.add_subplot(212) # >>> ax2.spy(mesh.nodal_laplacian) # >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) # >>> ax2.set_ylabel("Node Index", fontsize=12) # >>> ax2.set_xlabel("Node Index", fontsize=12) raise NotImplementedError(f"nodal_laplacian not implemented for {type(self)}") @property def stencil_cell_gradient(self): r"""Stencil for cell gradient operator (cell centers to faces). This property constructs a differencing operator that acts on cell centered quantities. The operator takes the difference between the values at adjacent cell centers along each axis direction, and places the result on the shared face; e.g. differences along the x-axis are mapped to x-faces. The operator is a sparse matrix :math:`\mathbf{G}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: diff_phi = G @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **stencil_cell_gradient** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **stencil_cell_gradient** is called, the boundary conditions are enforced for the differencing operator. Once constructed, the operator is stored as a property of the mesh. Returns ------- (n_faces, n_cells) scipy.sparse.csr_matrix The stencil for the cell gradient Examples -------- Below, we demonstrate how to set boundary conditions for the cell gradient stencil, construct the cell gradient stencil and apply it to a discrete scalar quantity. The mapping of the cell gradient operator and its sparsity is also illustrated. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers. In this case, the scalar represents some block within a homogeneous medium. Create a uniform grid >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], "CC") Create a discrete scalar at cell centers >>> centers = mesh.cell_centers >>> phi = np.zeros(mesh.nC) >>> k = (np.abs(mesh.cell_centers[:, 0]) < 10.) & (np.abs(mesh.cell_centers[:, 1]) < 10.) >>> phi[k] = 1. Before constructing the operator, we must define the boundary conditions; zero Neumann for our example. Once the operator is created, it is applied as a matrix-vector product. >>> mesh.set_cell_gradient_BC(['neumann', 'neumann']) >>> G = mesh.stencil_cell_gradient >>> diff_phi = G @ phi Now we plot the original scalar, and the differencing taken along the x and y axes. >>> fig = plt.figure(figsize=(15, 4.5)) >>> ax1 = fig.add_subplot(131) >>> mesh.plot_image(phi, ax=ax1) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(132) >>> mesh.plot_image(diff_phi, ax=ax2, v_type="Fx") >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Difference (x-axis)", fontsize=14) >>> ax3 = fig.add_subplot(133) >>> mesh.plot_image(diff_phi, ax=ax3, v_type="Fy") >>> ax3.set_yticks([]) >>> ax3.set_ylabel("") >>> ax3.set_title("Difference (y-axis)", fontsize=14) >>> plt.show() The cell gradient stencil is a sparse differencing matrix that maps from cell centers to faces. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements and a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 6)]]) >>> mesh.set_cell_gradient_BC('neumann') >>> fig = plt.figure(figsize=(12, 10)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_grid(ax=ax1) >>> ax1.set_title("Mapping of Stencil", fontsize=14, pad=15) >>> ax1.plot(mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") >>> ax1.plot(mesh.faces_x[:, 0], mesh.faces_x[:, 1], "g>", markersize=8) >>> for ii, loc in zip(range(mesh.nFx), mesh.faces_x): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="g") >>> ax1.plot(mesh.faces_y[:, 0], mesh.faces_y[:, 1], "g^", markersize=8) >>> for ii, loc in zip(range(mesh.nFy), mesh.faces_y): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format((ii + mesh.nFx)), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (centers)', r'$\mathbf{G^\ast \phi}$ (faces)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(122) >>> ax2.spy(mesh.stencil_cell_gradient) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Face Index", fontsize=12) >>> ax2.set_xlabel("Cell Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"stencil_cell_gradient not implemented for {type(self)}" ) # Inner Products def get_face_inner_product( self, model=None, invert_model=False, invert_matrix=False, do_fast=True, **kwargs, ): r"""Generate the face inner product matrix or its inverse. This method generates the inner product matrix (or its inverse) when discrete variables are defined on mesh faces. It is also capable of constructing the inner product matrix when physical properties are defined in the form of constitutive relations. For a comprehensive description of the inner product matrices that can be constructed with **get_face_inner_product**, see *Notes*. Parameters ---------- model : None or numpy.ndarray, optional Parameters defining the material properties for every cell in the mesh. Inner product matrices can be constructed for the following cases: - *None* : returns the basic inner product matrix - *(n_cells)* :class:`numpy.ndarray` : returns inner product matrix for an isotropic model. The array contains a scalar physical property value for each cell. - *(n_cells, dim)* :class:`numpy.ndarray` : returns inner product matrix for diagonal anisotropic case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz]``. This can also a be a 1D array with the same number of total elements in column major order. - *(n_cells, 3)* :class:`numpy.ndarray` (``dim`` is 2) or *(n_cells, 6)* :class:`numpy.ndarray` (``dim`` is 3) : returns inner product matrix for full tensor properties case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz, σ_xy, σ_xz, σ_yz]`` This can also be a 1D array with the same number of total elements in column major order. invert_model : bool, optional The inverse of *model* is used as the physical property. invert_matrix : bool, optional Returns the inverse of the inner product matrix. The inverse not implemented for full tensor properties. do_fast : bool, optional Do a faster implementation (if available). Returns ------- (n_faces, n_faces) scipy.sparse.csr_matrix inner product matrix Notes ----- For continuous vector quantities :math:`\vec{u}` and :math:`\vec{w}` whose discrete representations :math:`\mathbf{u}` and :math:`\mathbf{w}` live on the faces, **get_face_inner_product** constructs the inner product matrix :math:`\mathbf{M_\ast}` (or its inverse :math:`\mathbf{M_\ast^{-1}}`) for the following cases: **Basic Inner Product:** the inner product between :math:`\vec{u}` and :math:`\vec{w}` .. math:: \langle \vec{u}, \vec{w} \rangle = \mathbf{u^T \, M \, w} **Isotropic Case:** the inner product between :math:`\vec{u}` and :math:`\sigma \vec{w}` where :math:`\sigma` is a scalar function. .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \mathbf{u^T \, M_\sigma \, w} **Tensor Case:** the inner product between :math:`\vec{u}` and :math:`\Sigma \vec{w}` where :math:`\Sigma` is tensor function; :math:`\sigma_{xy} = \sigma_{xz} = \sigma_{yz} = 0` for diagonal anisotropy. .. math:: \langle \vec{u}, \Sigma \vec{w} \rangle = \mathbf{u^T \, M_\Sigma \, w} \;\;\; \textrm{where} \;\;\; \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ \sigma_{xy} & \sigma_{yy} & \sigma_{yz} \\ \sigma_{xz} & \sigma_{yz} & \sigma_{zz} \end{bmatrix} Examples -------- Here we provide some examples of face inner product matrices. For simplicity, we will work on a 2 x 2 x 2 tensor mesh. As seen below, we begin by constructing and imaging the basic face inner product matrix. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import matplotlib as mpl >>> h = np.ones(2) >>> mesh = TensorMesh([h, h, h]) >>> Mf = mesh.get_face_inner_product() >>> fig = plt.figure(figsize=(6, 6)) >>> ax = fig.add_subplot(111) >>> ax.imshow(Mf.todense()) >>> ax.set_title('Basic Face Inner Product Matrix', fontsize=18) >>> plt.show() Next, we consider the case where the physical properties of the cells are defined by consistutive relations. For the isotropic, diagonal anisotropic and full tensor cases, we show the physical property tensor for a single cell. Define 4 constitutive parameters and define the tensor for each cell for isotropic, diagonal and tensor cases. >>> sig1, sig2, sig3, sig4, sig5, sig6 = 6, 5, 4, 3, 2, 1 >>> sig_iso_tensor = sig1 * np.eye(3) >>> sig_diag_tensor = np.diag(np.array([sig1, sig2, sig3])) >>> sig_full_tensor = np.array([ ... [sig1, sig4, sig5], ... [sig4, sig2, sig6], ... [sig5, sig6, sig3] ... ]) Then plot matrix entries, >>> fig = plt.figure(figsize=(15, 5)) >>> ax1 = fig.add_subplot(131) >>> ax1.imshow(sig_iso_tensor) >>> ax1.axis('off') >>> ax1.set_title("Tensor (isotropic)", fontsize=16) >>> ax2 = fig.add_subplot(132) >>> ax2.imshow(sig_diag_tensor) >>> ax2.axis('off') >>> ax2.set_title("Tensor (diagonal anisotropic)", fontsize=16) >>> ax3 = fig.add_subplot(133) >>> ax3.imshow(sig_full_tensor) >>> ax3.axis('off') >>> ax3.set_title("Tensor (full anisotropic)", fontsize=16) >>> plt.show() Here, construct and image the face inner product matrices for the isotropic, diagonal anisotropic and full tensor cases. Spy plots are used to demonstrate the sparsity of the inner product matrices. Isotropic case: >>> v = np.ones(mesh.nC) >>> sig = sig1 * v >>> M1 = mesh.get_face_inner_product(sig) Diagonal anisotropic case: >>> sig = np.c_[sig1*v, sig2*v, sig3*v] >>> M2 = mesh.get_face_inner_product(sig) Full anisotropic case: >>> sig = np.tile(np.c_[sig1, sig2, sig3, sig4, sig5, sig6], (mesh.nC, 1)) >>> M3 = mesh.get_face_inner_product(sig) And then we can plot the sparse representation, >>> fig = plt.figure(figsize=(12, 4)) >>> ax1 = fig.add_subplot(131) >>> ax1.spy(M1, ms=5) >>> ax1.set_title("M (isotropic)", fontsize=16) >>> ax2 = fig.add_subplot(132) >>> ax2.spy(M2, ms=5) >>> ax2.set_title("M (diagonal anisotropic)", fontsize=16) >>> ax3 = fig.add_subplot(133) >>> ax3.spy(M3, ms=5) >>> ax3.set_title("M (full anisotropic)", fontsize=16) >>> plt.show() """ raise NotImplementedError( f"get_face_inner_product not implemented for {type(self)}" ) def get_edge_inner_product( self, model=None, invert_model=False, invert_matrix=False, do_fast=True, **kwargs, ): r"""Generate the edge inner product matrix or its inverse. This method generates the inner product matrix (or its inverse) when discrete variables are defined on mesh edges. It is also capable of constructing the inner product matrix when physical properties are defined in the form of constitutive relations. For a comprehensive description of the inner product matrices that can be constructed with **get_edge_inner_product**, see *Notes*. Parameters ---------- model : None or numpy.ndarray Parameters defining the material properties for every cell in the mesh. Inner product matrices can be constructed for the following cases: - *None* : returns the basic inner product matrix - *(n_cells)* :class:`numpy.ndarray` : returns inner product matrix for an isotropic model. The array contains a scalar physical property value for each cell. - *(n_cells, dim)* :class:`numpy.ndarray` : returns inner product matrix for diagonal anisotropic case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz]``. This can also a be a 1D array with the same number of total elements in column major order. - *(n_cells, 3)* :class:`numpy.ndarray` (``dim`` is 2) or *(n_cells, 6)* :class:`numpy.ndarray` (``dim`` is 3) : returns inner product matrix for full tensor properties case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz, σ_xy, σ_xz, σ_yz]`` This can also be a 1D array with the same number of total elements in column major order. invert_model : bool, optional The inverse of *model* is used as the physical property. invert_matrix : bool, optional Returns the inverse of the inner product matrix. The inverse not implemented for full tensor properties. do_fast : bool, optional Do a faster implementation (if available). Returns ------- (n_edges, n_edges) scipy.sparse.csr_matrix inner product matrix Notes ----- For continuous vector quantities :math:`\vec{u}` and :math:`\vec{w}` whose discrete representations :math:`\mathbf{u}` and :math:`\mathbf{w}` live on the edges, **get_edge_inner_product** constructs the inner product matrix :math:`\mathbf{M_\ast}` (or its inverse :math:`\mathbf{M_\ast^{-1}}`) for the following cases: **Basic Inner Product:** the inner product between :math:`\vec{u}` and :math:`\vec{w}`. .. math:: \langle \vec{u}, \vec{w} \rangle = \mathbf{u^T \, M \, w} **Isotropic Case:** the inner product between :math:`\vec{u}` and :math:`\sigma \vec{w}` where :math:`\sigma` is a scalar function. .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \mathbf{u^T \, M_\sigma \, w} **Tensor Case:** the inner product between :math:`\vec{u}` and :math:`\Sigma \vec{w}` where :math:`\Sigma` is tensor function; :math:`\sigma_{xy} = \sigma_{xz} = \sigma_{yz} = 0` for diagonal anisotropy. .. math:: \langle \vec{u}, \Sigma \vec{w} \rangle = \mathbf{u^T \, M_\Sigma \, w} \;\;\; \textrm{where} \;\;\; \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ \sigma_{xy} & \sigma_{yy} & \sigma_{yz} \\ \sigma_{xz} & \sigma_{yz} & \sigma_{zz} \end{bmatrix} Examples -------- Here we provide some examples of edge inner product matrices. For simplicity, we will work on a 2 x 2 x 2 tensor mesh. As seen below, we begin by constructing and imaging the basic edge inner product matrix. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import matplotlib as mpl >>> h = np.ones(2) >>> mesh = TensorMesh([h, h, h]) >>> Me = mesh.get_edge_inner_product() >>> fig = plt.figure(figsize=(6, 6)) >>> ax = fig.add_subplot(111) >>> ax.imshow(Me.todense()) >>> ax.set_title('Basic Edge Inner Product Matrix', fontsize=18) >>> plt.show() Next, we consider the case where the physical properties of the cells are defined by consistutive relations. For the isotropic, diagonal anisotropic and full tensor cases, we show the physical property tensor for a single cell. Define 4 constitutive parameters and define the tensor for each cell for isotropic, diagonal and tensor cases. >>> sig1, sig2, sig3, sig4, sig5, sig6 = 6, 5, 4, 3, 2, 1 >>> sig_iso_tensor = sig1 * np.eye(3) >>> sig_diag_tensor = np.diag(np.array([sig1, sig2, sig3])) >>> sig_full_tensor = np.array([ ... [sig1, sig4, sig5], ... [sig4, sig2, sig6], ... [sig5, sig6, sig3] ... ]) Then plot the matrix entries, >>> fig = plt.figure(figsize=(15, 5)) >>> ax1 = fig.add_subplot(131) >>> ax1.imshow(sig_iso_tensor) >>> ax1.axis('off') >>> ax1.set_title("Tensor (isotropic)", fontsize=16) >>> ax2 = fig.add_subplot(132) >>> ax2.imshow(sig_diag_tensor) >>> ax2.axis('off') >>> ax2.set_title("Tensor (diagonal anisotropic)", fontsize=16) >>> ax3 = fig.add_subplot(133) >>> ax3.imshow(sig_full_tensor) >>> ax3.axis('off') >>> ax3.set_title("Tensor (full anisotropic)", fontsize=16) >>> plt.show() Here construct and image the edge inner product matrices for the isotropic, diagonal anisotropic and full tensor cases. Spy plots are used to demonstrate the sparsity of the inner product matrices. Isotropic case: >>> v = np.ones(mesh.nC) >>> sig = sig1 * v >>> M1 = mesh.get_edge_inner_product(sig) Diagonal anisotropic case: >>> sig = np.c_[sig1*v, sig2*v, sig3*v] >>> M2 = mesh.get_edge_inner_product(sig) Full anisotropic >>> sig = np.tile(np.c_[sig1, sig2, sig3, sig4, sig5, sig6], (mesh.nC, 1)) >>> M3 = mesh.get_edge_inner_product(sig) Then plot the sparse representation, >>> fig = plt.figure(figsize=(12, 4)) >>> ax1 = fig.add_subplot(131) >>> ax1.spy(M1, ms=5) >>> ax1.set_title("M (isotropic)", fontsize=16) >>> ax2 = fig.add_subplot(132) >>> ax2.spy(M2, ms=5) >>> ax2.set_title("M (diagonal anisotropic)", fontsize=16) >>> ax3 = fig.add_subplot(133) >>> ax3.spy(M3, ms=5) >>> ax3.set_title("M (full anisotropic)", fontsize=16) >>> plt.show() """ raise NotImplementedError( f"get_edge_inner_product not implemented for {type(self)}" ) def get_edge_inner_product_surface( self, model=None, invert_model=False, invert_matrix=False, **kwargs, ): r"""Generate the edge inner product surface matrix or its inverse. This method generates the inner product surface matrix (or its inverse) when discrete variables are defined on mesh edges. It is also capable of constructing the inner product surface matrix when diagnostic properties (e.g. conductance) are defined on mesh faces. For a comprehensive description of the inner product surface matrices that can be constructed with **get_edge_inner_product_surface**, see *Notes*. Parameters ---------- model : None or numpy.ndarray Parameters defining the diagnostic properties for every face in the mesh. Inner product surface matrices can be constructed for the following cases: - *None* : returns the basic inner product surface matrix - *(n_faces)* :class:`numpy.ndarray` : returns inner product surface matrix for an isotropic model. The array contains a scalar diagnostic property value for each face. invert_model : bool, optional The inverse of *model* is used as the diagnostic property. invert_matrix : bool, optional Returns the inverse of the inner product surface matrix. The inverse not implemented for full tensor properties. Returns ------- (n_edges, n_edges) scipy.sparse.csr_matrix inner product surface matrix Notes ----- For continuous vector quantities :math:`\vec{u}` and :math:`\vec{w}`, and scalar physical property distribution :math:`\sigma`, we define the following inner product: .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \int_\Omega \, \vec{u} \cdot \sigma \vec{v} \, dv If the material property is distributed over a set of surfaces :math:`S_i` with thickness :math:`h`, we can define a diagnostic property value :math:`\tau = \sigma h`. And the inner-product can be approximated by a set of surface integrals as follows: .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \sum_i \int_{S_i} \, \vec{u} \cdot \tau \vec{v} \, da Let :math:`\vec{u}` and :math:`\vec{w}` have discrete representations :math:`\mathbf{u}` and :math:`\mathbf{w}` that live on the edges. Assuming the contribution of vector components normal to the surface are negligible compared to tangential components, **get_edge_inner_product_suface** constructs the inner product matrix :math:`\mathbf{M_\tau}` (or its inverse :math:`\mathbf{M_\tau^{-1}}`) such that: .. math:: \sum_i \int_{S_i} \, \vec{u} \cdot \tau \vec{v} \, da \approx \sum_i \int_{S_i} \, \vec{u}_\parallel \cdot \tau \vec{v}_\parallel \, da = \mathbf{u^T \, M_\tau \, w} where the diagnostic properties on mesh faces (i.e. the model) are stored within an array of the form: .. math:: \boldsymbol{\tau} = \begin{bmatrix} \boldsymbol{\tau_x} \\ \boldsymbol{\tau_y} \\ \boldsymbol{\tau_z} \end{bmatrix} Examples -------- Here we provide an example of edge inner product surface matrix. For simplicity, we will work on a 2 x 2 x 2 tensor mesh. As seen below, we begin by constructing and imaging the basic edge inner product surface matrix. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import matplotlib as mpl >>> h = np.ones(2) >>> mesh = TensorMesh([h, h, h]) >>> Me = mesh.get_edge_inner_product_surface() >>> fig = plt.figure(figsize=(6, 6)) >>> ax = fig.add_subplot(111) >>> ax.imshow(Me.todense()) >>> ax.set_title('Basic Edge Inner Product Surface Matrix', fontsize=18) >>> plt.show() Next, we consider the case where the physical properties are defined by diagnostic properties on mesh faces. For the isotropic case, we show the physical property tensor for a single cell. Define the diagnostic property values for x, y and z faces. >>> tau_x, tau_y, tau_z = 3, 2, 1 Here construct and image the edge inner product surface matrix for the isotropic case. Spy plots are used to demonstrate the sparsity of the inner product surface matrices. >>> tau = np.r_[ >>> tau_x * np.ones(mesh.n_faces_x), >>> tau_y * np.ones(mesh.n_faces_y), >>> tau_z * np.ones(mesh.n_faces_z) >>> ] >>> M = mesh.get_edge_inner_product_surface(tau) Then plot the sparse representation, >>> fig = plt.figure(figsize=(4, 4)) >>> ax1 = fig.add_subplot(111) >>> ax1.imshow(M.todense()) >>> ax1.set_title("M (isotropic)", fontsize=16) >>> plt.show() """ raise NotImplementedError( f"get_edge_inner_product_surface not implemented for {type(self)}" ) def get_face_inner_product_surface( self, model=None, invert_model=False, invert_matrix=False, **kwargs, ): r"""Generate the face inner product matrix or its inverse. This method generates the inner product surface matrix (or its inverse) when discrete variables are defined on mesh faces. It is also capable of constructing the inner product surface matrix when diagnostic quantitative properties (e.g. conductance) are defined on mesh faces. For a comprehensive description of the inner product surface matrices that can be constructed with **get_face_inner_product_surface**, see *Notes*. Parameters ---------- model : None or numpy.ndarray Parameters defining the diagnostic properties for every face in the mesh. Inner product surface matrices can be constructed for the following cases: - *None* : returns the basic inner product surface matrix - *(n_faces)* :class:`numpy.ndarray` : returns inner product surface matrix for an isotropic model. The array contains a scalar diagnostic property value for each face. invert_model : bool, optional The inverse of *model* is used as the diagnostic property. invert_matrix : bool, optional Returns the inverse of the inner product surface matrix. The inverse not implemented for full tensor properties. Returns ------- (n_faces, n_faces) scipy.sparse.csr_matrix inner product matrix Notes ----- For continuous vector quantities :math:`\vec{u}` and :math:`\vec{w}`, and scalar physical property distribution :math:`\sigma`, we define the following inner product: .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \int_\Omega \, \vec{u} \cdot \sigma \vec{v} \, dv If the material property is distributed over a set of surfaces :math:`S_i` with thickness :math:`h`, we can define a diagnostic property value :math:`\tau = \sigma h`. And the inner-product can be approximated by a set of surface integrals as follows: .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \sum_i \int_{S_i} \, \vec{u} \cdot \tau \vec{v} \, da Let :math:`\vec{u}` and :math:`\vec{w}` have discrete representations :math:`\mathbf{u}` and :math:`\mathbf{w}` that live on the edges. Assuming the contribution of vector components tangential to the surface are negligible compared to normal components, **get_face_inner_product_suface** constructs the inner product matrix :math:`\mathbf{M_\tau}` (or its inverse :math:`\mathbf{M_\tau^{-1}}`) such that: .. math:: \sum_i \int_{S_i} \, \vec{u} \cdot \tau \vec{v} \, da \approx \sum_i \int_{S_i} \, \vec{u}_\perp \cdot \tau \vec{v}_\perp \, da = \mathbf{u^T \, M_\tau \, w} where the diagnostic properties on mesh faces (i.e. the model) are stored within an array of the form: .. math:: \boldsymbol{\tau} = \begin{bmatrix} \boldsymbol{\tau_x} \\ \boldsymbol{\tau_y} \\ \boldsymbol{\tau_z} \end{bmatrix} Examples -------- Here we provide an example of face inner product surface matrix. For simplicity, we will work on a 2 x 2 x 2 tensor mesh. As seen below, we begin by constructing and imaging the basic face inner product surface matrix. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import matplotlib as mpl >>> h = np.ones(2) >>> mesh = TensorMesh([h, h, h]) >>> Mf = mesh.get_face_inner_product_surface() >>> fig = plt.figure(figsize=(6, 6)) >>> ax = fig.add_subplot(111) >>> ax.imshow(Mf.todense()) >>> ax.set_title('Basic Face Inner Product Surface Matrix', fontsize=18) >>> plt.show() Next, we consider the case where the physical properties are defined by diagnostic properties on mesh faces. For the isotropic case, we show the physical property tensor for a single cell. Define the diagnostic property values for x, y and z faces. >>> tau_x, tau_y, tau_z = 3, 2, 1 Here construct and image the face inner product surface matrix for the isotropic case. Spy plots are used to demonstrate the sparsity of the inner product surface matrices. >>> tau = np.r_[ >>> tau_x * np.ones(mesh.n_faces_x), >>> tau_y * np.ones(mesh.n_faces_y), >>> tau_z * np.ones(mesh.n_faces_z) >>> ] >>> M = mesh.get_face_inner_product_surface(tau) Then plot the sparse representation, >>> fig = plt.figure(figsize=(4, 4)) >>> ax1 = fig.add_subplot(111) >>> ax1.imshow(M.todense()) >>> ax1.set_title("M (isotropic)", fontsize=16) >>> plt.show() """ try: face_areas = self.face_areas except NotImplementedError: raise NotImplementedError( f"get_face_inner_product_surface not implemented for {type(self)}" ) if model is None: model = np.ones(self.nF) if invert_model: model = 1.0 / model if is_scalar(model): model = model * np.ones(self.nF) # Isotropic case only if model.size != self.nF: raise ValueError( "Unexpected shape of tensor: {}".format(model.shape), "Must be scalar or have length equal to total number of faces.", ) M = sdiag(face_areas * mkvc(model)) if invert_matrix: return sdinv(M) else: return M def get_edge_inner_product_line( self, model=None, invert_model=False, invert_matrix=False, **kwargs, ): r"""Generate the edge inner product line matrix or its inverse. This method generates the inner product line matrix (or its inverse) when discrete variables are defined on mesh edges. It is also capable of constructing the inner product line matrix when diagnostic properties (e.g. integrated conductance) are defined on mesh edges. For a comprehensive description of the inner product line matrices that can be constructed with **get_edge_inner_product_line**, see *Notes*. Parameters ---------- model : None or numpy.ndarray Parameters defining the diagnostic property for every edge in the mesh. Inner product line matrices can be constructed for the following cases: - *None* : returns the basic inner product line matrix - *(n_edges)* :class:`numpy.ndarray` : returns inner product line matrix for an isotropic model. The array contains a scalar diagnostic property value for each edge. invert_model : bool, optional The inverse of *model* is used as the diagnostic property. invert_matrix : bool, optional Returns the inverse of the inner product line matrix. The inverse not implemented for full tensor properties. Returns ------- (n_edges, n_edges) scipy.sparse.csr_matrix inner product line matriz Notes ----- For continuous vector quantities :math:`\vec{u}` and :math:`\vec{w}`, and scalar physical property distribution :math:`\sigma`, we define the following inner product: .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \int_\Omega \, \vec{u} \cdot \sigma \vec{v} \, dv If the material property is distributed over a set of lines :math:`\ell_i` with cross-sectional area :math:`a`, we can define a diagnostic property value :math:`\lambda = \sigma a`. And the inner-product can be approximated by a set of line integrals as follows: .. math:: \langle \vec{u}, \sigma \vec{w} \rangle = \sum_i \int_{\ell_i} \, \vec{u} \cdot \lambda \vec{v} \, ds Let :math:`\vec{u}` and :math:`\vec{w}` have discrete representations :math:`\mathbf{u}` and :math:`\mathbf{w}` that live on the edges. Assuming the contribution of vector components perpendicular to the lines are negligible compared to parallel components, **get_edge_inner_product_line** constructs the inner product matrix :math:`\mathbf{M_\lambda}` (or its inverse :math:`\mathbf{M_\lambda^{-1}}`) such that: .. math:: \sum_i \int_{\ell_i} \, \vec{u} \cdot \lambda \vec{v} \, ds \approx \sum_i \int_{\ell_i} \, \vec{u}_\parallel \cdot \lambda \vec{v}_\parallel \, ds = \mathbf{u^T \, M_\lambda \, w} where the diagnostic properties on mesh edges (i.e. the model) are stored within an array of the form: .. math:: \boldsymbol{\lambda} = \begin{bmatrix} \boldsymbol{\lambda_x} \\ \boldsymbol{\lambda_y} \\ \boldsymbol{\lambda_z} \end{bmatrix} Examples -------- Here we provide an example of edge inner product line matrix. For simplicity, we will work on a 2 x 2 x 2 tensor mesh. As seen below, we begin by constructing and imaging the basic edge inner product line matrix. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> h = np.ones(2) >>> mesh = TensorMesh([h, h, h]) >>> Me = mesh.get_edge_inner_product_line() >>> fig = plt.figure(figsize=(6, 6)) >>> ax = fig.add_subplot(111) >>> ax.imshow(Me.todense()) >>> ax.set_title('Basic Edge Inner Product Line Matrix', fontsize=18) >>> plt.show() Next, we consider the case where the physical properties are defined by diagnostic properties on mesh edges. For the isotropic case, we show the physical property tensor for a single cell. Define the diagnostic property values for x, y and z faces. >>> tau_x, tau_y, tau_z = 3, 2, 1 Here construct and image the edge inner product line matrix for the isotropic case. Spy plots are used to demonstrate the sparsity of the matrix. >>> tau = np.r_[ >>> tau_x * np.ones(mesh.n_edges_x), >>> tau_y * np.ones(mesh.n_edges_y), >>> tau_z * np.ones(mesh.n_edges_z) >>> ] >>> M = mesh.get_edge_inner_product_line(tau) Then plot the sparse representation, >>> fig = plt.figure(figsize=(4, 4)) >>> ax1 = fig.add_subplot(111) >>> ax1.imshow(M.todense()) >>> ax1.set_title("M (isotropic)", fontsize=16) >>> plt.show() """ try: edge_lengths = self.edge_lengths except NotImplementedError: raise NotImplementedError( f"get_edge_inner_product_line not implemented for {type(self)}" ) if model is None: model = np.ones(self.nE) if invert_model: model = 1.0 / model if is_scalar(model): model = model * np.ones(self.nE) # Isotropic case only if model.size != self.nE: raise ValueError( "Unexpected shape of tensor: {}".format(model.shape), "Must be scalar or have length equal to total number of edges.", ) M = sdiag(edge_lengths * mkvc(model)) if invert_matrix: return sdinv(M) else: return M def get_face_inner_product_deriv( self, model, do_fast=True, invert_model=False, invert_matrix=False, **kwargs ): r"""Get a function handle to multiply a vector with derivative of face inner product matrix (or its inverse). Let :math:`\mathbf{M}(\mathbf{m})` be the face inner product matrix constructed with a set of physical property parameters :math:`\mathbf{m}` (or its inverse). **get_face_inner_product_deriv** constructs a function handle .. math:: \mathbf{F}(\mathbf{u}) = \mathbf{u}^T \, \frac{\partial \mathbf{M}(\mathbf{m})}{\partial \mathbf{m}} which accepts any numpy.array :math:`\mathbf{u}` of shape (n_faces,). That is, **get_face_inner_product_deriv** constructs a function handle for computing the dot product between a vector :math:`\mathbf{u}` and the derivative of the face inner product matrix (or its inverse) with respect to the property parameters. When computed, :math:`\mathbf{F}(\mathbf{u})` returns a ``scipy.sparse.csr_matrix`` of shape (n_faces, n_param). The function handle can be created for isotropic, diagonal isotropic and full tensor physical properties; see notes. Parameters ---------- model : numpy.ndarray Parameters defining the material properties for every cell in the mesh. Inner product matrices can be constructed for the following cases: - *(n_cells)* :class:`numpy.ndarray` : Isotropic case. *model* contains a scalar physical property value for each cell. - *(n_cells, dim)* :class:`numpy.ndarray` : Diagonal anisotropic case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz]``. This can also a be a 1D array with the same number of total elements in column major order. - *(n_cells, 3)* :class:`numpy.ndarray` (``dim`` is 2) or *(n_cells, 6)* :class:`numpy.ndarray` (``dim`` is 3) : Full tensor properties case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz, σ_xy, σ_xz, σ_yz]`` This can also be a 1D array with the same number of total elements in column major order. do_fast : bool, optional Do a faster implementation (if available). invert_model : bool, optional The inverse of *model* is used as the physical property. invert_matrix : bool, optional Returns the inverse of the inner product matrix. The inverse not implemented for full tensor properties. Returns ------- function The function handle :math:`\mathbf{F}(\mathbf{u})` which accepts a (``n_faces``) :class:`numpy.ndarray` :math:`\mathbf{u}`. The function returns a (``n_faces``, ``n_params``) :class:`scipy.sparse.csr_matrix`. Notes ----- Let :math:`\mathbf{M}(\mathbf{m})` be the face inner product matrix (or its inverse) for the set of physical property parameters :math:`\mathbf{m}`. And let :math:`\mathbf{u}` be a discrete quantity that lives on the faces. **get_face_inner_product_deriv** creates a function handle for computing the following: .. math:: \mathbf{F}(\mathbf{u}) = \mathbf{u}^T \, \frac{\partial \mathbf{M}(\mathbf{m})}{\partial \mathbf{m}} The dimensions of the sparse matrix constructed by computing :math:`\mathbf{F}(\mathbf{u})` for some :math:`\mathbf{u}` depends on the constitutive relation defined for each cell. These cases are summarized below. **Isotropic Case:** The physical property for each cell is defined by a scalar value. Therefore :math:`\mathbf{m}` is a (``n_cells``) :class:`numpy.ndarray`. The sparse matrix output by computing :math:`\mathbf{F}(\mathbf{u})` has shape (``n_faces``, ``n_cells``). **Diagonal Anisotropic Case:** In this case, the physical properties for each cell are defined by a diagonal tensor .. math:: \Sigma = \begin{bmatrix} \sigma_{xx} & 0 & 0 \\ 0 & \sigma_{yy} & 0 \\ 0 & 0 & \sigma_{zz} \end{bmatrix} Thus there are ``dim * n_cells`` physical property parameters and :math:`\mathbf{m}` is a (``dim * n_cells``) :class:`numpy.ndarray`. The sparse matrix output by computing :math:`\mathbf{F}(\mathbf{u})` has shape (``n_faces``, ``dim * n_cells``). **Full Tensor Case:** In this case, the physical properties for each cell are defined by a full tensor .. math:: \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ \sigma_{xy} & \sigma_{yy} & \sigma_{yz} \\ \sigma_{xz} & \sigma_{yz} & \sigma_{zz} \end{bmatrix} Thus there are ``6 * n_cells`` physical property parameters in 3 dimensions, or ``3 * n_cells`` physical property parameters in 2 dimensions, and :math:`\mathbf{m}` is a (``n_params``) :class:`numpy.ndarray`. The sparse matrix output by computing :math:`\mathbf{F}(\mathbf{u})` has shape (``n_faces``, ``n_params``). Examples -------- Here, we construct a 4 cell by 4 cell tensor mesh. For our first example we consider isotropic physical properties; that is, the physical properties of each cell are defined a scalar value. We construct the face inner product matrix and visualize it with a spy plot. We then use **get_face_inner_product_deriv** to construct the function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation of this function on a spy plot. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import matplotlib as mpl >>> mpl.rcParams.update({'font.size': 14}) >>> rng = np.random.default_rng(45) >>> mesh = TensorMesh([[(1, 4)], [(1, 4)]]) Define a model, and a random vector to multiply the derivative with, then we grab the respective derivative function and calculate the sparse matrix, >>> m = rng.random(mesh.nC) # physical property parameters >>> u = rng.random(mesh.nF) # vector of shape (n_faces) >>> Mf = mesh.get_face_inner_product(m) >>> F = mesh.get_face_inner_product_deriv(m) # Function handle >>> dFdm_u = F(u) Spy plot for the inner product matrix and its derivative >>> fig = plt.figure(figsize=(15, 5)) >>> ax1 = fig.add_axes([0.05, 0.05, 0.3, 0.85]) >>> ax1.spy(Mf, ms=6) >>> ax1.set_title("Face Inner Product Matrix (Isotropic)", fontsize=14, pad=5) >>> ax1.set_xlabel("Face Index", fontsize=12) >>> ax1.set_ylabel("Face Index", fontsize=12) >>> ax2 = fig.add_axes([0.43, 0.05, 0.17, 0.8]) >>> ax2.spy(dFdm_u, ms=6) >>> ax2.set_title( ... r"$u^T \, \dfrac{\partial M(m)}{\partial m}$ (Isotropic)", ... fontsize=14, pad=5 ... ) >>> ax2.set_xlabel("Parameter Index", fontsize=12) >>> ax2.set_ylabel("Face Index", fontsize=12) >>> plt.show() For our second example, the physical properties on the mesh are fully anisotropic; that is, the physical properties of each cell are defined by a tensor with parameters :math:`\sigma_1`, :math:`\sigma_2` and :math:`\sigma_3`. Once again we construct the face inner product matrix and visualize it with a spy plot. We then use **get_face_inner_product_deriv** to construct the function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation of this function on a spy plot. >>> m = rng.random((mesh.nC, 3)) # anisotropic physical property parameters >>> u = rng.random(mesh.nF) # vector of shape (n_faces) >>> Mf = mesh.get_face_inner_product(m) >>> F = mesh.get_face_inner_product_deriv(m) # Function handle >>> dFdm_u = F(u) Plot the anisotropic inner product matrix and its derivative matrix, >>> fig = plt.figure(figsize=(15, 5)) >>> ax1 = fig.add_axes([0.05, 0.05, 0.3, 0.8]) >>> ax1.spy(Mf, ms=6) >>> ax1.set_title("Face Inner Product (Full Tensor)", fontsize=14, pad=5) >>> ax1.set_xlabel("Face Index", fontsize=12) >>> ax1.set_ylabel("Face Index", fontsize=12) >>> ax2 = fig.add_axes([0.4, 0.05, 0.45, 0.85]) >>> ax2.spy(dFdm_u, ms=6) >>> ax2.set_title( ... r"$u^T \, \dfrac{\partial M(m)}{\partial m} \;$ (Full Tensor)", ... fontsize=14, pad=5 ... ) >>> ax2.set_xlabel("Parameter Index", fontsize=12) >>> ax2.set_ylabel("Face Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"get_face_inner_product_deriv not implemented for {type(self)}" ) def get_edge_inner_product_deriv( self, model, do_fast=True, invert_model=False, invert_matrix=False, **kwargs ): r"""Get a function handle to multiply vector with derivative of edge inner product matrix (or its inverse). Let :math:`\mathbf{M}(\mathbf{m})` be the edge inner product matrix constructed with a set of physical property parameters :math:`\mathbf{m}` (or its inverse). **get_edge_inner_product_deriv** constructs a function handle .. math:: \mathbf{F}(\mathbf{u}) = \mathbf{u}^T \, \frac{\partial \mathbf{M}(\mathbf{m})}{\partial \mathbf{m}} which accepts any numpy.array :math:`\mathbf{u}` of shape (n_edges,). That is, **get_edge_inner_product_deriv** constructs a function handle for computing the dot product between a vector :math:`\mathbf{u}` and the derivative of the edge inner product matrix (or its inverse) with respect to the property parameters. When computed, :math:`\mathbf{F}(\mathbf{u})` returns a ``scipy.sparse.csr_matrix`` of shape (n_edges, n_param). The function handle can be created for isotropic, diagonal isotropic and full tensor physical properties; see notes. Parameters ---------- model : numpy.ndarray Parameters defining the material properties for every cell in the mesh. Allows for the following cases: - *(n_cells)* :class:`numpy.ndarray` : Isotropic case. *model* contains a scalar physical property value for each cell. - *(n_cells, dim)* :class:`numpy.ndarray` : Diagonal anisotropic case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz]``. This can also a be a 1D array with the same number of total elements in column major order. - *(n_cells, 3)* :class:`numpy.ndarray` (``dim`` is 2) or *(n_cells, 6)* :class:`numpy.ndarray` (``dim`` is 3) : Full tensor properties case. Columns are ordered ``np.c_[σ_xx, σ_yy, σ_zz, σ_xy, σ_xz, σ_yz]`` This can also be a 1D array with the same number of total elements in column major order. invert_model : bool, optional The inverse of *model* is used as the physical property. invert_matrix : bool, optional Returns the function handle for the inverse of the inner product matrix The inverse not implemented for full tensor properties. do_fast : bool, optional Do a faster implementation (if available). Returns ------- function The function handle :math:`\mathbf{F}(\mathbf{u})` which accepts a (``n_edges``) :class:`numpy.ndarray` :math:`\mathbf{u}`. The function returns a (``n_edges``, ``n_params``) :class:`scipy.sparse.csr_matrix`. Notes ----- Let :math:`\mathbf{M}(\mathbf{m})` be the edge inner product matrix (or its inverse) for the set of physical property parameters :math:`\mathbf{m}`. And let :math:`\mathbf{u}` be a discrete quantity that lives on the edges. **get_edge_inner_product_deriv** creates a function handle for computing the following: .. math:: \mathbf{F}(\mathbf{u}) = \mathbf{u}^T \, \frac{\partial \mathbf{M}(\mathbf{m})}{\partial \mathbf{m}} The dimensions of the sparse matrix constructed by computing :math:`\mathbf{F}(\mathbf{u})` for some :math:`\mathbf{u}` depends on the constitutive relation defined for each cell. These cases are summarized below. **Isotropic Case:** The physical property for each cell is defined by a scalar value. Therefore :math:`\mathbf{m}` is a (``n_cells``) :class:`numpy.ndarray`. The sparse matrix output by computing :math:`\mathbf{F}(\mathbf{u})` has shape (``n_edges``, ``n_cells``). **Diagonal Anisotropic Case:** In this case, the physical properties for each cell are defined by a diagonal tensor .. math:: \Sigma = \begin{bmatrix} \sigma_{xx} & 0 & 0 \\ 0 & \sigma_{yy} & 0 \\ 0 & 0 & \sigma_{zz} \end{bmatrix} Thus there are ``dim * n_cells`` physical property parameters and :math:`\mathbf{m}` is a (``dim * n_cells``) :class:`numpy.ndarray`. The sparse matrix output by computing :math:`\mathbf{F}(\mathbf{u})` has shape (``n_edges``, ``dim * n_cells``). **Full Tensor Case:** In this case, the physical properties for each cell are defined by a full tensor .. math:: \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ \sigma_{xy} & \sigma_{yy} & \sigma_{yz} \\ \sigma_{xz} & \sigma_{yz} & \sigma_{zz} \end{bmatrix} Thus there are ``6 * n_cells`` physical property parameters in 3 dimensions, or ``3 * n_cells`` physical property parameters in 2 dimensions, and :math:`\mathbf{m}` is a (``n_params``) :class:`numpy.ndarray`. The sparse matrix output by computing :math:`\mathbf{F}(\mathbf{u})` has shape (``n_edges``, ``n_params``). Examples -------- Here, we construct a 4 cell by 4 cell tensor mesh. For our first example we consider isotropic physical properties; that is, the physical properties of each cell are defined a scalar value. We construct the edge inner product matrix and visualize it with a spy plot. We then use **get_edge_inner_product_deriv** to construct the function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation of this function on a spy plot. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import matplotlib as mpl >>> mpl.rcParams.update({'font.size': 14}) >>> rng = np.random.default_rng(45) >>> mesh = TensorMesh([[(1, 4)], [(1, 4)]]) Next we create a random isotropic model vector, and a random vector to multiply the derivative with (for illustration purposes). >>> m = rng.random(mesh.nC) # physical property parameters >>> u = rng.random(mesh.nF) # vector of shape (n_edges) >>> Me = mesh.get_edge_inner_product(m) >>> F = mesh.get_edge_inner_product_deriv(m) # Function handle >>> dFdm_u = F(u) Plot inner product matrix and its derivative matrix >>> fig = plt.figure(figsize=(15, 5)) >>> ax1 = fig.add_axes([0.05, 0.05, 0.3, 0.8]) >>> ax1.spy(Me, ms=6) >>> ax1.set_title("Edge Inner Product Matrix (Isotropic)", fontsize=14, pad=5) >>> ax1.set_xlabel("Edge Index", fontsize=12) >>> ax1.set_ylabel("Edge Index", fontsize=12) >>> ax2 = fig.add_axes([0.43, 0.05, 0.17, 0.8]) >>> ax2.spy(dFdm_u, ms=6) >>> ax2.set_title( ... r"$u^T \, \dfrac{\partial M(m)}{\partial m}$ (Isotropic)", ... fontsize=14, pad=5 ... ) >>> ax2.set_xlabel("Parameter Index", fontsize=12) >>> ax2.set_ylabel("Edge Index", fontsize=12) >>> plt.show() For our second example, the physical properties on the mesh are fully anisotropic; that is, the physical properties of each cell are defined by a tensor with parameters :math:`\sigma_1`, :math:`\sigma_2` and :math:`\sigma_3`. Once again we construct the edge inner product matrix and visualize it with a spy plot. We then use **get_edge_inner_product_deriv** to construct the function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation of this function on a spy plot. >>> m = rng.random((mesh.nC, 3)) # physical property parameters >>> u = rng.random(mesh.nF) # vector of shape (n_edges) >>> Me = mesh.get_edge_inner_product(m) >>> F = mesh.get_edge_inner_product_deriv(m) # Function handle >>> dFdm_u = F(u) Plot the anisotropic inner product matrix and its derivative matrix >>> fig = plt.figure(figsize=(15, 5)) >>> ax1 = fig.add_axes([0.05, 0.05, 0.3, 0.8]) >>> ax1.spy(Me, ms=6) >>> ax1.set_title("Edge Inner Product (Full Tensor)", fontsize=14, pad=5) >>> ax1.set_xlabel("Edge Index", fontsize=12) >>> ax1.set_ylabel("Edge Index", fontsize=12) >>> ax2 = fig.add_axes([0.4, 0.05, 0.45, 0.8]) >>> ax2.spy(dFdm_u, ms=6) >>> ax2.set_title( ... r"$u^T \, \dfrac{\partial M(m)}{\partial m} \;$ (Full Tensor)", ... fontsize=14, pad=5 ... ) >>> ax2.set_xlabel("Parameter Index", fontsize=12) >>> ax2.set_ylabel("Edge Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"get_edge_inner_product_deriv not implemented for {type(self)}" ) def get_edge_inner_product_surface_deriv( self, model, invert_model=False, invert_matrix=False, **kwargs, ): r"""Get a function handle to multiply a vector with derivative of edge inner product surface matrix (or its inverse). Let :math:`\mathbf{M}(\mathbf{m})` be the edge inner product surface matrix constructed with a set of diagnostic property parameters :math:`\mathbf{m}` (or its inverse) defined on mesh faces. **get_edge_inner_product_surface_deriv** constructs a function handle .. math:: \mathbf{F}(\mathbf{u}) = \mathbf{u}^T \, \frac{\partial \mathbf{M}(\mathbf{m})}{\partial \mathbf{m}} which accepts any numpy.array :math:`\mathbf{u}` of shape (n_edges,). That is, **get_edge_inner_product_surface_deriv** constructs a function handle for computing the dot product between a vector :math:`\mathbf{u}` and the derivative of the edge inner product surface matrix (or its inverse) with respect to the property parameters. When computed, :math:`\mathbf{F}(\mathbf{u})` returns a ``scipy.sparse.csr_matrix`` of shape (n_edges, n_param). The function handle can only be created for isotropic diagnostic properties. Parameters ---------- model : (n_faces, ) numpy.ndarray Parameters defining the diagnostic property values for every face in the mesh. invert_model : bool, optional The inverse of *model* is used as the diagnostic property. invert_matrix : bool, optional Returns the inverse of the inner product surface matrix. Returns ------- function The function handle :math:`\mathbf{F}(\mathbf{u})` which accepts a (``n_edges``) :class:`numpy.ndarray` :math:`\mathbf{u}`. The function returns a (``n_edges``, ``n_params``) :class:`scipy.sparse.csr_matrix`. """ raise NotImplementedError( f"get_edge_inner_product_surface_deriv not implemented for {type(self)}" ) def get_face_inner_product_surface_deriv( self, model, invert_model=False, invert_matrix=False, **kwargs, ): r"""Get a function handle to multiply a vector with derivative of face inner product surface matrix (or its inverse). Let :math:`\mathbf{M}(\mathbf{m})` be the face inner product surface matrix constructed with a set of diagnostic property parameters :math:`\mathbf{m}` (or its inverse) defined on mesh faces. **get_face_inner_product_surface_deriv** constructs a function handle .. math:: \mathbf{F}(\mathbf{u}) = \mathbf{u}^T \, \frac{\partial \mathbf{M}(\mathbf{m})}{\partial \mathbf{m}} which accepts any numpy.array :math:`\mathbf{u}` of shape (n_faces,). That is, **get_face_inner_product_surface_deriv** constructs a function handle for computing the dot product between a vector :math:`\mathbf{u}` and the derivative of the face inner product surface matrix (or its inverse) with respect to the property parameters. When computed, :math:`\mathbf{F}(\mathbf{u})` returns a ``scipy.sparse.csr_matrix`` of shape (n_faces, n_param). The function handle can only be created for isotropic diagnostic properties. Parameters ---------- model : (n_faces, ) numpy.ndarray Parameters defining the diagnostic property values for every face in the mesh. invert_model : bool, optional The inverse of *model* is used as the diagnostic property. invert_matrix : bool, optional Returns the inverse of the inner product surface matrix. Returns ------- function The function handle :math:`\mathbf{F}(\mathbf{u})` which accepts a (``n_faces``) :class:`numpy.ndarray` :math:`\mathbf{u}`. The function returns a (``n_faces``, ``n_params``) :class:`scipy.sparse.csr_matrix`. """ try: A = sdiag(self.face_areas) except NotImplementedError: raise NotImplementedError( f"get_face_inner_product_surface_deriv not implemented for {type(self)}" ) if model is None: tensorType = -1 elif is_scalar(model): tensorType = 0 elif model.size == self.nF: tensorType = 1 else: raise ValueError( "Unexpected shape of tensor: {}".format(model.shape), "Must be scalar or have length equal to total number of faces.", ) dMdprop = None if invert_matrix or invert_model: MI = self.get_face_inner_product_surface( model, invert_model=invert_model, invert_matrix=invert_matrix, ) if tensorType == 0: # isotropic, constant ones = sp.csr_matrix( (np.ones(self.nF), (range(self.nF), np.zeros(self.nF))), shape=(self.nF, 1), ) if not invert_matrix and not invert_model: dMdprop = A * ones elif invert_matrix and invert_model: dMdprop = sdiag(MI.diagonal() ** 2) * A * ones * sdiag(1.0 / model**2) elif invert_model: dMdprop = A * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = sdiag(-MI.diagonal() ** 2) * A elif tensorType == 1: # isotropic, variable in space if not invert_matrix and not invert_model: dMdprop = A elif invert_matrix and invert_model: dMdprop = sdiag(MI.diagonal() ** 2) * A * sdiag(1.0 / model**2) elif invert_model: dMdprop = A * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = sdiag(-MI.diagonal() ** 2) * A if dMdprop is not None: def innerProductDeriv(v): return sdiag(v) * dMdprop return innerProductDeriv else: return None def get_edge_inner_product_line_deriv( self, model, invert_model=False, invert_matrix=False, **kwargs, ): r"""Get a function handle to multiply a vector with derivative of edge inner product line matrix (or its inverse). Let :math:`\mathbf{M}(\mathbf{m})` be the edge inner product line matrix constructed with a set of diagnostic property parameters :math:`\mathbf{m}` (or its inverse) defined on mesh edges. **get_edge_inner_product_line_deriv** constructs a function handle .. math:: \mathbf{F}(\mathbf{u}) = \mathbf{u}^T \, \frac{\partial \mathbf{M}(\mathbf{m})}{\partial \mathbf{m}} which accepts any numpy.array :math:`\mathbf{u}` of shape (n_edges,). That is, **get_edge_inner_product_line_deriv** constructs a function handle for computing the dot product between a vector :math:`\mathbf{u}` and the derivative of the edge inner product line matrix (or its inverse) with respect to the diagnostic parameters. When computed, :math:`\mathbf{F}(\mathbf{u})` returns a ``scipy.sparse.csr_matrix`` of shape (n_edges, n_param). The function handle can only be created for isotropic diagnostic properties. Parameters ---------- model : (n_edges, ) numpy.ndarray Parameters defining the diagnostic property values for every edge in the mesh. invert_model : bool, optional The inverse of *model* is used as the diagnostic property. invert_matrix : bool, optional Returns the inverse of the inner product line matrix. Returns ------- function The function handle :math:`\mathbf{F}(\mathbf{u})` which accepts a (``n_edges``) :class:`numpy.ndarray` :math:`\mathbf{u}`. The function returns a (``n_edges``, ``n_params``) :class:`scipy.sparse.csr_matrix`. """ try: L = sdiag(self.edge_lengths) except NotImplementedError: raise NotImplementedError( f"get_edge_inner_product_line_deriv not implemented for {type(self)}" ) if model is None: tensorType = -1 elif is_scalar(model): tensorType = 0 elif model.size == self.nE: tensorType = 1 else: raise ValueError( "Unexpected shape of tensor: {}.".format(model.shape), "Must be scalar or have length equal to total number of edges: {}.".format( self.nE ), ) dMdprop = None if invert_matrix or invert_model: MI = self.get_edge_inner_product_line( model, invert_model=invert_model, invert_matrix=invert_matrix, ) if tensorType == 0: # isotropic, constant ones = sp.csr_matrix( (np.ones(self.nE), (range(self.nE), np.zeros(self.nE))), shape=(self.nE, 1), ) if not invert_matrix and not invert_model: dMdprop = L * ones elif invert_matrix and invert_model: dMdprop = sdiag(MI.diagonal() ** 2) * L * ones * sdiag(1.0 / model**2) elif invert_model: dMdprop = L * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = sdiag(-MI.diagonal() ** 2) * L elif tensorType == 1: # isotropic, variable in space if not invert_matrix and not invert_model: dMdprop = L elif invert_matrix and invert_model: dMdprop = sdiag(MI.diagonal() ** 2) * L * sdiag(1.0 / model**2) elif invert_model: dMdprop = L * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = sdiag(-MI.diagonal() ** 2) * L if dMdprop is not None: def innerProductDeriv(v): return sdiag(v) * dMdprop return innerProductDeriv else: return None # Averaging @property def average_face_to_cell(self): r"""Averaging operator from faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on mesh faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_faces) scipy.sparse.csr_matrix The scalar averaging operator from faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_f}` be a discrete scalar quantity that lives on mesh faces. **average_face_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{fc}}` that projects :math:`\boldsymbol{\phi_f}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{fc}} \, \boldsymbol{\phi_f} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Afc @ phi_f Examples -------- Here we compute the values of a scalar function on the faces. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a scalar variable on faces >>> phi_f = np.zeros(mesh.nF) >>> xy = mesh.faces >>> phi_f[(xy[:, 1] > 0)] = 25.0 >>> phi_f[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. >>> Afc = mesh.average_face_to_cell >>> phi_c = Afc @ phi_f And finally plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi_f, ax=ax1, v_type="F") >>> ax1.set_title("Variable at faces", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Afc, ms=1) >>> ax1.set_title("Face Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_face_to_cell not implemented for {type(self)}" ) @property def average_face_to_cell_vector(self): r"""Averaging operator from faces to cell centers (vector quantities). This property constructs the averaging operator that independently maps the Cartesian components of vector quantities from faces to cell centers. This averaging operators is used when a discrete vector quantity defined on mesh faces must be approximated at cell centers. Once constructed, the operator is stored permanently as a property of the mesh. Be aware that the Cartesian components of the original vector are defined on their respective faces; e.g. the x-component lives on x-faces. However, the x, y and z components are being averaged separately to cell centers. The operation is implemented as a matrix vector product, i.e.:: u_c = Afc @ u_f Returns ------- (dim * n_cells, n_faces) scipy.sparse.csr_matrix The vector averaging operator from faces to cell centers. Since we are averaging a vector quantity to cell centers, the first dimension of the operator is the mesh dimension times the number of cells. Notes ----- Let :math:`\mathbf{u_f}` be the discrete representation of a vector quantity whose Cartesian components are defined on their respective faces; e.g. :math:`u_x` is defined on x-faces. **average_face_to_cell_vector** constructs a discrete linear operator :math:`\mathbf{A_{fc}}` that projects each Cartesian component of :math:`\mathbf{u_f}` independently to cell centers, i.e.: .. math:: \mathbf{u_c} = \mathbf{A_{fc}} \, \mathbf{u_f} where :math:`\mathbf{u_c}` is a discrete vector quantity whose Cartesian components defined at the cell centers and organized into a 1D array of the form np.r_[ux, uy, uz]. For each cell, and for each Cartesian component, we are simply taking the average of the values defined on the cell's corresponding faces and placing the result at the cell's center. Examples -------- Here we compute the values of a vector function discretized to the mesh faces. We then create an averaging operator to approximate the function at cell centers. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = 0.5 * np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a discrete vector on mesh faces >>> faces_x = mesh.faces_x >>> faces_y = mesh.faces_y >>> u_fx = -(faces_x[:, 1] / np.sqrt(np.sum(faces_x ** 2, axis=1))) * np.exp( >>> -(faces_x[:, 0] ** 2 + faces_x[:, 1] ** 2) / 6 ** 2 >>> ) >>> u_fy = (faces_y[:, 0] / np.sqrt(np.sum(faces_y ** 2, axis=1))) * np.exp( >>> -(faces_y[:, 0] ** 2 + faces_y[:, 1] ** 2) / 6 ** 2 >>> ) >>> u_f = np.r_[u_fx, u_fy] Next, we construct the averaging operator and apply it to the discrete vector quantity to approximate the value at cell centers. >>> Afc = mesh.average_face_to_cell_vector >>> u_c = Afc @ u_f And finally, plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(u_f, ax=ax1, v_type="F", view='vec') >>> ax1.set_title("Variable at faces", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(u_c, ax=ax2, v_type="CCv", view='vec') >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Afc, ms=1) >>> ax1.set_title("Face Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Vector Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_face_to_cell_vector not implemented for {type(self)}" ) @property def average_cell_to_face(self): r"""Averaging operator from cell centers to faces (scalar quantities). This property constructs an averaging operator that maps scalar quantities from cell centers to face. This averaging operator is used when a discrete scalar quantity defined cell centers must be projected to faces. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_faces, n_cells) scipy.sparse.csr_matrix The scalar averaging operator from cell centers to faces Notes ----- Let :math:`\boldsymbol{\phi_c}` be a discrete scalar quantity that lives at cell centers. **average_cell_to_face** constructs a discrete linear operator :math:`\mathbf{A_{cf}}` that projects :math:`\boldsymbol{\phi_c}` to faces, i.e.: .. math:: \boldsymbol{\phi_f} = \mathbf{A_{cf}} \, \boldsymbol{\phi_c} where :math:`\boldsymbol{\phi_f}` approximates the value of the scalar quantity at the faces. For each face, we are performing a weighted average between the values at adjacent cell centers. In 1D, where adjacent cells :math:`i` and :math:`i+1` have widths :math:`h_i` and :math:`h_{i+1}`, :math:`\phi` on face is approximated by: .. math:: \phi_{i \! + \! 1/2} \approx \frac{h_{i+1} \phi_i + h_i \phi_{i+1}}{h_i + h_{i+1}} On boundary faces, nearest neighbour is used to extrapolate the value from the nearest cell center. Once the operator is construct, the averaging is implemented as a matrix vector product, i.e.:: phi_f = Acf @ phi_c Examples -------- Here we compute the values of a scalar function at cell centers. We then create an averaging operator to approximate the function on the faces. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Create a scalar variable at cell centers >>> phi_c = np.zeros(mesh.nC) >>> xy = mesh.cell_centers >>> phi_c[(xy[:, 1] > 0)] = 25.0 >>> phi_c[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at the faces. >>> Acf = mesh.average_cell_to_face >>> phi_f = Acf @ phi_c Plot the results >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi_c, ax=ax1, v_type="CC") >>> ax1.set_title("Variable at cell centers", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_f, ax=ax2, v_type="F") >>> ax2.set_title("Averaged to faces", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator. >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Acf, ms=1) >>> ax1.set_title("Cell Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Face Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_cell_to_face not implemented for {type(self)}" ) @property def average_cell_vector_to_face(self): r"""Averaging operator from cell centers to faces (vector quantities). This property constructs the averaging operator that independently maps the Cartesian components of vector quantities from cell centers to faces. This averaging operators is used when a discrete vector quantity defined at cell centers must be approximated on the faces. Once constructed, the operator is stored permanently as a property of the mesh. Be aware that the Cartesian components of the original vector are defined seperately at cell centers in a 1D numpy.array organized [ux, uy, uz]. Once projected to faces, the Cartesian components are defined on their respective faces; e.g. the x-component lives on x-faces. The operation is implemented as a matrix vector product, i.e.:: u_f = Acf @ u_c Returns ------- (n_faces, dim * n_cells) scipy.sparse.csr_matrix The vector averaging operator from cell centers to faces. Since we are averaging a vector quantity from cell centers, the second dimension of the operator is the mesh dimension times the number of cells. Notes ----- Let :math:`\mathbf{u_c}` be the discrete representation of a vector quantity whose Cartesian components are defined separately at cell centers. **average_cell_vector_to_face** constructs a discrete linear operator :math:`\mathbf{A_{cf}}` that projects each Cartesian component of :math:`\mathbf{u_c}` to the faces, i.e.: .. math:: \mathbf{u_f} = \mathbf{A_{cf}} \, \mathbf{u_c} where :math:`\mathbf{u_f}` is the discrete vector quantity whose Cartesian components are approximated on their respective cell faces; e.g. the x-component is approximated on x-faces. For each face (x, y or z), we are simply taking a weighted average between the values of the correct Cartesian component at the corresponding cell centers. E.g. for the x-component, which is projected to x-faces, the weighted average on a 2D mesh would be: .. math:: u_x(i \! + \! 1/2, j) = \frac{h_{i+1} u_x (i,j) + h_i u_x(i \! + \! 1,j)}{hx_i + hx_{i+1}} where :math:`h_i` and :math:`h_{i+1}` represent the cell respective cell widths in the x-direction. For boundary faces, nearest neighbor is used to extrapolate the values. Examples -------- Here we compute the values of a vector function discretized to cell centers. We then create an averaging operator to approximate the function on the faces. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = 0.5 * np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a discrete vector at cell centers, >>> centers = mesh.cell_centers >>> u_x = -(centers[:, 1] / np.sqrt(np.sum(centers ** 2, axis=1))) * np.exp( ... -(centers[:, 0] ** 2 + centers[:, 1] ** 2) / 6 ** 2 ... ) >>> u_y = (centers[:, 0] / np.sqrt(np.sum(centers ** 2, axis=1))) * np.exp( ... -(centers[:, 0] ** 2 + centers[:, 1] ** 2) / 6 ** 2 ... ) >>> u_c = np.r_[u_x, u_y] Next, we construct the averaging operator and apply it to the discrete vector quantity to approximate the value on the faces. >>> Acf = mesh.average_cell_vector_to_face >>> u_f = Acf @ u_c And plot the results >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(u_c, ax=ax1, v_type="CCv", view='vec') >>> ax1.set_title("Variable at faces", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(u_f, ax=ax2, v_type="F", view='vec') >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Acf, ms=1) >>> ax1.set_title("Cell Vector Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Face Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_cell_vector_to_face not implemented for {type(self)}" ) @property def average_cell_to_edge(self): r"""Averaging operator from cell centers to edges (scalar quantities). This property constructs an averaging operator that maps scalar quantities from cell centers to edge. This averaging operator is used when a discrete scalar quantity defined cell centers must be projected to edges. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_edges, n_cells) scipy.sparse.csr_matrix The scalar averaging operator from edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_c}` be a discrete scalar quantity that lives at cell centers. **average_cell_to_edge** constructs a discrete linear operator :math:`\mathbf{A_{ce}}` that projects :math:`\boldsymbol{\phi_c}` to edges, i.e.: .. math:: \boldsymbol{\phi_e} = \mathbf{A_{ce}} \, \boldsymbol{\phi_c} where :math:`\boldsymbol{\phi_e}` approximates the value of the scalar quantity at the edges. For each edge, we are performing a weighted average between the values at adjacent cell centers. In 1D, where adjacent cells :math:`i` and :math:`i+1` have widths :math:`h_i` and :math:`h_{i+1}`, :math:`\phi` on edge (node location in 1D) is approximated by: .. math:: \phi_{i \! + \! 1/2} \approx \frac{h_{i+1} \phi_i + h_i \phi_{i+1}}{h_i + h_{i+1}} On boundary edges, nearest neighbour is used to extrapolate the value from the nearest cell center. Once the operator is construct, the averaging is implemented as a matrix vector product, i.e.:: phi_e = Ace @ phi_c Examples -------- Here we compute the values of a scalar function at cell centers. We then create an averaging operator to approximate the function on the edges. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a scalar variable at cell centers >>> phi_c = np.zeros(mesh.nC) >>> xy = mesh.cell_centers >>> phi_c[(xy[:, 1] > 0)] = 25.0 >>> phi_c[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at the edges. >>> Ace = mesh.average_cell_to_edge >>> phi_e = Ace @ phi_c And plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi_c, ax=ax1, v_type="CC") >>> ax1.set_title("Variable at cell centers", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_e, ax=ax2, v_type="E") >>> ax2.set_title("Averaged to edges", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator. >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Ace, ms=1) >>> ax1.set_title("Cell Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Edge Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_cell_to_edge not implemented for {type(self)}" ) @property def average_edge_to_cell(self): r"""Averaging operator from edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on mesh edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges) scipy.sparse.csr_matrix The scalar averaging operator from edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_e}` be a discrete scalar quantity that lives on mesh edges. **average_edge_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{ec}}` that projects :math:`\boldsymbol{\phi_e}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{ec}} \, \boldsymbol{\phi_e} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Aec @ phi_e Examples -------- Here we compute the values of a scalar function on the edges. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a scalar variable on edges, >>> phi_e = np.zeros(mesh.nE) >>> xy = mesh.edges >>> phi_e[(xy[:, 1] > 0)] = 25.0 >>> phi_e[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. >>> Aec = mesh.average_edge_to_cell >>> phi_c = Aec @ phi_e And plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi_e, ax=ax1, v_type="E") >>> ax1.set_title("Variable at edges", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Aec, ms=1) >>> ax1.set_title("Edge Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_edge_to_cell not implemented for {type(self)}" ) @property def average_edge_to_cell_vector(self): r"""Averaging operator from edges to cell centers (vector quantities). This property constructs the averaging operator that independently maps the Cartesian components of vector quantities from edges to cell centers. This averaging operators is used when a discrete vector quantity defined on mesh edges must be approximated at cell centers. Once constructed, the operator is stored permanently as a property of the mesh. Be aware that the Cartesian components of the original vector are defined on their respective edges; e.g. the x-component lives on x-edges. However, the x, y and z components are being averaged separately to cell centers. The operation is implemented as a matrix vector product, i.e.:: u_c = Aec @ u_e Returns ------- (dim * n_cells, n_edges) scipy.sparse.csr_matrix The vector averaging operator from edges to cell centers. Since we are averaging a vector quantity to cell centers, the first dimension of the operator is the mesh dimension times the number of cells. Notes ----- Let :math:`\mathbf{u_e}` be the discrete representation of a vector quantity whose Cartesian components are defined on their respective edges; e.g. :math:`u_x` is defined on x-edges. **average_edge_to_cell_vector** constructs a discrete linear operator :math:`\mathbf{A_{ec}}` that projects each Cartesian component of :math:`\mathbf{u_e}` independently to cell centers, i.e.: .. math:: \mathbf{u_c} = \mathbf{A_{ec}} \, \mathbf{u_e} where :math:`\mathbf{u_c}` is a discrete vector quantity whose Cartesian components defined at the cell centers and organized into a 1D array of the form np.r_[ux, uy, uz]. For each cell, and for each Cartesian component, we are simply taking the average of the values defined on the cell's corresponding edges and placing the result at the cell's center. Examples -------- Here we compute the values of a vector function discretized to the edges. We then create an averaging operator to approximate the function at cell centers. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = 0.5 * np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a discrete vector on mesh edges >>> edges_x = mesh.edges_x >>> edges_y = mesh.edges_y >>> u_ex = -(edges_x[:, 1] / np.sqrt(np.sum(edges_x ** 2, axis=1))) * np.exp( ... -(edges_x[:, 0] ** 2 + edges_x[:, 1] ** 2) / 6 ** 2 ... ) >>> u_ey = (edges_y[:, 0] / np.sqrt(np.sum(edges_y ** 2, axis=1))) * np.exp( ... -(edges_y[:, 0] ** 2 + edges_y[:, 1] ** 2) / 6 ** 2 ... ) >>> u_e = np.r_[u_ex, u_ey] Next, we construct the averaging operator and apply it to the discrete vector quantity to approximate the value at cell centers. >>> Aec = mesh.average_edge_to_cell_vector >>> u_c = Aec @ u_e And plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(u_e, ax=ax1, v_type="E", view='vec') >>> ax1.set_title("Variable at edges", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(u_c, ax=ax2, v_type="CCv", view='vec') >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Aec, ms=1) >>> ax1.set_title("Edge Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Vector Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_edge_to_cell_vector not implemented for {type(self)}" ) @property def average_edge_to_face(self): r"""Averaging operator from edges to faces. This property constructs the averaging operator that maps uantities from edges to faces. This averaging operators is used when a discrete quantity defined on mesh edges must be approximated at faces. The operation is implemented as a matrix vector product, i.e.:: u_f = Aef @ u_e Once constructed, the operator is stored permanently as a property of the mesh. Returns ------- (n_faces, n_edges) scipy.sparse.csr_matrix The averaging operator from edges to faces. Notes ----- Let :math:`\mathbf{u_e}` be the discrete representation of aquantity whose that is defined on the edges. **average_edge_to_face** constructs a discrete linear operator :math:`\mathbf{A_{ef}}` that projects :math:`\mathbf{u_e}` to its corresponding face, i.e.: .. math:: \mathbf{u_f} = \mathbf{A_{ef}} \, \mathbf{u_e} where :math:`\mathbf{u_f}` is a quantity defined on the respective faces. Examples -------- Here we compute the values of a vector function discretized to the edges. We then create an averaging operator to approximate the function on the faces. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = 0.5 * np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Create a discrete vector on mesh edges >>> edges = mesh.edges >>> u_ex = -(edges[:, 1] / np.sqrt(np.sum(edges ** 2, axis=1))) * np.exp( ... -(edges[:, 0] ** 2 + edges[:, 1] ** 2) / 6 ** 2 ... ) >>> u_ey = (edges[:, 0] / np.sqrt(np.sum(edges ** 2, axis=1))) * np.exp( ... -(edges[:, 0] ** 2 + edges[:, 1] ** 2) / 6 ** 2 ... ) >>> u_e = np.c_[u_ex, u_ey] Next, we construct the averaging operator and apply it to the discrete vector quantity to approximate the value at the faces. >>> Aef = mesh.average_edge_to_face >>> u_f = Aef @ u_e Plot the results, >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> proj_ue = mesh.project_edge_vector(u_e) >>> mesh.plot_image(proj_ue, ax=ax1, v_type="E", view='vec') >>> ax1.set_title("Variable at edges", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> proj_uf = mesh.project_face_vector(u_f) >>> mesh.plot_image(proj_uf, ax=ax2, v_type="F", view='vec') >>> ax2.set_title("Averaged to faces", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Aef, ms=1) >>> ax1.set_title("Edge Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Face Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_edge_to_face not implemented for {type(self)}" ) @property def average_node_to_cell(self): r"""Averaging operator from nodes to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from nodes to cell centers. This averaging operator is used when a discrete scalar quantity defined on mesh nodes must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_nodes) scipy.sparse.csr_matrix The scalar averaging operator from nodes to cell centers Notes ----- Let :math:`\boldsymbol{\phi_n}` be a discrete scalar quantity that lives on mesh nodes. **average_node_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{nc}}` that projects :math:`\boldsymbol{\phi_f}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{nc}} \, \boldsymbol{\phi_n} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its nodes. The operation is implemented as a matrix vector product, i.e.:: phi_c = Anc @ phi_n Examples -------- Here we compute the values of a scalar function on the nodes. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we Create a scalar variable on nodes >>> phi_n = np.zeros(mesh.nN) >>> xy = mesh.nodes >>> phi_n[(xy[:, 1] > 0)] = 25.0 >>> phi_n[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. >>> Anc = mesh.average_node_to_cell >>> phi_c = Anc @ phi_n Plot the results, >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi_n, ax=ax1, v_type="N") >>> ax1.set_title("Variable at nodes", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Anc, ms=1) >>> ax1.set_title("Node Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_node_to_cell not implemented for {type(self)}" ) @property def average_node_to_edge(self): r"""Averaging operator from nodes to edges (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from nodes to edges; scalar at edges is organized in a 1D numpy.array of the form [x-edges, y-edges, z-edges]. This averaging operator is used when a discrete scalar quantity defined on mesh nodes must be projected to edges. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_edges, n_nodes) scipy.sparse.csr_matrix The scalar averaging operator from nodes to edges Notes ----- Let :math:`\boldsymbol{\phi_n}` be a discrete scalar quantity that lives on mesh nodes. **average_node_to_edge** constructs a discrete linear operator :math:`\mathbf{A_{ne}}` that projects :math:`\boldsymbol{\phi_n}` to edges, i.e.: .. math:: \boldsymbol{\phi_e} = \mathbf{A_{ne}} \, \boldsymbol{\phi_n} where :math:`\boldsymbol{\phi_e}` approximates the value of the scalar quantity at edges. For each edge, we are simply averaging the values defined on the nodes it connects. The operation is implemented as a matrix vector product, i.e.:: phi_e = Ane @ phi_n Examples -------- Here we compute the values of a scalar function on the nodes. We then create an averaging operator to approximate the function at the edges. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a scalar variable on nodes, >>> phi_n = np.zeros(mesh.nN) >>> xy = mesh.nodes >>> phi_n[(xy[:, 1] > 0)] = 25.0 >>> phi_n[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value on the edges. >>> Ane = mesh.average_node_to_edge >>> phi_e = Ane @ phi_n Plot the results, >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi_n, ax=ax1, v_type="N") >>> ax1.set_title("Variable at nodes") >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_e, ax=ax2, v_type="E") >>> ax2.set_title("Averaged to edges") >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Ane, ms=1) >>> ax1.set_title("Node Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Edge Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_node_to_edge not implemented for {type(self)}" ) @property def average_node_to_face(self): r"""Averaging operator from nodes to faces (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from nodes to edges; scalar at faces is organized in a 1D numpy.array of the form [x-faces, y-faces, z-faces]. This averaging operator is used when a discrete scalar quantity defined on mesh nodes must be projected to faces. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_faces, n_nodes) scipy.sparse.csr_matrix The scalar averaging operator from nodes to faces Notes ----- Let :math:`\boldsymbol{\phi_n}` be a discrete scalar quantity that lives on mesh nodes. **average_node_to_face** constructs a discrete linear operator :math:`\mathbf{A_{nf}}` that projects :math:`\boldsymbol{\phi_n}` to faces, i.e.: .. math:: \boldsymbol{\phi_f} = \mathbf{A_{nf}} \, \boldsymbol{\phi_n} where :math:`\boldsymbol{\phi_f}` approximates the value of the scalar quantity at faces. For each face, we are simply averaging the values at the nodes which outline the face. The operation is implemented as a matrix vector product, i.e.:: phi_f = Anf @ phi_n Examples -------- Here we compute the values of a scalar function on the nodes. We then create an averaging operator to approximate the function at the faces. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we, create a scalar variable on nodes >>> phi_n = np.zeros(mesh.nN) >>> xy = mesh.nodes >>> phi_n[(xy[:, 1] > 0)] = 25.0 >>> phi_n[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value on the faces. >>> Anf = mesh.average_node_to_face >>> phi_f = Anf @ phi_n Plot the results, >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi_n, ax=ax1, v_type="N") >>> ax1.set_title("Variable at nodes") >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_f, ax=ax2, v_type="F") >>> ax2.set_title("Averaged to faces") >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Anf, ms=1) >>> ax1.set_title("Node Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Face Index", fontsize=12) >>> plt.show() """ raise NotImplementedError( f"average_node_to_face not implemented for {type(self)}" ) @property def project_face_to_boundary_face(self): r"""Projection matrix from all faces to boundary faces. Constructs and returns a matrix :math:`\mathbf{P}` that projects from all mesh faces to boundary faces. That is, for a discrete vector :math:`\mathbf{u}` that lives on the faces, the values on the boundary faces :math:`\mathbf{u_b}` can be extracted via the following matrix-vector product:: ub = P @ u Returns ------- scipy.sparse.csr_matrix (n_boundary_faces, n_faces) Projection matrix with shape """ raise NotImplementedError( f"project_face_to_boundary_face not implemented for {type(self)}" ) @property def project_edge_to_boundary_edge(self): r"""Projection matrix from all edges to boundary edges. Constructs and returns a matrix :math:`\mathbf{P}` that projects from all mesh edges to boundary edges. That is, for a discrete vector :math:`\mathbf{u}` that lives on the edges, the values on the boundary edges :math:`\mathbf{u_b}` can be extracted via the following matrix-vector product:: ub = P @ u Returns ------- (n_boundary_edges, n_edges) scipy.sparse.csr_matrix Projection matrix with shape """ raise NotImplementedError( f"project_edge_to_boundary_edge not implemented for {type(self)}" ) @property def project_node_to_boundary_node(self): r"""Projection matrix from all nodes to boundary nodes. Constructs and returns a matrix :math:`\mathbf{P}` that projects from all mesh nodes to boundary nodes. That is, for a discrete scalar :math:`\mathbf{u}` that lives on the nodes, the values on the boundary nodes :math:`\mathbf{u_b}` can be extracted via the following matrix-vector product:: ub = P @ u Returns ------- (n_boundary_nodes, n_nodes) scipy.sparse.csr_matrix Projection matrix with shape """ raise NotImplementedError( f"project_node_to_boundary_node not implemented for {type(self)}" ) def closest_points_index(self, locations, grid_loc="CC", discard=False): """Find the indicies for the nearest grid location for a set of points. Parameters ---------- locations : (n, dim) numpy.ndarray Points to query. grid_loc : {'CC', 'N', 'Fx', 'Fy', 'Fz', 'Ex', 'Ex', 'Ey', 'Ez'} Specifies the grid on which points are being moved to. discard : bool, optional Whether to discard the intenally created `scipy.spatial.KDTree`. Returns ------- (n ) numpy.ndarray of int Vector of length *n* containing the indicies for the closest respective cell center, node, face or edge. Examples -------- Here we define a set of random (x, y) locations and find the closest cell centers and nodes on a mesh. >>> from discretize import TensorMesh >>> from discretize.utils import closest_points_index >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = 2*np.ones(5) >>> mesh = TensorMesh([h, h], x0='00') Define some random locations, grid cell centers and grid nodes, >>> xy_random = np.random.uniform(0, 10, size=(4,2)) >>> xy_centers = mesh.cell_centers >>> xy_nodes = mesh.nodes Find indicies of closest cell centers and nodes, >>> ind_centers = mesh.closest_points_index(xy_random, 'cell_centers') >>> ind_nodes = mesh.closest_points_index(xy_random, 'nodes') Plot closest cell centers and nodes >>> fig = plt.figure(figsize=(5, 5)) >>> ax = fig.add_axes([0.1, 0.1, 0.8, 0.8]) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(xy_random[:, 0], xy_random[:, 1], 50, 'k') >>> ax.scatter(xy_centers[ind_centers, 0], xy_centers[ind_centers, 1], 50, 'r') >>> ax.scatter(xy_nodes[ind_nodes, 0], xy_nodes[ind_nodes, 1], 50, 'b') >>> plt.show() """ locations = as_array_n_by_dim(locations, self.dim) grid_loc = self._parse_location_type(grid_loc) tree_name = f"_{grid_loc}_tree" tree = getattr(self, tree_name, None) if tree is None: grid = as_array_n_by_dim(getattr(self, grid_loc), self.dim) tree = KDTree(grid) _, ind = tree.query(locations) if not discard: setattr(self, tree_name, tree) return ind def point2index(self, locs): """Find cells that contain the given points. Returns an array of index values of the cells that contain the given points Parameters ---------- locs: (N, dim) array_like points to search for the location of Returns ------- (N) array_like of int Cell indices that contain the points """ raise NotImplementedError(f"point2index not implemented for {type(self)}") def get_interpolation_matrix( self, loc, location_type="cell_centers", zeros_outside=False, **kwargs ): """Construct a linear interpolation matrix from mesh. This method constructs a linear interpolation matrix from tensor locations (nodes, cell-centers, faces, etc...) on the mesh to a set of arbitrary locations. Parameters ---------- loc : (n_pts, dim) numpy.ndarray Location of points being to interpolate to. Must have same dimensions as the mesh. location_type : str, optional Tensor locations on the mesh being interpolated from. *location_type* must be one of: - 'Ex', 'edges_x' -> x-component of field defined on x edges - 'Ey', 'edges_y' -> y-component of field defined on y edges - 'Ez', 'edges_z' -> z-component of field defined on z edges - 'Fx', 'faces_x' -> x-component of field defined on x faces - 'Fy', 'faces_y' -> y-component of field defined on y faces - 'Fz', 'faces_z' -> z-component of field defined on z faces - 'N', 'nodes' -> scalar field defined on nodes - 'CC', 'cell_centers' -> scalar field defined on cell centers - 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers - 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers - 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers zeros_outside : bool, optional If *False*, nearest neighbour is used to compute the interpolate value at locations outside the mesh. If *True* , values at locations outside the mesh will be zero. Returns ------- (n_pts, n_loc_type) scipy.sparse.csr_matrix A sparse matrix which interpolates the specified tensor quantity on mesh to the set of specified locations. Examples -------- Here is a 1D example where a function evaluated on the nodes is interpolated to a set of random locations. To compare the accuracy, the function is evaluated at the set of random locations. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> rng = np.random.default_rng(14) >>> locs = rng.random(50)*0.8+0.1 >>> dense = np.linspace(0, 1, 200) >>> fun = lambda x: np.cos(2*np.pi*x) >>> hx = 0.125 * np.ones(8) >>> mesh1D = TensorMesh([hx]) >>> Q = mesh1D.get_interpolation_matrix(locs, 'nodes') >>> plt.figure(figsize=(5, 3)) >>> plt.plot(dense, fun(dense), ':', c="C0", lw=3, label="True Function") >>> plt.plot(mesh1D.nodes, fun(mesh1D.nodes), 's', c="C0", ms=8, label="True sampled") >>> plt.plot(locs, Q*fun(mesh1D.nodes), 'o', ms=4, label="Interpolated") >>> plt.legend() >>> plt.show() Here, demonstrate a similar example on a 2D mesh using a 2D Gaussian distribution. We interpolate the Gaussian from the nodes to cell centers and examine the relative error. >>> hx = np.ones(10) >>> hy = np.ones(10) >>> mesh2D = TensorMesh([hx, hy], x0='CC') >>> def fun(x, y): ... return np.exp(-(x**2 + y**2)/2**2) >>> nodes = mesh2D.nodes >>> val_nodes = fun(nodes[:, 0], nodes[:, 1]) >>> centers = mesh2D.cell_centers >>> val_centers = fun(centers[:, 0], centers[:, 1]) >>> A = mesh2D.get_interpolation_matrix(centers, 'nodes') >>> val_interp = A.dot(val_nodes) >>> fig = plt.figure(figsize=(11,3.3)) >>> clim = (0., 1.) >>> ax1 = fig.add_subplot(131) >>> ax2 = fig.add_subplot(132) >>> ax3 = fig.add_subplot(133) >>> mesh2D.plot_image(val_centers, ax=ax1, clim=clim) >>> mesh2D.plot_image(val_interp, ax=ax2, clim=clim) >>> mesh2D.plot_image(val_centers-val_interp, ax=ax3, clim=clim) >>> ax1.set_title('Analytic at Centers') >>> ax2.set_title('Interpolated from Nodes') >>> ax3.set_title('Relative Error') >>> plt.show() """ raise NotImplementedError( f"get_interpolation_matrix not implemented for {type(self)}" ) def _parse_location_type(self, location_type): if len(location_type) == 0: return location_type elif location_type[0] == "F": if len(location_type) > 1: return "faces_" + location_type[-1] else: return "faces" elif location_type[0] == "E": if len(location_type) > 1: return "edges_" + location_type[-1] else: return "edges" elif location_type[0] == "N": return "nodes" elif location_type[0] == "C": if len(location_type) > 2: return "cell_centers_" + location_type[-1] else: return "cell_centers" else: return location_type # DEPRECATED normals = deprecate_property( "face_normals", "normals", removal_version="1.0.0", error=True ) tangents = deprecate_property( "edge_tangents", "tangents", removal_version="1.0.0", error=True ) projectEdgeVector = deprecate_method( "project_edge_vector", "projectEdgeVector", removal_version="1.0.0", error=True, ) projectFaceVector = deprecate_method( "project_face_vector", "projectFaceVector", removal_version="1.0.0", error=True, ) getInterpolationMat = deprecate_method( "get_interpolation_matrix", "getInterpolationMat", removal_version="1.0.0", error=True, ) nodalGrad = deprecate_property( "nodal_gradient", "nodalGrad", removal_version="1.0.0", error=True ) nodalLaplacian = deprecate_property( "nodal_laplacian", "nodalLaplacian", removal_version="1.0.0", error=True ) faceDiv = deprecate_property( "face_divergence", "faceDiv", removal_version="1.0.0", error=True ) edgeCurl = deprecate_property( "edge_curl", "edgeCurl", removal_version="1.0.0", error=True ) getFaceInnerProduct = deprecate_method( "get_face_inner_product", "getFaceInnerProduct", removal_version="1.0.0", error=True, ) getEdgeInnerProduct = deprecate_method( "get_edge_inner_product", "getEdgeInnerProduct", removal_version="1.0.0", error=True, ) getFaceInnerProductDeriv = deprecate_method( "get_face_inner_product_deriv", "getFaceInnerProductDeriv", removal_version="1.0.0", error=True, ) getEdgeInnerProductDeriv = deprecate_method( "get_edge_inner_product_deriv", "getEdgeInnerProductDeriv", removal_version="1.0.0", error=True, ) vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0", error=True) area = deprecate_property("face_areas", "area", removal_version="1.0.0", error=True) edge = deprecate_property( "edge_lengths", "edge", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/base/base_regular_mesh.py ================================================ """Base classes for all regular shaped meshes supported in ``discretize``.""" import numpy as np from discretize.utils import mkvc, Identity from discretize.base.base_mesh import BaseMesh from discretize.utils.code_utils import deprecate_method class BaseRegularMesh(BaseMesh): """Base Regular mesh class for the ``discretize`` package. The ``BaseRegularMesh`` class does all the basic counting and organizing you wouldn't want to do manually. ``BaseRegularMesh`` is a class that should always be inherited by meshes with a regular structure; e.g. :class:`~discretize.TensorMesh`, :class:`~discretize.CylindricalMesh`, :class:`~discretize.TreeMesh` or :class:`~discretize.CurvilinearMesh`. Parameters ---------- shape_cells : array_like of int number of cells in each dimension origin : array_like of float, optional origin of the bottom south west corner of the mesh, defaults to 0. orientation : discretize.utils.Identity or array_like of float, optional Orientation of the three major axes of the mesh; defaults to :class:`~discretize.utils.Identity`. If provided, this must be an orthogonal matrix with the correct dimension. reference_system : {'cartesian', 'cylindrical', 'spherical'} Can also be a shorthand version of these, e.g. {'car[t]', 'cy[l]', 'sph'} """ _aliases = { **BaseMesh._aliases, "nEx": "n_edges_x", "nEy": "n_edges_y", "nEz": "n_edges_z", "vnE": "n_edges_per_direction", "nFx": "n_faces_x", "nFy": "n_faces_y", "nFz": "n_faces_z", "vnF": "n_faces_per_direction", "vnC": "shape_cells", } _items = {"shape_cells", "origin", "orientation", "reference_system"} # Instantiate the class def __init__( self, shape_cells, origin=None, orientation=None, reference_system=None, **kwargs, ): if "n" in kwargs: shape_cells = kwargs.pop("n") if "x0" in kwargs: origin = kwargs.pop("x0") axis_u = kwargs.pop("axis_u", None) axis_v = kwargs.pop("axis_v", None) axis_w = kwargs.pop("axis_w", None) if axis_u is not None and axis_v is not None and axis_w is not None: orientation = np.array([axis_u, axis_v, axis_w]) shape_cells = tuple((int(val) for val in shape_cells)) self._shape_cells = shape_cells # some default values if origin is None: origin = np.zeros(self.dim) self.origin = origin if orientation is None: orientation = Identity() self.orientation = orientation if reference_system is None: reference_system = "cartesian" self.reference_system = reference_system super().__init__(**kwargs) @property def origin(self): """Origin or 'anchor point' of the mesh. For a mesh defined in Cartesian coordinates (e.g. :class:`~discretize.TensorMesh`, :class:`~discretize.CylindricalMesh`, :class:`~discretize.TreeMesh`), *origin* is the bottom southwest corner. For a :class:`~discretize.CylindricalMesh`, *origin* is the bottom of the axis of rotational symmetry for the mesh (i.e. bottom of z-axis). Returns ------- (dim) numpy.ndarray of float origin location """ return self._origin @origin.setter def origin(self, value): # ensure the value is a numpy array value = np.asarray(value, dtype=np.float64) value = np.atleast_1d(value) if len(value) != self.dim: raise ValueError( f"origin and shape must be the same length, got {len(value)} and {self.dim}" ) self._origin = value @property def shape_cells(self): """Number of cells in each coordinate direction. For meshes of class :class:`~discretize.TensorMesh`, :class:`~discretize.CylindricalMesh` or :class:`~discretize.CurvilinearMesh`, **shape_cells** returns the number of cells along each coordinate axis direction. For mesh of class :class:`~discretize.TreeMesh`, *shape_cells* returns the number of underlying tensor mesh cells along each coordinate direction. Returns ------- (dim) tuple of int the number of cells in each coordinate direcion Notes ----- Property also accessible as using the shorthand **vnC** """ return self._shape_cells @property def orientation(self): """Rotation matrix defining mesh axes relative to Cartesian. This property returns a rotation matrix between the local coordinate axes of the mesh and the standard Cartesian axes. For a 3D mesh, this would define the x, y and z axes of the mesh relative to the Easting, Northing and elevation directions. The *orientation* property can be used to transform locations from a local coordinate system to a conventional Cartesian system. By default, *orientation* is an identity matrix of shape (mesh.dim, mesh.dim). Returns ------- (dim, dim) numpy.ndarray of float Square rotation matrix defining orientation Examples -------- For a visual example of this, please see the figure in the docs for :class:`~discretize.mixins.InterfaceVTK`. """ return self._orientation @orientation.setter def orientation(self, value): if isinstance(value, Identity): self._orientation = np.identity(self.dim) else: R = np.atleast_2d(np.asarray(value, dtype=np.float64)) dim = self.dim if R.shape != (dim, dim): raise ValueError( f"Orientation matrix must be square and of shape {(dim, dim)}, got {R.shape}" ) # Ensure each row is unitary R = R / np.linalg.norm(R, axis=1)[:, None] # Check if matrix is orthogonal if not np.allclose(R @ R.T, np.identity(self.dim), rtol=1.0e-5, atol=1e-6): raise ValueError("Orientation matrix is not orthogonal") self._orientation = R @property def reference_system(self): """Coordinate reference system. The type of coordinate reference frame. Will be one of the values "cartesian", "cylindrical", or "spherical". Returns ------- str {'cartesian', 'cylindrical', 'spherical'} The coordinate system associated with the mesh. """ return self._reference_system @reference_system.setter def reference_system(self, value): """Check if the reference system is of a known type.""" choices = ["cartesian", "cylindrical", "spherical"] # Here are a few abbreviations that users can harnes abrevs = { "car": choices[0], "cart": choices[0], "cy": choices[1], "cyl": choices[1], "sph": choices[2], } # Get the name and fix it if it is abbreviated value = value.lower() value = abrevs.get(value, value) if value not in choices: raise ValueError( "Coordinate system ({}) unknown.".format(self.reference_system) ) self._reference_system = value @property def x0(self): """Alias for the :py:attr:`~.BaseRegularMesh.origin`. See Also -------- origin """ return self.origin @x0.setter def x0(self, val): self.origin = val @property def dim(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return len(self.shape_cells) @property def n_cells(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return int(np.prod(self.shape_cells)) @property def n_nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return int(np.prod([x + 1 for x in self.shape_cells])) @property def n_edges_x(self): """Number of x-edges in the mesh. This property returns the number of edges that are parallel to the x-axis; i.e. x-edges. Returns ------- int Number of x-edges in the mesh Notes ----- Property also accessible as using the shorthand **nEx** """ return int(np.prod([x + y for x, y in zip(self.shape_cells, (0, 1, 1))])) @property def n_edges_y(self): """Number of y-edges in the mesh. This property returns the number of edges that are parallel to the y-axis; i.e. y-edges. Returns ------- int Number of y-edges in the mesh Notes ----- Property also accessible as using the shorthand **nEy** """ if self.dim < 2: return None return int(np.prod([x + y for x, y in zip(self.shape_cells, (1, 0, 1))])) @property def n_edges_z(self): """Number of z-edges in the mesh. This property returns the number of edges that are parallel to the z-axis; i.e. z-edges. Returns ------- int Number of z-edges in the mesh Notes ----- Property also accessible as using the shorthand **nEz** """ if self.dim < 3: return None return int(np.prod([x + y for x, y in zip(self.shape_cells, (1, 1, 0))])) @property def n_edges_per_direction(self): """The number of edges in each direction. This property returns a tuple with the number of edges in each axis direction of the mesh. For a 3D mesh, *n_edges_per_direction* would return a tuple of the form (nEx, nEy, nEz). Thus the length of the tuple depends on the dimension of the mesh. Returns ------- (dim) tuple of int Number of edges in each direction Notes ----- Property also accessible as using the shorthand **vnE** Examples -------- >>> import discretize >>> import matplotlib.pyplot as plt >>> import numpy as np >>> M = discretize.TensorMesh([np.ones(n) for n in [2,3]]) >>> M.plot_grid(edges=True) >>> plt.show() """ return tuple( x for x in [self.n_edges_x, self.n_edges_y, self.n_edges_z] if x is not None ) @property def n_edges(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh n = self.n_edges_x if self.dim > 1: n += self.n_edges_y if self.dim > 2: n += self.n_edges_z return n @property def n_faces_x(self): """Number of x-faces in the mesh. This property returns the number of faces whose normal vector is parallel to the x-axis; i.e. x-faces. Returns ------- int Number of x-faces in the mesh Notes ----- Property also accessible as using the shorthand **nFx** """ return int(np.prod([x + y for x, y in zip(self.shape_cells, (1, 0, 0))])) @property def n_faces_y(self): """Number of y-faces in the mesh. This property returns the number of faces whose normal vector is parallel to the y-axis; i.e. y-faces. Returns ------- int Number of y-faces in the mesh Notes ----- Property also accessible as using the shorthand **nFy** """ if self.dim < 2: return None return int(np.prod([x + y for x, y in zip(self.shape_cells, (0, 1, 0))])) @property def n_faces_z(self): """Number of z-faces in the mesh. This property returns the number of faces whose normal vector is parallel to the z-axis; i.e. z-faces. Returns ------- int Number of z-faces in the mesh Notes ----- Property also accessible as using the shorthand **nFz** """ if self.dim < 3: return None return int(np.prod([x + y for x, y in zip(self.shape_cells, (0, 0, 1))])) @property def n_faces_per_direction(self): """The number of faces in each axis direction. This property returns a tuple with the number of faces in each axis direction of the mesh. For a 3D mesh, *n_faces_per_direction* would return a tuple of the form (nFx, nFy, nFz). Thus the length of the tuple depends on the dimension of the mesh. Returns ------- (dim) tuple of int Number of faces in each axis direction Notes ----- Property also accessible as using the shorthand **vnF** Examples -------- >>> import discretize >>> import numpy as np >>> import matplotlib.pyplot as plt >>> M = discretize.TensorMesh([np.ones(n) for n in [2,3]]) >>> M.plot_grid(faces=True) >>> plt.show() """ return tuple( x for x in [self.n_faces_x, self.n_faces_y, self.n_faces_z] if x is not None ) @property def n_faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh n = self.n_faces_x if self.dim > 1: n += self.n_faces_y if self.dim > 2: n += self.n_faces_z return n @property def face_normals(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: nX = np.c_[np.ones(self.n_faces_x), np.zeros(self.n_faces_x)] nY = np.c_[np.zeros(self.n_faces_y), np.ones(self.n_faces_y)] return np.r_[nX, nY] elif self.dim == 3: nX = np.c_[ np.ones(self.n_faces_x), np.zeros(self.n_faces_x), np.zeros(self.n_faces_x), ] nY = np.c_[ np.zeros(self.n_faces_y), np.ones(self.n_faces_y), np.zeros(self.n_faces_y), ] nZ = np.c_[ np.zeros(self.n_faces_z), np.zeros(self.n_faces_z), np.ones(self.n_faces_z), ] return np.r_[nX, nY, nZ] @property def edge_tangents(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: tX = np.c_[np.ones(self.n_edges_x), np.zeros(self.n_edges_x)] tY = np.c_[np.zeros(self.n_edges_y), np.ones(self.n_edges_y)] return np.r_[tX, tY] elif self.dim == 3: tX = np.c_[ np.ones(self.n_edges_x), np.zeros(self.n_edges_x), np.zeros(self.n_edges_x), ] tY = np.c_[ np.zeros(self.n_edges_y), np.ones(self.n_edges_y), np.zeros(self.n_edges_y), ] tZ = np.c_[ np.zeros(self.n_edges_z), np.zeros(self.n_edges_z), np.ones(self.n_edges_z), ] return np.r_[tX, tY, tZ] @property def reference_is_rotated(self): """Indicate whether mesh uses standard coordinate axes. The standard basis vectors defining the x, y, and z axes of a mesh are :math:`(1,0,0)`, :math:`(0,1,0)` and :math:`(0,0,1)`, respectively. However, the :py:attr:`~BaseRegularMesh.orientation` property can be used to define rotated coordinate axes for our mesh. The *reference_is_rotated* property determines whether the mesh is using standard coordinate axes. If the coordinate axes are standard, *mesh.orientation* is the identity matrix and *reference_is_rotated* returns a value of *False*. Otherwise, *reference_is_rotated* returns a value of *True*. Returns ------- bool *False* is the mesh uses the standard coordinate axes and *True* otherwise. """ return not np.allclose(self.orientation, np.identity(self.dim)) @property def rotation_matrix(self): """Alias for :py:attr:`~.BaseRegularMesh.orientation`. See Also -------- orientation """ return self.orientation # np.array([self.axis_u, self.axis_v, self.axis_w]) @property def axis_u(self): """Orientation of the first axis. .. deprecated:: 0.7.0 `axis_u` will be removed in discretize 1.0.0. This functionality was replaced by the :py:attr:`~.BaseRegularMesh.orientation`. """ raise NotImplementedError( "The axis_u property is rmoved, please access as self.orientation[0]. " "This will be removed in discretize 1.0.0." ) @axis_u.setter def axis_u(self, value): raise NotImplementedError( "The axis_u property is removed, please access as self.orientation[0]. " "This will be removed in discretize 1.0.0." ) @property def axis_v(self): """Orientation of the second axis. .. deprecated:: 0.7.0 `axis_v` will be removed in discretize 1.0.0. This functionality was replaced by the :py:attr:`~.BaseRegularMesh.orientation`. """ raise NotImplementedError( "The axis_v property is removed, please access as self.orientation[1]. " "This will be removed in discretize 1.0.0." ) @axis_v.setter def axis_v(self, value): raise NotImplementedError( "The axis_v property is removed, please access as self.orientation[1]. " "This will be removed in discretize 1.0.0." ) @property def axis_w(self): """Orientation of the third axis. .. deprecated:: 0.7.0 `axis_w` will be removed in discretize 1.0.0. This functionality was replaced by the :py:attr:`~.BaseRegularMesh.orientation`. """ raise NotImplementedError( "The axis_w property is removed, please access as self.orientation[2]. " "This will be removed in discretize 1.0.0." ) @axis_w.setter def axis_w(self, value): raise NotImplementedError( "The axis_w property is removed, please access as self.orientation[2]. " "This will be removed in discretize 1.0.0." ) class BaseRectangularMesh(BaseRegularMesh): """Base rectangular mesh class for the ``discretize`` package. The ``BaseRectangularMesh`` class acts as an extension of the :class:`~discretize.base.BaseRegularMesh` classes with a regular structure. """ _aliases = { **BaseRegularMesh._aliases, **{ "vnN": "shape_nodes", "vnEx": "shape_edges_x", "vnEy": "shape_edges_y", "vnEz": "shape_edges_z", "vnFx": "shape_faces_x", "vnFy": "shape_faces_y", "vnFz": "shape_faces_z", }, } @property def shape_nodes(self): """The number of nodes along each axis direction. This property returns a tuple containing the number of nodes along each axis direction. The length of the tuple is equal to the dimension of the mesh; i.e. 1, 2 or 3. Returns ------- (dim) tuple of int Number of nodes along each axis direction Notes ----- Property also accessible as using the shorthand **vnN** """ return tuple(x + 1 for x in self.shape_cells) @property def shape_edges_x(self): """Number of x-edges along each axis direction. This property returns a tuple containing the number of x-edges along each axis direction. The length of the tuple is equal to the dimension of the mesh; i.e. 1, 2 or 3. Returns ------- (dim) tuple of int Number of x-edges along each axis direction - *1D mesh:* `(n_cells_x)` - *2D mesh:* `(n_cells_x, n_nodes_y)` - *3D mesh:* `(n_cells_x, n_nodes_y, n_nodes_z)` Notes ----- Property also accessible as using the shorthand **vnEx** """ return self.shape_cells[:1] + self.shape_nodes[1:] @property def shape_edges_y(self): """Number of y-edges along each axis direction. This property returns a tuple containing the number of y-edges along each axis direction. If `dim` is 1, there are no y-edges. Returns ------- None or (dim) tuple of int Number of y-edges along each axis direction - *1D mesh: None* - *2D mesh:* `(n_nodes_x, n_cells_y)` - *3D mesh:* `(n_nodes_x, n_cells_y, n_nodes_z)` Notes ----- Property also accessible as using the shorthand **vnEy** """ if self.dim < 2: return None sc = self.shape_cells sn = self.shape_nodes return (sn[0], sc[1]) + sn[2:] # conditionally added if dim == 3! @property def shape_edges_z(self): """Number of z-edges along each axis direction. This property returns a tuple containing the number of z-edges along each axis direction. There are only z-edges if `dim` is 3. Returns ------- None or (dim) tuple of int Number of z-edges along each axis direction. - *1D mesh: None* - *2D mesh: None* - *3D mesh:* `(n_nodes_x, n_nodes_y, n_cells_z)` Notes ----- Property also accessible as using the shorthand **vnEz** """ if self.dim < 3: return None return self.shape_nodes[:2] + self.shape_cells[2:] @property def shape_faces_x(self): """Number of x-faces along each axis direction. This property returns a tuple containing the number of x-faces along each axis direction. Returns ------- (dim) tuple of int Number of x-faces along each axis direction - *1D mesh:* `(n_nodes_x)` - *2D mesh:* `(n_nodes_x, n_cells_y)` - *3D mesh:* `(n_nodes_x, n_cells_y, n_cells_z)` Notes ----- Property also accessible as using the shorthand **vnFx** """ return self.shape_nodes[:1] + self.shape_cells[1:] @property def shape_faces_y(self): """Number of y-faces along each axis direction. This property returns a tuple containing the number of y-faces along each axis direction. If `dim` is 1, there are no y-edges. Returns ------- None or (dim) tuple of int Number of y-faces along each axis direction - *1D mesh: None* - *2D mesh:* `(n_cells_x, n_nodes_y)` - *3D mesh:* `(n_cells_x, n_nodes_y, n_cells_z)` Notes ----- Property also accessible as using the shorthand **vnFy** """ if self.dim < 2: return None sc = self.shape_cells sn = self.shape_nodes return (sc[0], sn[1]) + sc[2:] @property def shape_faces_z(self): """Number of z-faces along each axis direction. This property returns a tuple containing the number of z-faces along each axis direction. There are only z-faces if `dim` is 3. Returns ------- None or (dim) tuple of int Number of z-faces along each axis direction. - *1D mesh: None* - *2D mesh: None* - *3D mesh:* (n_cells_x, n_cells_y, n_nodes_z) Notes ----- Property also accessible as using the shorthand **vnFz** """ if self.dim < 3: return None return self.shape_cells[:2] + self.shape_nodes[2:] ################################## # Redo the numbering so they are dependent of the shape tuples # these should all inherit the parent's docstrings ################################## @property def n_cells(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return int(np.prod(self.shape_cells)) @property def n_nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return int(np.prod(self.shape_nodes)) @property def n_edges_x(self): # NOQA D102 # Documentation inherited from discretize.base.BaseRegularMesh return int(np.prod(self.shape_edges_x)) @property def n_edges_y(self): # NOQA D102 # Documentation inherited from discretize.base.BaseRegularMesh if self.dim < 2: return return int(np.prod(self.shape_edges_y)) @property def n_edges_z(self): # NOQA D102 # Documentation inherited from discretize.base.BaseRegularMesh if self.dim < 3: return return int(np.prod(self.shape_edges_z)) @property def n_faces_x(self): # NOQA D102 # Documentation inherited from discretize.base.BaseRegularMesh return int(np.prod(self.shape_faces_x)) @property def n_faces_y(self): # NOQA D102 # Documentation inherited from discretize.base.BaseRegularMesh if self.dim < 2: return return int(np.prod(self.shape_faces_y)) @property def n_faces_z(self): # NOQA D102 # Documentation inherited from discretize.base.BaseRegularMesh if self.dim < 3: return return int(np.prod(self.shape_faces_z)) def reshape( self, x, x_type="cell_centers", out_type="cell_centers", return_format="V", **kwargs, ): """Reshape tensor quantities. **Reshape** is a quick command that will do its best to reshape discrete quantities living on meshes than inherit the :class:`discretize.base_mesh.RectangularMesh` class. For example, you may have a 1D array defining a vector on mesh faces, and you would like to extract the x-component and reshaped it to a 3D matrix. Parameters ---------- x : numpy.ndarray or list of numpy.ndarray The input quantity. , ndarray (tensor) or a list x_type : {'CC', 'N', 'F', 'Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', 'Ez'} Defines the locations on the mesh where input parameter *x* lives. out_type : str Defines the output quantity. Choice depends on your input for *x_type*: - *x_type* = 'CC' ---> *out_type* = 'CC' - *x_type* = 'N' ---> *out_type* = 'N' - *x_type* = 'F' ---> *out_type* = {'F', 'Fx', 'Fy', 'Fz'} - *x_type* = 'E' ---> *out_type* = {'E', 'Ex', 'Ey', 'Ez'} return_format : str The dimensions of quantity being returned - *V:* return a vector (1D array) or a list of vectors - *M:* return matrix (nD array) or a list of matrices """ if "xType" in kwargs: raise TypeError( "The xType keyword argument has been removed, please use x_type. " "This will be removed in discretize 1.0.0" ) x_type = kwargs["xType"] if "outType" in kwargs: raise TypeError( "The outType keyword argument has been removed, please use out_type. " "This will be removed in discretize 1.0.0", ) if "format" in kwargs: raise TypeError( "The format keyword argument has been removed, please use return_format. " "This will be removed in discretize 1.0.0", ) return_format = kwargs["format"] x_type = self._parse_location_type(x_type) out_type = self._parse_location_type(out_type) allowed_x_type = [ "cell_centers", "nodes", "faces", "faces_x", "faces_y", "faces_z", "edges", "edges_x", "edges_y", "edges_z", ] if not (isinstance(x, list) or isinstance(x, np.ndarray)): raise TypeError("x must be either a list or a ndarray") if x_type not in allowed_x_type: raise ValueError( "x_type must be either '" + "', '".join(allowed_x_type) + "'" ) if out_type not in allowed_x_type: raise ValueError( "out_type must be either '" + "', '".join(allowed_x_type) + "'" ) if return_format not in ["M", "V"]: raise ValueError("return_format must be either 'M' or 'V'") if out_type[: len(x_type)] != x_type: raise ValueError("You cannot change types when reshaping.") if x_type not in out_type: raise ValueError("You cannot change type of components.") if isinstance(x, list): for i, xi in enumerate(x): if not isinstance(x, np.ndarray): raise TypeError("x[{0:d}] must be a numpy array".format(i)) if xi.size != x[0].size: raise ValueError("Number of elements in list must not change.") x_array = np.ones((x.size, len(x))) # Unwrap it and put it in a np array for i, xi in enumerate(x): x_array[:, i] = mkvc(xi) x = x_array if not isinstance(x, np.ndarray): raise TypeError("x must be a numpy array") x = x[:] # make a copy. x_type_is_FE_xyz = ( len(x_type) > 1 and x_type[0] in ["f", "e"] and x_type[-1] in ["x", "y", "z"] ) def outKernal(xx, nn): """Return xx as either a matrix (shape == nn) or a vector.""" if return_format == "M": return xx.reshape(nn, order="F") elif return_format == "V": return mkvc(xx) def switchKernal(xx): """Switch over the different options.""" if x_type in ["cell_centers", "nodes"]: nn = self.shape_cells if x_type == "cell_centers" else self.shape_nodes if xx.size != np.prod(nn): raise ValueError("Number of elements must not change.") return outKernal(xx, nn) elif x_type in ["faces", "edges"]: # This will only deal with components of fields, # not full 'F' or 'E' xx = mkvc(xx) # unwrap it in case it is a matrix if x_type == "faces": nn = (self.nFx, self.nFy, self.nFz)[: self.dim] else: nn = (self.nEx, self.nEy, self.nEz)[: self.dim] nn = np.r_[0, nn] nx = [0, 0, 0] nx[0] = self.shape_faces_x if x_type == "faces" else self.shape_edges_x nx[1] = self.shape_faces_y if x_type == "faces" else self.shape_edges_y nx[2] = self.shape_faces_z if x_type == "faces" else self.shape_edges_z for dim, dimName in enumerate(["x", "y", "z"]): if dimName in out_type: if self.dim <= dim: raise ValueError( "Dimensions of mesh not great enough for " "{}_{}".format(x_type, dimName) ) if xx.size != np.sum(nn): raise ValueError("Vector is not the right size.") start = np.sum(nn[: dim + 1]) end = np.sum(nn[: dim + 2]) return outKernal(xx[start:end], nx[dim]) elif x_type_is_FE_xyz: # This will deal with partial components (x, y or z) # lying on edges or faces if "x" in x_type: nn = self.shape_faces_x if "f" in x_type else self.shape_edges_x elif "y" in x_type: nn = self.shape_faces_y if "f" in x_type else self.shape_edges_y elif "z" in x_type: nn = self.shape_faces_z if "f" in x_type else self.shape_edges_z if xx.size != np.prod(nn): raise ValueError( f"Vector is not the right size. Expected {np.prod(nn)}, got {xx.size}" ) return outKernal(xx, nn) # Check if we are dealing with a vector quantity isVectorQuantity = len(x.shape) == 2 and x.shape[1] == self.dim if out_type in ["faces", "edges"]: if isVectorQuantity: raise ValueError("Not sure what to do with a vector vector quantity..") outTypeCopy = out_type out = () for dirName in ["x", "y", "z"][: self.dim]: out_type = outTypeCopy + "_" + dirName out += (switchKernal(x),) return out elif isVectorQuantity: out = () for ii in range(x.shape[1]): out += (switchKernal(x[:, ii]),) return out else: return switchKernal(x) # DEPRECATED r = deprecate_method("reshape", "r", removal_version="1.0.0", error=True) @property def nCx(self): """Number of cells in the x direction. `nCx` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[0]` to reduce namespace clutter. """ raise NotImplementedError( "The nCx property is removed, please access as mesh.shape_cells[0]. " "This message will be removed in discretize 1.0.0." ) @property def nCy(self): """Number of cells in the y direction. `nCy` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[1]` to reduce namespace clutter. """ raise NotImplementedError( "The nCy property is removed, please access as mesh.shape_cells[1]. " "This message will be removed in discretize 1.0.0." ) @property def nCz(self): """Number of cells in the z direction. `nCz` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[2]` to reduce namespace clutter. """ raise NotImplementedError( "The nCz property is removed, please access as mesh.shape_cells[2]. " "This message will be removed in discretize 1.0.0." ) @property def nNx(self): """Number of nodes in the x-direction. `nNx` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[0]` to reduce namespace clutter. """ raise NotImplementedError( "The nNx property is removed, please access as mesh.shape_nodes[0]. " "This message will be removed in discretize 1.0.0." ) @property def nNy(self): """Number of nodes in the y-direction. `nNy` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[1]` to reduce namespace clutter. """ raise NotImplementedError( "The nNy property is removed, please access as mesh.shape_nodes[1]. " "This message will be removed in discretize 1.0.0." ) @property def nNz(self): """Number of nodes in the z-direction. `nNz` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[2]` to reduce namespace clutter. """ raise NotImplementedError( "The nNz property is removed, please access as mesh.shape_nodes[2]. " "This message will be removed in discretize 1.0.0." ) ================================================ FILE: discretize/base/base_tensor_mesh.py ================================================ """Base class for tensor-product style meshes.""" import numpy as np import scipy.sparse as sp from discretize.base.base_regular_mesh import BaseRegularMesh from discretize.utils import ( is_scalar, as_array_n_by_dim, unpack_widths, mkvc, ndgrid, spzeros, sdiag, sdinv, TensorType, interpolation_matrix, make_boundary_bool, ) from discretize.utils.code_utils import deprecate_method, deprecate_property import warnings class BaseTensorMesh(BaseRegularMesh): """Base class for tensor-product style meshes. This class contains properites and methods that are common to Cartesian and cylindrical meshes. That is, meshes whose cell centers, nodes, faces and edges can be constructed with tensor-products of vectors. Do not use this class directly! Practical tensor meshes supported in discretize will inherit this class; i.e. :class:`discretize.TensorMesh` and :class:`~discretize.CylindricalMesh`. Inherit this class if you plan to develop a new tensor-style mesh class (e.g. a spherical mesh). Parameters ---------- h : (dim) iterable of int, numpy.ndarray, or tuple Defines the cell widths along each axis. The length of the iterable object is equal to the dimension of the mesh (1, 2 or 3). For a 3D mesh, the list would have the form *[hx, hy, hz]* . Along each axis, the user has 3 choices for defining the cells widths: - :class:`int` -> A unit interval is equally discretized into `N` cells. - :class:`numpy.ndarray` -> The widths are explicity given for each cell - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])* where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional) is a padding factor denoting exponential increase/decrease in the cell width for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]* origin : (dim) iterable, default: 0 Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost corner. By default, the mesh is anchored such that its origin is at ``[0, 0, 0]``. For each dimension (x, y or z), The user may set the origin 2 ways: - a ``scalar`` which explicitly defines origin along that dimension. - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along each axis is the first node location ('0'), in the center ('C') or the last node location ('N'). See Also -------- utils.unpack_widths : The function used to expand a ``list`` or ``tuple`` to generate widths. """ _meshType = "BASETENSOR" _aliases = { **BaseRegularMesh._aliases, **{ "gridFx": "faces_x", "gridFy": "faces_y", "gridFz": "faces_z", "gridEx": "edges_x", "gridEy": "edges_y", "gridEz": "edges_z", }, } _unitDimensions = [1, 1, 1] _items = {"h"} | BaseRegularMesh._items def __init__(self, h, origin=None, **kwargs): if "x0" in kwargs: origin = kwargs.pop("x0") try: h = list(h) # ensure value is a list (and make a copy) except TypeError: raise TypeError("h must be an iterable object, not {}".format(type(h))) if len(h) == 0 or len(h) > 3: raise ValueError("h must be of dimension 1, 2, or 3 not {}".format(len(h))) # expand value for i, h_i in enumerate(h): if is_scalar(h_i) and not isinstance(h_i, np.ndarray): # This gives you something over the unit cube. h_i = self._unitDimensions[i] * np.ones(int(h_i)) / int(h_i) elif isinstance(h_i, (list, tuple)): h_i = unpack_widths(h_i) if not isinstance(h_i, np.ndarray): raise TypeError("h[{0:d}] is not a numpy array.".format(i)) if len(h_i.shape) != 1: raise ValueError("h[{0:d}] must be a 1D numpy array.".format(i)) h[i] = h_i[:] # make a copy. self._h = tuple(h) shape_cells = tuple([len(h_i) for h_i in h]) kwargs.pop("shape_cells", None) super().__init__(shape_cells=shape_cells, **kwargs) # do not pass origin here if origin is not None: self.origin = origin @property def h(self): r"""Cell widths along each axis direction. The widths of the cells along each axis direction are returned as a tuple of 1D arrays; e.g. (hx, hy, hz) for a 3D mesh. The lengths of the 1D arrays in the tuple are given by :py:attr:`~discretize.base.BaseRegularMesh.shape_cells`. Ordering begins at the bottom southwest corner. These are the cell widths used when creating the mesh. Returns ------- (dim) tuple of numpy.ndarray Cell widths along each axis direction. This depends on the mesh class: - :class:`~discretize.TensorMesh`: cell widths along the *x* , [*y* and *z* ] directions - :class:`~discretize.CylindricalMesh`: cell widths along the *r*, :math:`\phi` and *z* directions - :class:`~discretize.TreeMesh`: cells widths of the *underlying tensor mesh* along the *x* , *y* [and *z* ] directions """ return self._h @BaseRegularMesh.origin.setter def origin(self, value): # NOQA D102 # ensure value is a 1D array at all times try: value = list(value) except TypeError: raise TypeError("origin must be iterable") if len(value) != self.dim: raise ValueError("Dimension mismatch. len(origin) != len(h)") for i, (val, h_i) in enumerate(zip(value, self.h)): if val == "C": value[i] = -h_i.sum() * 0.5 elif val == "N": value[i] = -h_i.sum() value = np.asarray(value, dtype=np.float64) self._origin = value @property def nodes_x(self): """Return x-coordinates of the nodes along the x-direction. This property returns a vector containing the x-coordinate values of the nodes along the x-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the node positions which define the tensor along the x-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the x-coordinate values of the nodes along the x-direction for the underlying tensor mesh. Returns ------- (n_nodes_x) numpy.ndarray of float A 1D array containing the x-coordinates of the nodes along the x-direction. """ return np.r_[self.origin[0], self.h[0]].cumsum() @property def nodes_y(self): """Return y-coordinates of the nodes along the y-direction. For 2D and 3D meshes, this property returns a vector containing the y-coordinate values of the nodes along the y-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the node positions which define the tensor along the y-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the y-coordinate values of the nodes along the y-direction for the underlying tensor mesh. Returns ------- (n_nodes_y) numpy.ndarray of float or None A 1D array containing the y-coordinates of the nodes along the y-direction. Returns *None* for 1D meshes. """ return None if self.dim < 2 else np.r_[self.origin[1], self.h[1]].cumsum() @property def nodes_z(self): """Return z-coordinates of the nodes along the z-direction. For 3D meshes, this property returns a 1D vector containing the z-coordinate values of the nodes along the z-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the node positions which define the tensor along the z-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the z-coordinate values of the nodes along the z-direction for the underlying tensor mesh. Returns ------- (n_nodes_z) numpy.ndarray of float or None A 1D array containing the z-coordinates of the nodes along the z-direction. Returns *None* for 1D and 2D meshes. """ return None if self.dim < 3 else np.r_[self.origin[2], self.h[2]].cumsum() @property def cell_centers_x(self): """Return x-coordinates of the cell centers along the x-direction. For 1D, 2D and 3D meshes, this property returns a 1D vector containing the x-coordinate values of the cell centers along the x-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the cell center positions which define the tensor along the x-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the x-coordinate values of the cell centers along the x-direction for the underlying tensor mesh. Returns ------- (n_cells_x) numpy.ndarray of float A 1D array containing the x-coordinates of the cell centers along the x-direction. """ nodes = self.nodes_x return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers_y(self): """Return y-coordinates of the cell centers along the y-direction. For 2D and 3D meshes, this property returns a 1D vector containing the y-coordinate values of the cell centers along the y-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the cell center positions which define the tensor along the y-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the y-coordinate values of the cell centers along the y-direction for the underlying tensor mesh . Returns ------- (n_cells_y) numpy.ndarray of float or None A 1D array containing the y-coordinates of the cell centers along the y-direction. Returns *None* for 1D meshes. """ if self.dim < 2: return None nodes = self.nodes_y return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers_z(self): """Return z-coordinates of the cell centers along the z-direction. For 3D meshes, this property returns a 1D vector containing the z-coordinate values of the cell centers along the z-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the cell center positions which define the tensor along the z-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the z-coordinate values of the cell centers along the z-direction for the underlying tensor mesh . Returns ------- (n_cells_z) numpy.ndarray of float or None A 1D array containing the z-coordinates of the cell centers along the z-direction. Returns *None* for 1D and 2D meshes. """ if self.dim < 3: return None nodes = self.nodes_z return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self._getTensorGrid("cell_centers") @property def nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self._getTensorGrid("nodes") @property def boundary_nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh dim = self.dim if dim == 1: return self.nodes_x[[0, -1]] return self.nodes[make_boundary_bool(self.shape_nodes)] @property def h_gridded(self): """Return dimensions of all mesh cells as staggered grid. This property returns a numpy array of shape (n_cells, dim) containing gridded x, (y and z) dimensions for all cells in the mesh. The first row corresponds to the bottom-front-leftmost cell. The cells are ordered along the x, then y, then z directions. Returns ------- (n_cells, dim) numpy.ndarray of float Dimensions of all mesh cells as staggered grid Examples -------- The following is a 1D example. >>> from discretize import TensorMesh >>> hx = np.ones(5) >>> mesh_1D = TensorMesh([hx]) >>> mesh_1D.h_gridded array([[1.], [1.], [1.], [1.], [1.]]) The following is a 3D example. >>> hx, hy, hz = np.ones(2), 2*np.ones(2), 3*np.ones(2) >>> mesh_3D = TensorMesh([hx, hy, hz]) >>> mesh_3D.h_gridded array([[1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.]]) """ if self.dim == 1: return self.h[0][:, None] return ndgrid(*self.h) @property def faces_x(self): """Gridded x-face locations. This property returns a numpy array of shape (n_faces_x, dim) containing gridded locations for all x-faces in the mesh. The first row corresponds to the bottom-front-leftmost x-face. The x-faces are ordered along the x, then y, then z directions. Returns ------- (n_faces_x, dim) numpy.ndarray of float Gridded x-face locations """ if self.nFx == 0: return return self._getTensorGrid("faces_x") @property def faces_y(self): """Gridded y-face locations. This property returns a numpy array of shape (n_faces_y, dim) containing gridded locations for all y-faces in the mesh. The first row corresponds to the bottom-front-leftmost y-face. The y-faces are ordered along the x, then y, then z directions. Returns ------- n_faces_y, dim) numpy.ndarray of float or None Gridded y-face locations for 2D and 3D mesh. Returns *None* for 1D meshes. """ if self.nFy == 0 or self.dim < 2: return return self._getTensorGrid("faces_y") @property def faces_z(self): """Gridded z-face locations. This property returns a numpy array of shape (n_faces_z, dim) containing gridded locations for all z-faces in the mesh. The first row corresponds to the bottom-front-leftmost z-face. The z-faces are ordered along the x, then y, then z directions. Returns ------- (n_faces_z, dim) numpy.ndarray of float or None Gridded z-face locations for 3D mesh. Returns *None* for 1D and 2D meshes. """ if self.nFz == 0 or self.dim < 3: return return self._getTensorGrid("faces_z") @property def faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.faces_x is not None: faces = self.faces_x else: faces = np.empty((0, self.dim)) if self.dim > 1 and self.faces_y is not None: faces = np.r_[faces, self.faces_y] if self.dim > 2 and self.faces_z is not None: faces = np.r_[faces, self.faces_z] return faces @property def boundary_faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh dim = self.dim if dim == 1: return self.nodes_x[[0, -1]] if dim == 2: fx = ndgrid(self.nodes_x[[0, -1]], self.cell_centers_y) fy = ndgrid(self.cell_centers_x, self.nodes_y[[0, -1]]) return np.r_[fx, fy] if dim == 3: fx = ndgrid(self.nodes_x[[0, -1]], self.cell_centers_y, self.cell_centers_z) fy = ndgrid(self.cell_centers_x, self.nodes_y[[0, -1]], self.cell_centers_z) fz = ndgrid(self.cell_centers_x, self.cell_centers_y, self.nodes_z[[0, -1]]) return np.r_[fx, fy, fz] @property def boundary_face_outward_normals(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh dim = self.dim if dim == 1: return np.array([-1, 1]) if dim == 2: nx = ndgrid(np.r_[-1, 1], np.zeros(self.shape_cells[1])) ny = ndgrid(np.zeros(self.shape_cells[0]), np.r_[-1, 1]) return np.r_[nx, ny] if dim == 3: nx = ndgrid( np.r_[-1, 1], np.zeros(self.shape_cells[1]), np.zeros(self.shape_cells[2]), ) ny = ndgrid( np.zeros(self.shape_cells[0]), np.r_[-1, 1], np.zeros(self.shape_cells[2]), ) nz = ndgrid( np.zeros(self.shape_cells[0]), np.zeros(self.shape_cells[1]), np.r_[-1, 1], ) return np.r_[nx, ny, nz] @property def edges_x(self): """Gridded x-edge locations. This property returns a numpy array of shape (n_edges_x, dim) containing gridded locations for all x-edges in the mesh. The first row corresponds to the bottom-front-leftmost x-edge. The x-edges are ordered along the x, then y, then z directions. Returns ------- (n_edges_x, dim) numpy.ndarray of float or None Gridded x-edge locations. Returns *None* if `shape_edges_x[0]` is 0. """ if self.nEx == 0: return return self._getTensorGrid("edges_x") @property def edges_y(self): """Gridded y-edge locations. This property returns a numpy array of shape (n_edges_y, dim) containing gridded locations for all y-edges in the mesh. The first row corresponds to the bottom-front-leftmost y-edge. The y-edges are ordered along the x, then y, then z directions. Returns ------- (n_edges_y, dim) numpy.ndarray of float Gridded y-edge locations. Returns *None* for 1D meshes. """ if self.nEy == 0 or self.dim < 2: return return self._getTensorGrid("edges_y") @property def edges_z(self): """Gridded z-edge locations. This property returns a numpy array of shape (n_edges_z, dim) containing gridded locations for all z-edges in the mesh. The first row corresponds to the bottom-front-leftmost z-edge. The z-edges are ordered along the x, then y, then z directions. Returns ------- (n_edges_z, dim) numpy.ndarray of float Gridded z-edge locations. Returns *None* for 1D and 2D meshes. """ if self.nEz == 0 or self.dim < 3: return return self._getTensorGrid("edges_z") @property def edges(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.edges_x is not None: edges = self.edges_x else: edges = np.empty((0, self.dim)) if self.dim > 1 and self.edges_y is not None: edges = np.r_[edges, self.edges_y] if self.dim > 2 and self.edges_z is not None: edges = np.r_[edges, self.edges_z] return edges @property def boundary_edges(self): """Boundary edge locations. This property returns the locations of the edges on the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary edges by the dimension of the mesh. Returns ------- (n_boundary_edges, dim) numpy.ndarray of float Boundary edge locations """ dim = self.dim if dim == 1: return None # no boundary edges in 1D if dim == 2: ex = ndgrid(self.cell_centers_x, self.nodes_y[[0, -1]]) ey = ndgrid(self.nodes_x[[0, -1]], self.cell_centers_y) return np.r_[ex, ey] if dim == 3: ex = self.edges_x[make_boundary_bool(self.shape_edges_x, bdir="yz")] ey = self.edges_y[make_boundary_bool(self.shape_edges_y, bdir="xz")] ez = self.edges_z[make_boundary_bool(self.shape_edges_z, bdir="xy")] return np.r_[ex, ey, ez] def _getTensorGrid(self, key): if getattr(self, "_" + key, None) is None: setattr(self, "_" + key, ndgrid(self.get_tensor(key))) return getattr(self, "_" + key) def get_tensor(self, key): """Return the base 1D arrays for a specified mesh tensor. The cell-centers, nodes, x-faces, z-edges, etc... of a tensor mesh can be constructed by applying tensor products to the set of base 1D arrays; i.e. (vx, vy, vz). These 1D arrays define the gridded locations for the mesh tensor along each axis. For a given mesh tensor (i.e. cell centers, nodes, x/y/z faces or x/y/z edges), **get_tensor** returns a list containing the base 1D arrays. Parameters ---------- key : str Specifies the tensor being returned. Please choose from:: 'CC', 'cell_centers' -> location of cell centers 'N', 'nodes' -> location of nodes 'Fx', 'faces_x' -> location of faces with an x normal 'Fy', 'faces_y' -> location of faces with an y normal 'Fz', 'faces_z' -> location of faces with an z normal 'Ex', 'edges_x' -> location of edges with an x tangent 'Ey', 'edges_y' -> location of edges with an y tangent 'Ez', 'edges_z' -> location of edges with an z tangent Returns ------- (dim) list of 1D numpy.ndarray list of base 1D arrays for the tensor. """ key = self._parse_location_type(key) if key == "faces_x": ten = [ self.nodes_x, self.cell_centers_y, self.cell_centers_z, ] elif key == "faces_y": ten = [ self.cell_centers_x, self.nodes_y, self.cell_centers_z, ] elif key == "faces_z": ten = [ self.cell_centers_x, self.cell_centers_y, self.nodes_z, ] elif key == "edges_x": ten = [self.cell_centers_x, self.nodes_y, self.nodes_z] elif key == "edges_y": ten = [self.nodes_x, self.cell_centers_y, self.nodes_z] elif key == "edges_z": ten = [self.nodes_x, self.nodes_y, self.cell_centers_z] elif key == "cell_centers": ten = [ self.cell_centers_x, self.cell_centers_y, self.cell_centers_z, ] elif key == "nodes": ten = [self.nodes_x, self.nodes_y, self.nodes_z] else: raise KeyError(r"Unrecognized key {key}") return [t for t in ten if t is not None] # --------------- Methods --------------------- def is_inside(self, pts, location_type="nodes", **kwargs): """Determine which points lie within the mesh. For an arbitrary set of points, **is_indside** returns a boolean array identifying which points lie within the mesh. Parameters ---------- pts : (n_pts, dim) numpy.ndarray Locations of input points. Must have same dimension as the mesh. location_type : str, optional Use *N* to determine points lying within the cluster of mesh nodes. Use *CC* to determine points lying within the cluster of mesh cell centers. Returns ------- (n_pts) numpy.ndarray of bool Boolean array identifying points which lie within the mesh """ if "locType" in kwargs: raise TypeError( "The locType keyword argument has been removed, please use location_type. " "This will be removed in discretize 1.0.0", ) pts = as_array_n_by_dim(pts, self.dim) tensors = self.get_tensor(location_type) if location_type[0].lower() == "n" and self._meshType == "CYL": # NOTE: for a CYL mesh we add a node to check if we are inside in # the radial direction! tensors[0] = np.r_[0.0, tensors[0]] tensors[1] = np.r_[tensors[1], 2.0 * np.pi] inside = np.ones(pts.shape[0], dtype=bool) for i, tensor in enumerate(tensors): TOL = np.diff(tensor).min() * 1.0e-10 inside = ( inside & (pts[:, i] >= tensor.min() - TOL) & (pts[:, i] <= tensor.max() + TOL) ) return inside def _get_interpolation_matrix( self, loc, location_type="cell_centers", zeros_outside=False ): """Produce an interpolation matrix. Parameters ---------- loc : numpy.ndarray Location of points to interpolate to location_type: str, optional What to interpolate location_type can be:: 'Ex', 'edges_x' -> x-component of field defined on x edges 'Ey', 'edges_y' -> y-component of field defined on y edges 'Ez', 'edges_z' -> z-component of field defined on z edges 'Fx', 'faces_x' -> x-component of field defined on x faces 'Fy', 'faces_y' -> y-component of field defined on y faces 'Fz', 'faces_z' -> z-component of field defined on z faces 'N', 'nodes' -> scalar field defined on nodes 'CC', 'cell_centers' -> scalar field defined on cell centers 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers Returns ------- scipy.sparse.csr_matrix M, the interpolation matrix """ loc = as_array_n_by_dim(loc, self.dim) if not zeros_outside: if not np.all(self.is_inside(loc)): raise ValueError("Points outside of mesh") else: indZeros = np.logical_not(self.is_inside(loc)) loc = loc.copy() loc[indZeros, :] = np.array([v.mean() for v in self.get_tensor("CC")]) location_type = self._parse_location_type(location_type) if location_type in [ "faces_x", "faces_y", "faces_z", "edges_x", "edges_y", "edges_z", ]: ind = {"x": 0, "y": 1, "z": 2}[location_type[-1]] if self.dim < ind: raise ValueError("mesh is not high enough dimension.") if "f" in location_type.lower(): items = (self.nFx, self.nFy, self.nFz)[: self.dim] else: items = (self.nEx, self.nEy, self.nEz)[: self.dim] components = [spzeros(loc.shape[0], n) for n in items] components[ind] = interpolation_matrix(loc, *self.get_tensor(location_type)) # remove any zero blocks (hstack complains) components = [comp for comp in components if comp.shape[1] > 0] Q = sp.hstack(components) elif location_type in ["cell_centers", "nodes"]: Q = interpolation_matrix(loc, *self.get_tensor(location_type)) elif location_type in ["cell_centers_x", "cell_centers_y", "cell_centers_z"]: Q = interpolation_matrix(loc, *self.get_tensor("CC")) Z = spzeros(loc.shape[0], self.nC) if location_type[-1] == "x": Q = sp.hstack([Q, Z, Z]) elif location_type[-1] == "y": Q = sp.hstack([Z, Q, Z]) elif location_type[-1] == "z": Q = sp.hstack([Z, Z, Q]) else: raise NotImplementedError( "get_interpolation_matrix: location_type==" + location_type + " and mesh.dim==" + str(self.dim) ) if zeros_outside: Q[indZeros, :] = 0 return Q.tocsr() def get_interpolation_matrix( # NOQA D102 self, loc, location_type="cell_centers", zeros_outside=False, **kwargs ): # Documentation inherited from discretize.base.BaseMesh if "locType" in kwargs: raise TypeError( "The locType keyword argument has been removed, please use location_type. " "This will be removed in discretize 1.0.0", ) if "zerosOutside" in kwargs: raise TypeError( "The zerosOutside keyword argument has been removed, please use zeros_outside. " "This will be removed in discretize 1.0.0", ) return self._get_interpolation_matrix(loc, location_type, zeros_outside) def _fastInnerProduct( self, projection_type, model=None, invert_model=False, invert_matrix=False ): """Fast version of get_face_inner_product_deriv. This does not handle the case of a full tensor property. Parameters ---------- projection_type : str 'edges' or 'faces' model : numpy.ndarray material property (tensor properties are possible) at each cell center (nC, (1, 3, or 6)) invert_model : bool inverts the material property invert_matrix : bool inverts the matrix Returns ------- (n_faces, n_faces) scipy.sparse.csr_matrix M, the inner product matrix """ projection_type = projection_type[0].upper() if projection_type not in ["F", "E"]: raise ValueError("projection_type must be 'F' for faces or 'E' for edges") if model is None: model = np.ones(self.nC) if invert_model: model = 1.0 / model if is_scalar(model): model = model * np.ones(self.nC) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = getattr(self, "vn" + projection_type) n_elements = sum([1 if x != 0 else 0 for x in shape]) else: n_elements = self.dim # Isotropic? or anisotropic? if model.size == self.nC: Av = getattr(self, "ave" + projection_type + "2CC") Vprop = self.cell_volumes * mkvc(model) M = n_elements * sdiag(Av.T * Vprop) elif model.size == self.nC * self.dim: Av = getattr(self, "ave" + projection_type + "2CCV") # if cyl, then only certain components are relevant due to symmetry # for faces, x, z matters, for edges, y (which is theta) matters if self._meshType == "CYL" and self.is_symmetric: if projection_type == "E": model = model[:, 1] # this is the action of a projection mat elif projection_type == "F": model = model[:, [0, 2]] V = sp.kron(sp.identity(n_elements), sdiag(self.cell_volumes)) M = sdiag(Av.T * V * mkvc(model)) else: return None if invert_matrix: return sdinv(M) else: return M def _fastInnerProductDeriv( self, projection_type, model, invert_model=False, invert_matrix=False ): """Faster function for inner product derivatives on tensor meshes. Parameters ---------- projection_type : str 'edges' or 'faces' model : numpy.ndarray material property (tensor properties are possible) at each cell center (nC, (1, 3, or 6)) invert_model : bool inverts the material property invert_matrix : bool inverts the matrix Returns ------- function dMdmu, the derivative of the inner product matrix """ projection_type = projection_type[0].upper() if projection_type not in ["F", "E"]: raise ValueError("projection_type must be 'F' for faces or 'E' for edges") tensorType = TensorType(self, model) dMdprop = None if invert_matrix or invert_model: MI = self._fastInnerProduct( projection_type, model, invert_model=invert_model, invert_matrix=invert_matrix, ) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = getattr(self, "vn" + projection_type) n_elements = sum([1 if x != 0 else 0 for x in shape]) else: n_elements = self.dim if tensorType == 0: # isotropic, constant Av = getattr(self, "ave" + projection_type + "2CC") V = sdiag(self.cell_volumes) ones = sp.csr_matrix( (np.ones(self.nC), (range(self.nC), np.zeros(self.nC))), shape=(self.nC, 1), ) if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * V * ones elif invert_matrix and invert_model: dMdprop = n_elements * ( sdiag(MI.diagonal() ** 2) * Av.T * V * ones * sdiag(1.0 / model**2) ) elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * V) elif tensorType == 1: # isotropic, variable in space Av = getattr(self, "ave" + projection_type + "2CC") V = sdiag(self.cell_volumes) if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * V elif invert_matrix and invert_model: dMdprop = n_elements * ( sdiag(MI.diagonal() ** 2) * Av.T * V * sdiag(1.0 / model**2) ) elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * V) elif tensorType == 2: # anisotropic Av = getattr(self, "ave" + projection_type + "2CCV") V = sp.kron(sp.identity(self.dim), sdiag(self.cell_volumes)) if self._meshType == "CYL" and self.is_symmetric: Zero = sp.csr_matrix((self.nC, self.nC)) Eye = sp.eye(self.nC) if projection_type == "E": P = sp.hstack([Zero, Eye, Zero]) # print(P.todense()) elif projection_type == "F": P = sp.vstack( [sp.hstack([Eye, Zero, Zero]), sp.hstack([Zero, Zero, Eye])] ) # print(P.todense()) else: P = sp.eye(self.nC * self.dim) if not invert_matrix and not invert_model: dMdprop = Av.T * P * V elif invert_matrix and invert_model: dMdprop = ( sdiag(MI.diagonal() ** 2) * Av.T * P * V * sdiag(1.0 / model**2) ) elif invert_model: dMdprop = Av.T * P * V * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = sdiag(-MI.diagonal() ** 2) * Av.T * P * V if dMdprop is not None: def innerProductDeriv(v=None): if v is None: warnings.warn( "Depreciation Warning: TensorMesh.innerProductDeriv." " You should be supplying a vector. " "Use: sdiag(u)*dMdprop", FutureWarning, stacklevel=2, ) return dMdprop return sdiag(v) * dMdprop return innerProductDeriv else: return None # DEPRECATED @property def hx(self): """Width of cells in the x direction. `hx` will be removed in discretize 1.0.0, it is replaced by `mesh.h[0]` to reduce namespace clutter. """ raise NotImplementedError( "The hx property is removed, please access as mesh.h[0]. " "This message will be removed in discretize 1.0.0." ) @property def hy(self): """Width of cells in the y direction. `hy` will be removed in discretize 1.0.0, it is replaced by `mesh.h[1]` to reduce namespace clutter. """ raise NotImplementedError( "The hy property is removed, please access as mesh.h[1]. " "This message will be removed in discretize 1.0.0." ) @property def hz(self): """Width of cells in the z direction. `hz` will be removed in discretize 1.0.0, it is replaced by `mesh.h[2]` to reduce namespace clutter. """ raise NotImplementedError( "The hz property is removed, please access as mesh.h[2]. " "This message will be removed in discretize 1.0.0." ) vectorNx = deprecate_property( "nodes_x", "vectorNx", removal_version="1.0.0", error=True ) vectorNy = deprecate_property( "nodes_y", "vectorNy", removal_version="1.0.0", error=True ) vectorNz = deprecate_property( "nodes_z", "vectorNz", removal_version="1.0.0", error=True ) vectorCCx = deprecate_property( "cell_centers_x", "vectorCCx", removal_version="1.0.0", error=True ) vectorCCy = deprecate_property( "cell_centers_y", "vectorCCy", removal_version="1.0.0", error=True ) vectorCCz = deprecate_property( "cell_centers_z", "vectorCCz", removal_version="1.0.0", error=True ) isInside = deprecate_method( "is_inside", "isInside", removal_version="1.0.0", error=True ) getTensor = deprecate_method( "get_tensor", "getTensor", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/base/meson.build ================================================ python_sources = [ '__init__.py', 'base_mesh.py', 'base_regular_mesh.py', 'base_tensor_mesh.py', ] py.install_sources( python_sources, subdir: 'discretize/base' ) ================================================ FILE: discretize/curvilinear_mesh.py ================================================ """Module containing the curvilinear mesh implementation.""" import numpy as np import scipy.sparse as sp from discretize.utils import ( mkvc, index_cube, face_info, volume_tetrahedron, make_boundary_bool, ) from discretize.base import BaseRectangularMesh from discretize.operators import DiffOperators, InnerProducts from discretize.mixins import InterfaceMixins # Some helper functions. def _length2D(x): return (x[:, 0] ** 2 + x[:, 1] ** 2) ** 0.5 def _length3D(x): return (x[:, 0] ** 2 + x[:, 1] ** 2 + x[:, 2] ** 2) ** 0.5 def _normalize2D(x): return x / np.kron(np.ones((1, 2)), mkvc(_length2D(x), 2)) def _normalize3D(x): return x / np.kron(np.ones((1, 3)), mkvc(_length3D(x), 2)) class CurvilinearMesh( DiffOperators, InnerProducts, BaseRectangularMesh, InterfaceMixins ): """Curvilinear mesh class. Curvilinear meshes are numerical grids whose cells are general quadrilaterals (2D) or cuboid (3D); unlike tensor meshes (see :class:`~discretize.TensorMesh`) whose cells are rectangles or rectangular prisms. That being said, the combinatorial structure (i.e. connectivity of mesh cells) of curvilinear meshes is the same as tensor meshes. Parameters ---------- node_list : list of array_like List :class:`array_like` containing the gridded x, y (and z) node locations. - For a 2D curvilinear mesh, *node_list* = [X, Y] where X and Y have shape (``n_nodes_x``, ``n_nodes_y``) - For a 3D curvilinear mesh, *node_list* = [X, Y, Z] where X, Y and Z have shape (``n_nodes_x``, ``n_nodes_y``, ``n_nodes_z``) Examples -------- Using the :py:func:`~discretize.utils.example_curvilinear_grid` utility, we provide an example of a curvilinear mesh. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid >>> import matplotlib.pyplot as plt The example grid slightly rotates the nodes in the center of the mesh, >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> x.shape (11, 11) >>> y.shape (11, 11) >>> curvilinear_mesh = CurvilinearMesh([x, y]) >>> curvilinear_mesh.shape_nodes (11, 11) >>> fig = plt.figure(figsize=(5,5)) >>> ax = fig.add_subplot(111) >>> curvilinear_mesh.plot_grid(ax=ax) >>> plt.show() """ _meshType = "Curv" _aliases = { **DiffOperators._aliases, **BaseRectangularMesh._aliases, **{ "gridFx": "faces_x", "gridFy": "faces_y", "gridFz": "faces_z", "gridEx": "edges_x", "gridEy": "edges_y", "gridEz": "edges_z", }, } _items = {"node_list"} def __init__(self, node_list, **kwargs): if "nodes" in kwargs: node_list = kwargs.pop("nodes") node_list = tuple(np.asarray(item, dtype=np.float64) for item in node_list) # check shapes of each node array match dim = len(node_list) if dim not in [2, 3]: raise ValueError( f"Only supports 2 and 3 dimensional meshes, saw a node_list of length {dim}" ) for nodes in node_list: if len(nodes.shape) != dim: raise ValueError( f"Unexpected shape of item in node list, expect array with {dim} dimensions, got {len(nodes.shape)}" ) if node_list[0].shape != nodes.shape: raise ValueError( f"The shape of nodes are not consistent, saw {node_list[0].shape} and {nodes.shape}" ) self._node_list = tuple(node_list) # Save nodes to private variable _nodes as vectors self._nodes = np.ones((self.node_list[0].size, dim)) for i, nodes in enumerate(self.node_list): self._nodes[:, i] = mkvc(nodes) shape_cells = (n - 1 for n in self.node_list[0].shape) # absorb the rest of kwargs, and do not pass to super super().__init__(shape_cells, origin=self.nodes[0]) @property def node_list(self): """The gridded x, y (and z) node locations used to create the mesh. Returns ------- (dim) list of numpy.ndarray Gridded x, y (and z) node locations used to create the mesh. - *2D:* return is a list [X, Y] where X and Y have shape (n_nodes_x, n_nodes_y) - *3D:* return is a list [X, Y, Z] where X, Y and Z have shape (n_nodes_x, n_nodes_y, n_nodes_z) """ return self._node_list @classmethod def deserialize(cls, value, **kwargs): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if "nodes" in value: value["node_list"] = value.pop("nodes") return super().deserialize(value, **kwargs) @property def cell_centers(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_cell_centers", None) is None: self._cell_centers = np.concatenate( [self.aveN2CC * self.gridN[:, i] for i in range(self.dim)] ).reshape((-1, self.dim), order="F") return self._cell_centers @property def nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_nodes", None) is None: raise Exception("Someone deleted this. I blame you.") return self._nodes @property def faces_x(self): """Gridded x-face locations (staggered grid). This property returns a numpy array of shape (n_faces_x, dim) containing gridded locations for all x-faces in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the x-faces are faces whose normal vectors are primarily along the x-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_faces_x, dim) numpy.ndarray of float Gridded x-face locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x-faces have normal vectors that are primarily along the x-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> x_faces = mesh1.faces_x >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'X-faces'], fontsize=16) >>> plt.show() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the x-faces are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> x_faces = mesh2.faces_x >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'X-faces'], fontsize=16) >>> plt.show() """ if getattr(self, "_faces_x", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N] self._faces_x = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [ mkvc( 0.25 * ( n[:, :-1, :-1] + n[:, :-1, 1:] + n[:, 1:, :-1] + n[:, 1:, 1:] ) ) for n in N ] self._faces_x = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_x @property def faces_y(self): """Gridded y-face locations (staggered grid). This property returns a numpy array of shape (n_faces_y, dim) containing gridded locations for all y-faces in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the y-faces are faces whose normal vectors are primarily along the y-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_faces_y, dim) numpy.ndarray of float Gridded y-face locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the y-faces have normal vectors that are primarily along the x-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> y_faces = mesh1.faces_y >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'Y-faces'], fontsize=16) >>> plt.show() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-faces are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> y_faces = mesh2.faces_y >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'Y-faces'], fontsize=16) >>> plt.show() """ if getattr(self, "_faces_y", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N] self._faces_y = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [ mkvc( 0.25 * ( n[:-1, :, :-1] + n[:-1, :, 1:] + n[1:, :, :-1] + n[1:, :, 1:] ) ) for n in N ] self._faces_y = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_y @property def faces_z(self): """Gridded z-face locations (staggered grid). This property returns a numpy array of shape (n_faces_z, dim) containing gridded locations for all z-faces in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the z-faces are faces whose normal vectors are primarily along the z-direction. For highly irregular meshes however, this is not the case. Returns ------- (n_faces_z, dim) numpy.ndarray of float Gridded z-face locations (staggered grid) """ if getattr(self, "_faces_z", None) is None: N = self.reshape(self.gridN, "N", "N", "M") XYZ = [ mkvc( 0.25 * (n[:-1, :-1, :] + n[:-1, 1:, :] + n[1:, :-1, :] + n[1:, 1:, :]) ) for n in N ] self._faces_z = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_z @property def faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh faces = np.r_[self.faces_x, self.faces_y] if self.dim > 2: faces = np.r_[faces, self.faces_z] return faces @property def edges_x(self): """Gridded x-edge locations (staggered grid). This property returns a numpy array of shape (n_edges_x, dim) containing gridded locations for all x-edges in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the x-edges are edges oriented primarily along the x-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_edges_x, dim) numpy.ndarray of float Gridded x-edge locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x-edges are primarily oriented along the x-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> x_edges = mesh1.edges_x >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'X-edges'], fontsize=16) >>> plt.show() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the x-edges are not aligned primarily along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> x_edges = mesh2.edges_x >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'X-edges'], fontsize=16) >>> plt.show() """ if getattr(self, "_edges_x", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N] self._edges_x = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [mkvc(0.5 * (n[:-1, :, :] + n[1:, :, :])) for n in N] self._edges_x = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_x @property def edges_y(self): """Gridded y-edge locations (staggered grid). This property returns a numpy array of shape (n_edges_y, dim) containing gridded locations for all y-edges in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the y-edges are edges oriented primarily along the y-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_edges_y, dim) numpy.ndarray of float Gridded y-edge locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the y-edges are primarily oriented along the y-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> y_edges = mesh1.edges_y >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'Y-edges'], fontsize=16) >>> plt.show() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-edges are not aligned primarily along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> y_edges = mesh2.edges_y >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'X-edges'], fontsize=16) >>> plt.show() """ if getattr(self, "_edges_y", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N] self._edges_y = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [mkvc(0.5 * (n[:, :-1, :] + n[:, 1:, :])) for n in N] self._edges_y = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_y @property def edges_z(self): """Gridded z-edge locations (staggered grid). This property returns a numpy array of shape (n_edges_z, dim) containing gridded locations for all z-edges in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the z-edges are faces whose normal vectors are primarily along the z-direction. For highly irregular meshes however, this is not the case. Returns ------- (n_edges_z, dim) numpy.ndarray of float Gridded z-edge locations (staggered grid) """ if getattr(self, "_edges_z", None) is None and self.dim == 3: N = self.reshape(self.gridN, "N", "N", "M") XYZ = [mkvc(0.5 * (n[:, :, :-1] + n[:, :, 1:])) for n in N] self._edges_z = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_z @property def edges(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh edges = np.r_[self.edges_x, self.edges_y] if self.dim > 2: edges = np.r_[edges, self.edges_z] return edges @property def boundary_nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.nodes[make_boundary_bool(self.shape_nodes)] @property def boundary_edges(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: ex = self.edges_x[make_boundary_bool(self.shape_edges_x, bdir="y")] ey = self.edges_y[make_boundary_bool(self.shape_edges_y, bdir="x")] return np.r_[ex, ey] elif self.dim == 3: ex = self.edges_x[make_boundary_bool(self.shape_edges_x, bdir="yz")] ey = self.edges_y[make_boundary_bool(self.shape_edges_y, bdir="xz")] ez = self.edges_z[make_boundary_bool(self.shape_edges_z, bdir="xy")] return np.r_[ex, ey, ez] @property def boundary_faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh fx = self.faces_x[make_boundary_bool(self.shape_faces_x, bdir="x")] fy = self.faces_y[make_boundary_bool(self.shape_faces_y, bdir="y")] if self.dim == 2: return np.r_[fx, fy] elif self.dim == 3: fz = self.faces_z[make_boundary_bool(self.shape_faces_z, bdir="z")] return np.r_[fx, fy, fz] @property def boundary_face_outward_normals(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh is_bxm = np.zeros(self.shape_faces_x, order="F", dtype=bool) is_bxm[0, :] = True is_bxm = is_bxm.reshape(-1, order="F") is_bym = np.zeros(self.shape_faces_y, order="F", dtype=bool) is_bym[:, 0] = True is_bym = is_bym.reshape(-1, order="F") is_b = np.r_[ make_boundary_bool(self.shape_faces_x, bdir="x"), make_boundary_bool(self.shape_faces_y, bdir="y"), ] switch = np.r_[is_bxm, is_bym] if self.dim == 3: is_bzm = np.zeros(self.shape_faces_z, order="F", dtype=bool) is_bzm[:, :, 0] = True is_bzm = is_bzm.reshape(-1, order="F") is_b = np.r_[is_b, make_boundary_bool(self.shape_faces_z, bdir="z")] switch = np.r_[switch, is_bzm] face_normals = self.face_normals.copy() face_normals[switch] *= -1 return face_normals[is_b] # --------------- Geometries --------------------- # # # ------------------- 2D ------------------------- # # node(i,j) node(i,j+1) # A -------------- B # | | # | cell(i,j) | # | I | # | | # D -------------- C # node(i+1,j) node(i+1,j+1) # # ------------------- 3D ------------------------- # # # node(i,j,k+1) node(i,j+1,k+1) # E --------------- F # /| / | # / | / | # / | / | # node(i,j,k) node(i,j+1,k) # A -------------- B | # | H ----------|---- G # | /cell(i,j) | / # | / I | / # | / | / # D -------------- C # node(i+1,j,k) node(i+1,j+1,k) @property def cell_volumes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_cell_volumes", None) is None: if self.dim == 2: A, B, C, D = index_cube("ABCD", self.vnN) normal, area = face_info( np.c_[self.gridN, np.zeros((self.nN, 1))], A, B, C, D ) self._cell_volumes = area elif self.dim == 3: # Each polyhedron can be decomposed into 5 tetrahedrons # However, this presents a choice so we may as well divide in # two ways and average. A, B, C, D, E, F, G, H = index_cube("ABCDEFGH", self.vnN) vol1 = ( volume_tetrahedron(self.gridN, A, B, D, E) + volume_tetrahedron(self.gridN, B, E, F, G) # cutted edge top + volume_tetrahedron(self.gridN, B, D, E, G) # cutted edge top + volume_tetrahedron(self.gridN, B, C, D, G) # middle + volume_tetrahedron(self.gridN, D, E, G, H) # cutted edge bottom ) # cutted edge bottom vol2 = ( volume_tetrahedron(self.gridN, A, F, B, C) + volume_tetrahedron(self.gridN, A, E, F, H) # cutted edge top + volume_tetrahedron(self.gridN, A, H, F, C) # cutted edge top + volume_tetrahedron(self.gridN, C, H, D, A) # middle + volume_tetrahedron(self.gridN, C, G, H, F) # cutted edge bottom ) # cutted edge bottom self._cell_volumes = (vol1 + vol2) / 2 return self._cell_volumes @property def face_areas(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if ( getattr(self, "_face_areas", None) is None or getattr(self, "_normals", None) is None ): # Compute areas of cell faces if self.dim == 2: xy = self.gridN A, B = index_cube("AB", self.vnN, self.vnFx) edge1 = xy[B, :] - xy[A, :] normal1 = np.c_[edge1[:, 1], -edge1[:, 0]] area1 = _length2D(edge1) A, D = index_cube("AD", self.vnN, self.vnFy) # Note that we are doing A-D to make sure the normal points the # right way. # Think about it. Look at the picture. Normal points towards C # iff you do this. edge2 = xy[A, :] - xy[D, :] normal2 = np.c_[edge2[:, 1], -edge2[:, 0]] area2 = _length2D(edge2) self._face_areas = np.r_[mkvc(area1), mkvc(area2)] self._normals = [_normalize2D(normal1), _normalize2D(normal2)] elif self.dim == 3: A, E, F, B = index_cube("AEFB", self.vnN, self.vnFx) normal1, area1 = face_info( self.gridN, A, E, F, B, average=False, normalize_normals=False ) A, D, H, E = index_cube("ADHE", self.vnN, self.vnFy) normal2, area2 = face_info( self.gridN, A, D, H, E, average=False, normalize_normals=False ) A, B, C, D = index_cube("ABCD", self.vnN, self.vnFz) normal3, area3 = face_info( self.gridN, A, B, C, D, average=False, normalize_normals=False ) self._face_areas = np.r_[mkvc(area1), mkvc(area2), mkvc(area3)] self._normals = [normal1, normal2, normal3] return self._face_areas @property def face_normals(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh # For 3D meshes, there are 4 nodes in which # cross-products can be used to compute the normal vector. # In this case, the average normal vector is returned so there # is only 1 vector per face. if getattr(self, "_normals", None) is None: self.face_areas # calling .face_areas will create the face normals if self.dim == 2: return _normalize2D(np.r_[self._normals[0], self._normals[1]]) elif self.dim == 3: normal1 = ( self._normals[0][0] + self._normals[0][1] + self._normals[0][2] + self._normals[0][3] ) / 4 normal2 = ( self._normals[1][0] + self._normals[1][1] + self._normals[1][2] + self._normals[1][3] ) / 4 normal3 = ( self._normals[2][0] + self._normals[2][1] + self._normals[2][2] + self._normals[2][3] ) / 4 return _normalize3D(np.r_[normal1, normal2, normal3]) @property def edge_lengths(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_edge_lengths", None) is None: if self.dim == 2: xy = self.gridN A, D = index_cube("AD", self.vnN, self.vnEx) edge1 = xy[D, :] - xy[A, :] A, B = index_cube("AB", self.vnN, self.vnEy) edge2 = xy[B, :] - xy[A, :] self._edge_lengths = np.r_[ mkvc(_length2D(edge1)), mkvc(_length2D(edge2)) ] self._edge_tangents = ( np.r_[edge1, edge2] / np.c_[self._edge_lengths, self._edge_lengths] ) elif self.dim == 3: xyz = self.gridN A, D = index_cube("AD", self.vnN, self.vnEx) edge1 = xyz[D, :] - xyz[A, :] A, B = index_cube("AB", self.vnN, self.vnEy) edge2 = xyz[B, :] - xyz[A, :] A, E = index_cube("AE", self.vnN, self.vnEz) edge3 = xyz[E, :] - xyz[A, :] self._edge_lengths = np.r_[ mkvc(_length3D(edge1)), mkvc(_length3D(edge2)), mkvc(_length3D(edge3)), ] self._edge_tangents = ( np.r_[edge1, edge2, edge3] / np.c_[self._edge_lengths, self._edge_lengths, self._edge_lengths] ) return self._edge_lengths @property def edge_tangents(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_edge_tangents", None) is None: self.edge_lengths # calling .edge_lengths will create the tangents return self._edge_tangents def _get_edge_surf_int_proj_mats(self, only_boundary=False, with_area=True): """Return the projection operators for integrating edges on each face. Parameters ---------- only_boundary : bool, optional Whether to only operate on the boundary faces or not. with_area : bool, optional Whether to include the face area. Returns ------- list of (3 * n_faces, n_edges) scipy.sparse.csr_matrix """ # edges associated with each face... can just get the indices of the curl... face_edges = self.edge_curl.indices.reshape(-1, 4) face_areas = self.face_areas if only_boundary: bf_inds = self.project_face_to_boundary_face.indices face_edges = face_edges[bf_inds] face_areas = face_areas[bf_inds] face_normals = self.boundary_face_outward_normals else: face_normals = self.face_normals # face_edges is edge_x1m, edge_x1p, edge_x2m, edge_x2p for each of them so... edge_inds = [[0, 2], [1, 2], [0, 3], [1, 3]] n_f = face_edges.shape[0] ones = np.ones(n_f * 2) P_indptr = np.arange(2 * n_f + 1) d = np.ones(3, dtype=int)[:, None] * np.arange(2) t = np.arange(n_f) T_col_inds = (d + t[:, None, None] * 2).reshape(-1) T_ind_ptr = 2 * np.arange(3 * n_f + 1) Ps = [] # translate c to fortran ordering C2F_col_inds = np.arange(n_f * 3).reshape((-1, 3), order="C").reshape(-1) C2F_row_inds = np.arange(n_f * 3).reshape((-1, 3), order="F").reshape(-1) C2F = sp.csr_matrix( (np.ones(n_f * 3), (C2F_row_inds, C2F_col_inds)), shape=(n_f * 3, n_f * 3) ) for i in range(4): # matrix which selects the edges associate with each of the nodes of each boundary face node_edges = face_edges[:, edge_inds[i]] P = sp.csr_matrix( (ones, node_edges.reshape(-1), P_indptr), shape=(2 * n_f, self.n_edges) ) edge_dirs = self.edge_tangents[node_edges] t_for = np.concatenate((edge_dirs, face_normals[:, None, :]), axis=1) t_inv = np.linalg.inv(t_for) t_inv = t_inv[:, :, :-1] / 4 # n_edges_per_thing if with_area: t_inv *= face_areas[:, None, None] T = C2F @ sp.csr_matrix( (t_inv.reshape(-1), T_col_inds, T_ind_ptr), shape=(3 * n_f, 2 * n_f), ) Ps.append((T @ P)) return Ps @property def boundary_edge_vector_integral(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: return super().boundary_edge_vector_integral Ps = self._get_edge_surf_int_proj_mats(only_boundary=True, with_area=True) # cross product matrix: # cx = mesh.boundary_face_outward_normals[:, 0] # cy = mesh.boundary_face_outward_normals[:, 1] # cz = mesh.boundary_face_outward_normals[:, 2] # z = np.zeros(n_bf) # # vs = np.stack([np.c_[z, cz, -cy], np.c_[-cz, z, cx], np.c_[cy, -cx, z]], axis=-1) # the cross product of a vector defined on each face with the face outward normal... # so V cross n = -n cross V cx = sp.diags(self.boundary_face_outward_normals[:, 0]) cy = sp.diags(self.boundary_face_outward_normals[:, 1]) cz = sp.diags(self.boundary_face_outward_normals[:, 2]) # the negative cross mat cross_mat = sp.bmat([[None, cz, -cy], [-cz, None, cx], [cy, -cx, None]]) Pf = self.project_face_to_boundary_face Pe = self.project_edge_to_boundary_edge Av = (Pf @ self.average_edge_to_face) @ Pe.T Av = cross_mat @ sp.block_diag((Av, Av, Av)) Me = np.sum(Ps).T @ Av return Me ================================================ FILE: discretize/cylindrical_mesh.py ================================================ """Module containing the cylindrical mesh implementation.""" import numpy as np import scipy.sparse as sp from scipy.constants import pi from discretize.utils import ( kron3, ndgrid, av, av_extrap, speye, ddx, sdiag, spzeros, interpolation_matrix, cyl2cart, as_array_n_by_dim, Identity, ) from discretize.base import BaseTensorMesh, BaseRectangularMesh from discretize.operators import DiffOperators, InnerProducts from discretize.mixins import InterfaceMixins from discretize.utils.code_utils import ( deprecate_class, deprecate_property, deprecate_method, ) class CylindricalMesh( InnerProducts, DiffOperators, BaseTensorMesh, BaseRectangularMesh, InterfaceMixins ): r""" Class for cylindrical meshes. ``CylindricalMesh`` is a mesh class for problems with rotational symmetry. It supports both cylindrically symmetric and 3D cylindrical meshes where the azimuthal discretization is user-defined. In discretize, the coordinates for cylindrical meshes are as follows: - **x:** radial direction (:math:`r`) - **y:** azimuthal direction (:math:`\phi`) - **z:** vertical direction (:math:`z`) Parameters ---------- h : (dim) iterable of int, numpy.ndarray, or tuple Defines the cell widths along each axis. The length of the iterable object is equal to the dimension of the mesh (1, 2 or 3). For a 3D mesh, the list would have the form *[hr, hphi, hz]* . Note that the sum of cell widths in the phi direction **must** equal :math:`2\pi`. You can also use a flat value of *hphi* = *1* to define a cylindrically symmetric mesh. Along each axis, the user has 3 choices for defining the cells widths: - :class:`int` -> A unit interval is equally discretized into `N` cells. - :class:`numpy.ndarray` -> The widths are explicity given for each cell - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])* where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional)is a padding factor denoting exponential increase/decrease in the cell width or each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]* origin : (dim) iterable, default: 0 Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost corner. By default, the mesh is anchored such that its origin is at ``[0, 0, 0]``. For each dimension (r, theta or z), The user may set the origin 2 ways: - a ``scalar`` which explicitly defines origin along that dimension. - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along each axis is the first node location ('0'), in the center ('C') or the last node location ('N'). Examples -------- To create a general 3D cylindrical mesh, we discretize along the radial, azimuthal and vertical axis. For example: >>> from discretize import CylindricalMesh >>> import matplotlib.pyplot as plt >>> import numpy as np >>> ncr = 10 # number of mesh cells in r >>> ncp = 8 # number of mesh cells in phi >>> ncz = 15 # number of mesh cells in z >>> dr = 15 # cell width r >>> dz = 10 # cell width z >>> hr = dr * np.ones(ncr) >>> hp = (2 * np.pi / ncp) * np.ones(ncp) >>> hz = dz * np.ones(ncz) >>> mesh1 = CylindricalMesh([hr, hp, hz]) >>> mesh1.plot_grid() >>> plt.show() For a cylindrically symmetric mesh, the disretization along the azimuthal direction is set with a flag of *1*. This reduces the size of numerical systems given that the derivative along the azimuthal direction is 0. For example: >>> ncr = 10 # number of mesh cells in r >>> ncz = 15 # number of mesh cells in z >>> dr = 15 # cell width r >>> dz = 10 # cell width z >>> npad_r = 4 # number of padding cells in r >>> npad_z = 4 # number of padding cells in z >>> exp_r = 1.25 # expansion rate of padding cells in r >>> exp_z = 1.25 # expansion rate of padding cells in z A value of 1 is used to define the discretization in phi for this case. >>> hr = [(dr, ncr), (dr, npad_r, exp_r)] >>> hz = [(dz, npad_z, -exp_z), (dz, ncz), (dz, npad_z, exp_z)] >>> mesh2 = CylindricalMesh([hr, 1, hz]) >>> mesh2.plot_grid() >>> plt.show() """ _meshType = "CYL" _unitDimensions = [1, 2 * np.pi, 1] _aliases = { **DiffOperators._aliases, **BaseRectangularMesh._aliases, **BaseTensorMesh._aliases, } _items = BaseTensorMesh._items | {"cartesian_origin"} def __init__(self, h, origin=None, cartesian_origin=None, **kwargs): kwargs.pop("reference_system", None) # reference system must be cylindrical if "cartesianOrigin" in kwargs.keys(): cartesian_origin = kwargs.pop("cartesianOrigin") super().__init__(h=h, origin=origin, reference_system="cylindrical", **kwargs) if self.h[1].sum() > 2 * np.pi + 1e-10: raise ValueError("The 2nd dimension must cannot sum to more than 2*pi.") if self.dim == 2: print("Warning, a disk mesh has not been tested thoroughly.") if cartesian_origin is None: cartesian_origin = np.zeros(self.dim) self.cartesian_origin = cartesian_origin @property def cartesian_origin(self): """Cartesian origin of the mesh. Returns the origin or 'anchor point' of the cylindrical mesh in Cartesian coordinates; i.e. [x0, y0, z0]. For cylindrical meshes, the origin is the bottom of the z-axis which defines the mesh's rotational symmetry. Returns ------- (dim) numpy.ndarray The Cartesian origin (or anchor point) of the mesh """ return self._cartesian_origin @cartesian_origin.setter def cartesian_origin(self, value): # ensure the value is a numpy array value = np.asarray(value, dtype=np.float64) value = np.atleast_1d(value) if len(value) != self.dim: raise ValueError( f"cartesian origin and shape must be the same length, got {len(value)} and {self.dim}" ) self._cartesian_origin = value @property def is_wrapped(self): """Whether the mesh discretizes the full azimuthal space. Returns ------- bool """ return np.allclose(self.h[1].sum(), 2 * np.pi, atol=1e-10) @property def is_symmetric(self): """Validate whether mesh is symmetric. Symmetric cylindrical meshes have useful mathematical properties that allow us to reduce the computational cost of solving radially symmetric 3D problems. When constructing cylindrical meshes in discretize, we almost always use a flag of *1* when defining the discretization in the azimuthal direction. By doing so, we define a mesh that is symmetric. In this case, the *is_symmetric* returns a value of *True* . If the discretization in the azimuthal direction was defined explicitly, the mesh would not be symmetric and *is_symmetric* would return a value of *False* . Returns ------- bool *True* if the mesh is symmetric, *False* otherwise """ return self.is_wrapped and self.shape_cells[1] == 1 @property def includes_zero(self): """Whether the radial dimension starts at 0. Returns ------- bool """ return self.origin[0] == 0.0 @property def shape_nodes(self): """Return the number of nodes along each axis. This property returns a tuple containing the number of nodes along the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) directions, respectively. In the case where the mesh is symmetric, the number of nodes defining the discretization in the azimuthal direction is *0* ; see :py:attr:`~.CylindricalMesh.is_symmetric`. Returns ------- (dim) tuple of int Number of nodes in the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) directions, respectively. """ vnC = self.shape_cells if self.is_symmetric: if self.includes_zero: return (vnC[0], 0, vnC[2] + 1) return (vnC[0] + 1, 0, vnC[2] + 1) elif self.is_wrapped: return (vnC[0] + 1, vnC[1], vnC[2] + 1) else: return super().shape_nodes @property def _shape_total_nodes(self): vnC = self.shape_cells if self.is_symmetric: if self.includes_zero: return (vnC[0], 1, vnC[2] + 1) return (vnC[0] + 1, 1, vnC[2] + 1) else: return tuple(x + 1 for x in vnC) @property def n_nodes(self): """Return total number of mesh nodes. For non-symmetric cylindrical meshes, this property returns the total number of nodes. For symmetric meshes, this property returns a value of 0; see :py:attr:`~.CylindricalMesh.is_symmetric`. The symmetric mesh case is unique because the azimuthal position of the nodes is undefined. Returns ------- int Total number of nodes for non-symmetric meshes, 0 for symmetric meshes """ if self.is_symmetric: return 0 nx, ny, nz = self.shape_nodes if self.includes_zero: return (nx - 1) * ny * nz + nz else: return nx * ny * nz @property def _n_total_nodes(self): return int(np.prod(self._shape_total_nodes)) @property def _shape_total_faces_x(self): """Vector number of total Fx (prior to deflating).""" return self._shape_total_nodes[:1] + self.shape_cells[1:] @property def _n_total_faces_x(self): """Number of total Fx (prior to deflating).""" return int(np.prod(self._shape_total_faces_x)) @property def _n_hanging_faces_x(self): """Number of hanging Fx.""" if self.includes_zero: return int(np.prod(self.shape_cells[1:])) else: return 0 @property def shape_faces_x(self): """Number of x-faces along each axis direction. This property returns the number of x-faces along the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) directions, respectively. Note that for symmetric meshes, the number of x-faces along the azimuthal direction is 1; see :py:attr:`~.CylindricalMesh.is_symmetric`. Returns ------- (dim) tuple of int Number of x-faces along the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) directions, respectively. """ if self.includes_zero: return self.shape_cells else: return super().shape_faces_x @property def _shape_total_faces_y(self): """Vector number of total Fy (prior to deflating).""" vnC = self.shape_cells return (vnC[0], self._shape_total_nodes[1]) + vnC[2:] @property def _n_total_faces_y(self): """Number of total Fy (prior to deflating).""" return int(np.prod(self._shape_total_faces_y)) @property def _n_hanging_faces_y(self): """Number of hanging y-faces.""" if self.is_wrapped: return int(np.prod(self.shape_cells[::2])) else: return 0 @property def _shape_total_faces_z(self): """Vector number of total Fz (prior to deflating).""" return self.shape_cells[:-1] + self._shape_total_nodes[-1:] @property def _n_total_faces_z(self): """Number of total Fz (prior to deflating).""" return int(np.prod(self._shape_total_faces_z)) @property def _n_hanging_faces_z(self): """Number of hanging Fz.""" return 0 @property def _shape_total_edges_x(self): """Vector number of total Ex (prior to deflating).""" return self.shape_cells[:1] + self._shape_total_nodes[1:] @property def _n_total_edges_x(self): """Number of total Ex (prior to deflating).""" return int(np.prod(self._shape_total_edges_x)) @property def _shape_total_edges_y(self): """Vector number of total Ey (prior to deflating).""" _shape_total_nodes = self._shape_total_nodes return (_shape_total_nodes[0], self.shape_cells[1], _shape_total_nodes[2]) @property def _n_total_edges_y(self): """Number of total Ey (prior to deflating).""" return int(np.prod(self._shape_total_edges_y)) @property def shape_edges_y(self): """Number of y-edges along each axis direction. This property returns the number of y-edges along the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) directions, respectively. Note that for symmetric meshes, the number of y-edges along the azimuthal direction is 1; see :py:attr:`~.CylindricalMesh.is_symmetric`. Returns ------- (dim) tuple of int Number of y-edges along the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) directions, respectively. """ if self.includes_zero: return tuple(x + y for x, y in zip(self.shape_cells, [0, 0, 1])) else: return tuple(x + y for x, y in zip(self.shape_cells, [1, 0, 1])) @property def _shape_total_edges_z(self): """Vector number of total Ez (prior to deflating).""" return self._shape_total_nodes[:-1] + self.shape_cells[-1:] @property def _n_total_edges_z(self): """Number of total Ez (prior to deflating).""" return int(np.prod(self._shape_total_edges_z)) @property def shape_edges_z(self): """Number of z-edges along each axis direction. This property returns the number of z-edges along the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) directions, respectively. Note that for symmetric meshes, the number of z-edges along the azimuthal direction is 0; see :py:attr:`~.CylindricalMesh.is_symmetric`. The symmetric mesh case is unique because the azimuthal position of the z-edges is undefined. Returns ------- (dim) tuple of int Number of z-edges along the :math:`x` (radial), :math:`y` (azimuthal) and :math:`z` (vertical) direction, respectively. """ return self.shape_nodes[:-1] + self.shape_cells[-1:] @property def n_edges_z(self): """Total number of z-edges in the mesh. This property returns the total number of z-edges for non-symmetric cyindrical meshes; see :py:attr:`~.CylindricalMesh.is_symmetric`. If the mesh is symmetric, the property returns *0* because the azimuthal position of the z-edges is undefined. Returns ------- int Number of z-edges for non-symmetric meshes and *0* for symmetric meshes """ z_shape = self.shape_edges_z cell_shape = self.shape_cells if self.is_symmetric: return int(np.prod(z_shape)) if self.includes_zero: return ( int(np.prod([z_shape[0] - 1, z_shape[1], cell_shape[2]])) + cell_shape[2] ) return int(np.prod(z_shape)) @property def cell_centers_x(self): """Return the x-positions of cell centers along the x-direction. This property returns a 1D vector containing the x-position values of the cell centers along the x-direction (radial). The length of the vector is equal to the number of cells in the x-direction. Returns ------- (n_cells_x) numpy.ndarray x-positions of cell centers along the x-direction """ nodes = self.nodes_x ccx = 0.5 * (nodes[1:] + nodes[:-1]) if self.is_symmetric and self.includes_zero: return np.r_[self.h[0][0] * 0.5, ccx] return ccx # return np.r_[self.origin[0], self.h[0][:-1].cumsum()] + self.h[0] * 0.5 @property def cell_centers_y(self): """Return the y-positions of cell centers along the y-direction (azimuthal). This property returns a 1D vector containing the y-position values of the cell centers along the y-direction (azimuthal). The length of the vector is equal to the number of cells in the y-direction. If the mesh is symmetric, this property returns a numpy array with a single entry of *0* ; indicating all cell-centers have a y-position of 0. see :py:attr:`~.CylindricalMesh.is_symmetric`. Returns ------- (n_cells_y) numpy.ndarray y-positions of cell centers along the y-direction """ if self.is_symmetric: return np.r_[self.origin[1]] nodes = self._nodes_y_full return (nodes[1:] + nodes[:-1]) / 2 @property def nodes_x(self): """Return the x-positions of nodes along the x-direction (radial). This property returns a 1D vector containing the x-position values of the nodes along the x-direction (radial). The length of the vector is equal to the number of nodes in the x-direction. Returns ------- (n_nodes_x) numpy.ndarray x-positions of nodes along the x-direction """ nodes = np.r_[self.origin[0], self.h[0]].cumsum() if self.is_symmetric and self.includes_zero: return nodes[1:] return nodes @property def _nodes_y_full(self): """Full nodal y vector (prior to deflating).""" if self.is_symmetric: return np.r_[self.origin[1]] return self.origin[1] + np.r_[0, self.h[1].cumsum()] @property def nodes_y(self): """Return the y-positions of nodes along the y-direction (azimuthal). This property returns a 1D vector containing the y-position values of the nodes along the y-direction (azimuthal). If the mesh is symmetric, this property returns a numpy array with a single entry of *0* ; indicating all nodes have a y-position of 0. See :py:attr:`~.CylindricalMesh.is_symmetric`. Returns ------- (n_nodes_y) numpy.ndarray y-positions of nodes along the y-direction """ if self.is_wrapped: return self.origin[1] + np.r_[0, self.h[1][:-1].cumsum()] return super().nodes_y @property def _edge_x_lengths_full(self): """Full x-edge lengths (prior to deflating).""" nx, ny, nz = self._shape_total_nodes return np.kron(np.ones(nz), np.kron(np.ones(ny), self.h[0])) @property def edge_x_lengths(self): """Lengths of each x edge for the entire mesh. If the mesh is not symmetric, this property returns a 1D vector containing the lengths of all x-edges in the mesh. If the mesh is symmetric, this property returns an empty numpy array since there are no x-edges; see :py:attr:`~CylindricalMesh.is_symmetric`. Returns ------- (n_edges_x) numpy.ndarray A 1D array containing the x-edge lengths for the entire mesh """ if getattr(self, "_edge_lengths_x", None) is None: self._edge_lengths_x = self._edge_x_lengths_full[~self._ishanging_edges_x] return self._edge_lengths_x @property def _edge_y_lengths_full(self): """Full vector of y-edge lengths (prior to deflating).""" if self.is_symmetric: return 2 * pi * self.nodes[:, 0] return np.kron( np.ones(self._shape_total_nodes[2]), np.kron(self.h[1], self.nodes_x) ) @property def edge_y_lengths(self): r"""Arc-lengths of each y-edge for the entire mesh. This property returns a 1D vector containing the arc-lengths of all y-edges in the mesh. For a single y-edge at radial location :math:`r` with azimuthal width :math:`\\Delta \\phi`, the arc-length is given by: .. math:: \Delta y = r \Delta \phi Returns ------- (n_edges_y) numpy.ndarray A 1D array containing the y-edge arc-lengths for the entire mesh """ if getattr(self, "_edge_lengths_y", None) is None: if self.is_symmetric: self._edge_lengths_y = self._edge_y_lengths_full else: self._edge_lengths_y = self._edge_y_lengths_full[ ~self._ishanging_edges_y ] return self._edge_lengths_y @property def _edge_z_lengths_full(self): """Full z-edge lengths (prior to deflation).""" nx, ny, nz = self._shape_total_nodes return np.kron(self.h[2], np.kron(np.ones(ny), np.ones(nx))) @property def edge_z_lengths(self): """Lengths of each z-edges for the entire mesh. If the mesh is not symmetric, this property returns a 1D vector containing the lengths of all z-edges in the mesh. If the mesh is symmetric, this property returns an empty numpy array since there are no z-edges; see :py:attr:`~CylindricalMesh.is_symmetric`. Returns ------- (n_edges_z) numpy.ndarray A 1D array containing the z-edge lengths for the entire mesh """ if getattr(self, "_edge_lengths_z", None) is None: self._edge_lengths_z = self._edge_z_lengths_full[~self._ishanging_edges_z] return self._edge_lengths_z @property def _edge_lengths_full(self): """Full edge lengths [r-edges, theta-edges z-edges] (prior to deflation).""" if self.is_symmetric: raise NotImplementedError else: return np.r_[ self._edge_x_lengths_full, self._edge_y_lengths_full, self._edge_z_lengths_full, ] @property def edge_lengths(self): """Lengths of all mesh edges. This property returns a 1D vector containing the lengths of all edges in the mesh organized by x-edges, y-edges, then z-edges; i.e. radial, azimuthal, then vertical. However if the mesh is symmetric, there are no x or z-edges and calling the property returns the y-edge lengths; see :py:attr:`~CylindricalMesh.is_symmetric`. Note that y-edge lengths take curvature into account; see :py:attr:`~.CylindricalMesh.edge_y_lengths`. Returns ------- (n_edges) numpy.ndarray Edge lengths of all mesh edges organized x (radial), y (azimuthal), then z (vertical) """ if self.is_symmetric: return self.edge_y_lengths else: return np.r_[self.edge_x_lengths, self.edge_y_lengths, self.edge_z_lengths] @property def _face_x_areas_full(self): """Areas of x-faces prior to deflation.""" if self.is_symmetric: return np.kron(self.h[2], 2 * pi * self.nodes_x) return np.kron(self.h[2], np.kron(self.h[1], self.nodes_x)) @property def face_x_areas(self): r"""Areas of each x-face for the entire mesh. This property returns a 1D vector containing the areas of the x-faces of the mesh. The surface area takes into account curvature. For a single x-face at radial location :math:`r` with azimuthal width :math:`\Delta \phi` and vertical width :math:`h_z`, the area is given by: .. math:: A_x = r \Delta \phi h_z Returns ------- (n_faces_x) numpy.ndarray A 1D array containing the x-face areas for the entire mesh """ if getattr(self, "_face_x_areas", None) is None: if self.is_symmetric: self._face_x_areas = self._face_x_areas_full else: self._face_x_areas = self._face_x_areas_full[~self._ishanging_faces_x] return self._face_x_areas @property def _face_y_areas_full(self): """Area of y-faces (Azimuthal faces), prior to deflation.""" return np.kron( self.h[2], np.kron(np.ones(self._shape_total_nodes[1]), self.h[0]) ) @property def face_y_areas(self): """Areas of each y-face for the entire mesh. This property returns a 1D vector containing the areas of the y-faces of the mesh. For a single y-face with edge lengths :math:`h_x` and :math:`h_z`, the area is given by: .. math:: A_y = h_x h_z *Note that for symmetric meshes* , there are no y-faces and calling this property will return an error. Returns ------- (n_faces_y) numpy.ndarray A 1D array containing the y-face areas in the case of non-symmetric meshes. Returns an error for symmetric meshes. """ if getattr(self, "_face_y_areas", None) is None: if self.is_symmetric: raise AttributeError("There are no y-faces on the Cyl Symmetric mesh") self._face_y_areas = self._face_y_areas_full[~self._ishanging_faces_y] return self._face_y_areas @property def _face_z_areas_full(self): """Area of z-faces prior to deflation.""" if self.is_symmetric and self.includes_zero: return np.kron( np.ones_like(self.nodes_z), pi * (self.nodes_x**2 - np.r_[0, self.nodes_x[:-1]] ** 2), ) return np.kron( np.ones(self._shape_total_nodes[2]), np.kron( self.h[1], 0.5 * (self.nodes_x[1:] ** 2 - self.nodes_x[:-1] ** 2), ), ) @property def face_z_areas(self): r"""Areas of each z-face for the entire mesh. This property returns a 1D vector containing the areas of the z-faces of the mesh. The surface area takes into account curvature. For a single z-face at between :math:`r_1` and :math:`r_2` with azimuthal width :math:`\Delta \phi`, the area is given by: .. math:: A_z = \frac{\Delta \phi}{2} (r_2^2 - r_1^2) Returns ------- (n_faces_z) numpy.ndarray A 1D array containing the z-face areas for the entire mesh """ if getattr(self, "_face_z_areas", None) is None: if self.is_symmetric: self._face_z_areas = self._face_z_areas_full else: self._face_z_areas = self._face_z_areas_full[~self._ishanging_faces_z] return self._face_z_areas @property def _face_areas_full(self): """Area of all faces (prior to deflation).""" return np.r_[ self._face_x_areas_full, self._face_y_areas_full, self._face_z_areas_full ] @property def face_areas(self): """Face areas for the entire mesh. This property returns a 1D vector containing the areas of all mesh faces organized by x-faces, y-faces, then z-faces; i.e. faces normal to the radial, azimuthal, then vertical direction. Note that for symmetric meshes, there are no y-faces and calling the property will return only the x and z-faces. To see how the face areas corresponding to each component are computed, see :py:attr:`~.CylindricalMesh.face_x_areas`, :py:attr:`~.CylindricalMesh.face_y_areas` and :py:attr:`~.CylindricalMesh.face_z_areas`. Returns ------- (n_faces) numpy.ndarray Areas of all faces in the mesh """ # if getattr(self, '_area', None) is None: if self.is_symmetric: return np.r_[self.face_x_areas, self.face_z_areas] else: return np.r_[self.face_x_areas, self.face_y_areas, self.face_z_areas] @property def cell_volumes(self): r"""Volumes of all mesh cells. This property returns a 1D vector containing the volumes of all cells in the mesh. When computing the volume of each cell, we take into account curvature. Thus a cell lying within radial distance :math:`r_1` and :math:`r_2`, with height :math:`h_z` and with azimuthal width :math:`\Delta \phi`, the volume is given by: .. math:: V = \frac{\Delta \phi \, h_z}{2} (r_2^2 - r_1^2) Returns ------- (n_cells numpy.ndarray Volumes of all mesh cells """ if getattr(self, "_cell_volumes", None) is None: if self.is_symmetric and self.includes_zero: az = pi * (self.nodes_x**2 - np.r_[0, self.nodes_x[:-1]] ** 2) self._cell_volumes = np.kron(self.h[2], az) else: self._cell_volumes = np.kron( self.h[2], np.kron( self.h[1], 0.5 * (self.nodes_x[1:] ** 2 - self.nodes_x[:-1] ** 2), ), ) return self._cell_volumes @property def _ishanging_faces_x(self): """Boolean vector indicating if an x-face is hanging or not.""" if getattr(self, "_ishanging_faces_x_bool", None) is None: hang_x = np.zeros(self._shape_total_faces_x, dtype=bool, order="F") if self.includes_zero and not self.is_symmetric: hang_x[0] = True self._ishanging_faces_x_bool = hang_x.reshape(-1, order="F") return self._ishanging_faces_x_bool @property def _hanging_faces_x(self): """Hanging x-faces dictionary mapping. Dictionary of the indices of the hanging x-faces (keys) and a list of indices that the eliminated faces map to (if applicable) """ if getattr(self, "_hanging_faces_x_dict", None) is None: if self.includes_zero: hanging_f = np.where(self._ishanging_faces_x)[0] # Mark as None to remove them... deflate_f = [None] * len(hanging_f) else: hanging_f = deflate_f = [] self._hanging_faces_x_dict = dict(zip(hanging_f, deflate_f)) return self._hanging_faces_x_dict @property def _ishanging_faces_y(self): """Boolean vector indicating if a y-face is hanging or not.""" if getattr(self, "_ishanging_faces_y_bool", None) is None: hang_y = np.zeros(self._shape_total_faces_y, dtype=bool, order="F") if self.is_wrapped: hang_y[:, -1] = True self._ishanging_faces_y_bool = hang_y.reshape(-1, order="F") return self._ishanging_faces_y_bool @property def _hanging_faces_y(self): """Hanging y-faces dictionary mapping. Dictionary of the indices of the hanging y-faces (keys) and a list of indices that the eliminated faces map to (if applicable). """ if getattr(self, "_hanging_faces_y_dict", None) is None: hanging_f = np.where(self._ishanging_faces_y)[0] nx, ny, nz = self._shape_total_faces_y irs, its, izs = np.unravel_index(hanging_f, (nx, ny, nz), order="F") if self.is_wrapped: ny = ny - 1 its %= ny deflate_f = np.ravel_multi_index((irs, its, izs), (nx, ny, nz), order="F") self._hanging_faces_y_dict = dict(zip(hanging_f, deflate_f)) return self._hanging_faces_y_dict @property def _ishanging_faces_z(self): """Boolean vector indicating if a z-face is hanging or not.""" if getattr(self, "_ishanging_faces_z_bool", None) is None: self._ishanging_faces_z_bool = np.zeros(self._n_total_faces_z, dtype=bool) return self._ishanging_faces_z_bool @property def _hanging_faces_z(self): """Hanging z-faces dictionary mapping. Dictionary of the indices of the hanging z-faces (keys) and a list of indices that the eliminated faces map to (if applicable). """ return {} @property def _ishanging_edges_x(self): """Boolean vector indicating if a x-edge is hanging or not.""" if getattr(self, "_ishanging_edges_x_bool", None) is None: hang_x = np.zeros(self._shape_total_edges_x, dtype=bool, order="F") if self.is_wrapped: hang_x[:, -1] = True self._ishanging_edges_x_bool = hang_x.reshape(-1, order="F") return self._ishanging_edges_x_bool @property def _hanging_edges_x(self): """Hanging x-edges dictionary mapping. Dictionary of the indices of the hanging x-edges (keys) and a list of indices that the eliminated faces map to (if applicable). """ if getattr(self, "_hanging_edges_x_dict", None) is None: hanging_e = np.where(self._ishanging_edges_x)[0] nx, ny, nz = self._shape_total_edges_x irs, its, izs = np.unravel_index(hanging_e, (nx, ny, nz), order="F") if self.is_wrapped: ny = ny - 1 its %= ny deflated_e = np.ravel_multi_index((irs, its, izs), (nx, ny, nz), order="F") self._hanging_edges_x_dict = dict(zip(hanging_e, deflated_e)) return self._hanging_edges_x_dict @property def _ishanging_edges_y(self): """Boolean vector indicating if a y-edge is hanging or not.""" if getattr(self, "_ishanging_edges_y_bool", None) is None: hang_y = np.zeros(self._shape_total_edges_y, dtype=bool, order="F") if self.includes_zero or self.is_symmetric: hang_y[0] = True self._ishanging_edges_y_bool = hang_y.reshape(-1, order="F") return self._ishanging_edges_y_bool @property def _hanging_edges_y(self): """Hanging y-edges dictionary mapping. Dictionary of the indices of the hanging y-edges (keys) and a list of indices that the eliminated faces map to (if applicable). """ if getattr(self, "_hanging_edges_y_dict", None) is None: if self.includes_zero: hanging_e = np.where(self._ishanging_edges_y)[0] # Mark as None to remove them... deflate_e = [None] * len(hanging_e) else: hanging_e = deflate_e = [] self._hanging_edges_y_dict = dict(zip(hanging_e, deflate_e)) return self._hanging_edges_y_dict @property def _ishanging_edges(self): if self.dim == 2: return np.r_[self._ishanging_edges_x, self._ishanging_edges_y] else: return np.r_[ self._ishanging_edges_x, self._ishanging_edges_y, self._ishanging_edges_z, ] @property def _ishanging_edges_z(self): """Boolean vector indicating if a z-edge is hanging or not.""" if getattr(self, "_ishanging_edges_z_bool", None) is None: if self.is_symmetric: self._ishanging_edges_z_bool = np.ones( self._n_total_edges_z, dtype=bool ) else: is_hanging = np.zeros(self._shape_total_edges_z, dtype=bool, order="F") if self.includes_zero: is_hanging[0] = True # axis of symmetry nodes are hanging is_hanging[0, 0] = ( False # axis of symmetry nodes which are not hanging ) if self.is_wrapped: is_hanging[:, -1] = ( True # nodes at maximum theta that are duplicated ) self._ishanging_edges_z_bool = is_hanging.reshape(-1, order="F") return self._ishanging_edges_z_bool @property def _hanging_edges_z(self): """Hanging z-edges dictionary mapping. Dictionary of the indices of the hanging z-edges (keys) and a list of indices that the eliminated faces map to (if applicable). """ if getattr(self, "_hanging_edges_z_dict", None) is None: hanging_e = np.where(self._ishanging_edges_z)[0] nx, ny, nz = self._shape_total_edges_z irs, its, izs = np.unravel_index(hanging_e, (nx, ny, nz), order="F") # If wrapped, map max it to it=0. if self.is_wrapped: ny = ny - 1 its %= ny # If I include zero, wrap all the thetas at the center together if self.includes_zero: centers = irs == 0 its[centers] = 0 deflated_e = irs + ny * its + ((nx - 1) * ny + 1) * izs else: deflated_e = np.ravel_multi_index( (irs, its, izs), (nx, ny, nz), order="F" ) self._hanging_edges_z_dict = dict(zip(hanging_e, deflated_e)) return self._hanging_edges_z_dict @property def _ishanging_nodes(self): """Boolean vector indicating if a node is hanging or not.""" if getattr(self, "_ishanging_nodes_bool", None) is None: if self.is_symmetric: self._ishanging_nodes_bool = np.zeros(self._n_total_nodes, dtype=bool) else: is_hanging = np.zeros(self._shape_total_nodes, dtype=bool, order="F") if self.includes_zero: is_hanging[0, 1:] = True # axis of symmetry nodes that are hanging if self.is_wrapped: is_hanging[:, -1] = ( True # nodes at maximum theta that are duplicated ) self._ishanging_nodes_bool = is_hanging.reshape(-1, order="F") return self._ishanging_nodes_bool @property def _hanging_nodes(self): """Hanging nodes dictionary mapping. Dictionary of the indices of the hanging nodes (keys) and a list of indices that the eliminated nodes map to (if applicable). """ if getattr(self, "_hanging_nodes_dict", None) is None: hanging_nodes = np.where(self._ishanging_nodes)[0] nx, ny, nz = self._shape_total_nodes irs, its, izs = np.unravel_index(hanging_nodes, (nx, ny, nz), order="F") # If wrapped, map max it to it=0. if self.is_wrapped: ny = ny - 1 its %= ny # If I include zero, wrap all the thetas at the center together if self.includes_zero: centers = irs == 0 its[centers] = 0 deflated_n = irs + ny * its + ((nx - 1) * ny + 1) * izs else: deflated_n = np.ravel_multi_index( (irs, its, izs), (nx, ny, nz), order="F" ) self._hanging_nodes_dict = dict(zip(hanging_nodes, deflated_n)) return self._hanging_nodes_dict #################################################### # Grids #################################################### @property def _nodes_full(self): """Full Nodal grid (including hanging nodes).""" return ndgrid([self.nodes_x, self._nodes_y_full, self.nodes_z]) @property def nodes(self): r"""Gridded node locations. This property outputs a numpy array containing the gridded locations of all mesh nodes in cylindrical ccordinates; i.e. :math:`(r, \phi, z)`. Note that for symmetric meshes, the azimuthal position of all nodes is set to :math:`\phi = 0`. Returns ------- (n_nodes, dim) numpy.ndarray gridded node locations """ if self.is_symmetric: self._nodes = self._nodes_full if getattr(self, "_nodes", None) is None: self._nodes = self._nodes_full[~self._ishanging_nodes, :] return self._nodes @property def _faces_x_full(self): """Full Fx grid (including hanging faces).""" return ndgrid([self.nodes_x, self.cell_centers_y, self.cell_centers_z]) @property def faces_x(self): r"""Gridded x-face (radial face) locations. This property outputs a numpy array containing the gridded locations of all x-faces (radial faces) in cylindrical coordinates; i.e. the :math:`(r, \phi, z)` position of the center of each face. The shape of the array is (n_faces_x, 3). Note that for symmetric meshes, the azimuthal position of all x-faces is set to :math:`\phi = 0`. Returns ------- (n_faces_x, dim) numpy.ndarray gridded x-face (radial face) locations """ if getattr(self, "_faces_x", None) is None: if self.is_symmetric: return super().faces_x else: self._faces_x = self._faces_x_full[~self._ishanging_faces_x, :] return self._faces_x @property def _edges_y_full(self): """Full grid of y-edges (including eliminated edges).""" return super().edges_y @property def edges_y(self): r"""Gridded y-edge (azimuthal edge) locations. This property outputs a numpy array containing the gridded locations of all y-edges (azimuthal edges) in cylindrical coordinates; i.e. the :math:`(r, \phi, z)` position of the middle of each y-edge. The shape of the array is (n_edges_y, 3). Note that for symmetric meshes, the azimuthal position of all y-edges is set to :math:`\phi = 0`. Returns ------- (n_edges_y, dim) numpy.ndarray gridded y-edge (azimuthal edge) locations """ if getattr(self, "_edges_y", None) is None: if self.is_symmetric: return self._edges_y_full else: self._edges_y = self._edges_y_full[~self._ishanging_edges_y, :] return self._edges_y @property def _edges_z_full(self): """Full z-edge grid (including hanging edges).""" return ndgrid([self.nodes_x, self._nodes_y_full, self.cell_centers_z]) @property def edges_z(self): r"""Gridded z-edge (vertical edge) locations. This property outputs a numpy array containing the gridded locations of all z-edges (vertical edges) in cylindrical coordinates; i.e. the :math:`(r, \phi, z)` position of the middle of each z-edge. The shape of the array is (n_edges_z, 3). In the case of symmetric meshes, there are no z-edges and this property returns *None*. Returns ------- (n_edges_z, dim) numpy.ndarray or None gridded z-edge (vertical edge) locations. Returns *None* for symmetric meshes. """ if getattr(self, "_edges_z", None) is None: if self.is_symmetric: self._edges_z = None else: self._edges_z = self._edges_z_full[~self._ishanging_edges_z, :] return self._edges_z @property def _is_boundary_face(self): is_br = np.zeros(self._shape_total_faces_x, dtype=bool, order="F") # if I don't start at r=0, then the inner faces are boundary faces if not self.includes_zero: is_br[0] = True # outer most radial faces are a boundaries is_br[-1] = True is_br = is_br.reshape(-1, order="F") # Theta face is on a boundary if not wrapped is_bt = np.zeros(self._shape_total_faces_y, dtype=bool, order="F") if not self.is_wrapped: is_bt[:, [0, -1]] = True is_bt = is_bt.reshape(-1, order="F") # top and bottom faces are boundaries is_bz = np.zeros(self.shape_faces_z, dtype=bool, order="F") is_bz[:, :, [0, -1]] = True is_bz = is_bz.reshape(-1, order="F") is_b = np.r_[ is_br[~self._ishanging_faces_x], is_bt[~self._ishanging_faces_y], is_bz ] return is_b @property def boundary_faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.faces[self._is_boundary_face] @property def boundary_face_outward_normals(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh normals = self.face_normals[self._is_boundary_face] # determine which to flip neg_x = np.zeros(self._shape_total_faces_x, dtype=bool, order="F") neg_x[0] = True neg_x = neg_x.reshape(-1, order="F") neg_y = np.zeros(self._shape_total_faces_y, dtype=bool, order="F") neg_y[:, 0] = True neg_y = neg_y.reshape(-1, order="F") neg_z = np.zeros(self._shape_total_faces_z, dtype=bool, order="F") neg_z[:, :, 0] = True neg_z = neg_z.reshape(-1, order="F") neg = np.r_[ neg_x[~self._ishanging_faces_x], neg_y[~self._ishanging_faces_y], neg_z ][self._is_boundary_face] # then n_cells_theta * n_cells_r, bottom faces, normals[neg] = -normals[neg] return normals @property def _is_boundary_node(self): is_b = np.zeros(self._shape_total_nodes, dtype=bool, order="F") # outward rs are boundary: is_b[-1] = True if not self.includes_zero: is_b[0] = True if not self.is_wrapped: is_b[:, [0, -1]] = True # top and bottom zs are boundary is_b[:, :, [0, -1]] = True is_b = is_b.reshape(-1, order="F") is_b = is_b[~self._ishanging_nodes] return is_b @property def boundary_nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.nodes[self._is_boundary_node] @property def _is_boundary_edge(self): # top and bottom radial edges are on the boundary is_br = np.zeros(self._shape_total_edges_x, dtype=bool, order="F") is_br[:, :, [0, -1]] = True if not self.is_wrapped: is_br[:, [0, -1]] = True is_br = is_br.reshape(-1, order="F") is_bt = np.zeros(self._shape_total_edges_y, dtype=bool, order="F") # outside theta edges are on boundary is_bt[-1] = True if not self.includes_zero: is_bt[0] = True # top and bottom theta edges are on boundary is_bt[:, :, [0, -1]] = True is_bt = is_bt.reshape(-1, order="F") # outside z edges are on boundaries is_bz = np.zeros(self._shape_total_edges_z, dtype=bool, order="F") is_bz[-1] = True if not self.includes_zero or not self.is_wrapped: is_bz[0] = True is_bz = is_bz.reshape(-1, order="F") is_b = np.r_[ is_br[~self._ishanging_edges_x], is_bt[~self._ishanging_edges_y], is_bz[~self._ishanging_edges_z], ] return is_b @property def boundary_edges(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.edges[self._is_boundary_edge] #################################################### # Operators #################################################### @property def face_divergence(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_face_divergence", None) is None: # Compute faceDivergence operator on faces D1 = self.face_x_divergence D3 = self.face_z_divergence if self.is_symmetric: D = sp.hstack((D1, D3), format="csr") elif self.shape_cells[1] > 1: D2 = self.face_y_divergence D = sp.hstack((D1, D2, D3), format="csr") self._face_divergence = D return self._face_divergence @property def face_x_divergence(self): # NOQA D102 # Documentation inherited from discretize.base.BaseTensorMesh if getattr(self, "_face_x_divergence", None) is None: if self.is_symmetric and self.includes_zero: ncx, ncy, ncz = self.shape_cells D1 = kron3(speye(ncz), speye(ncy), ddx(ncx)[:, 1:]) else: D1 = super()._face_x_divergence_stencil S = self._face_x_areas_full V = self.cell_volumes self._face_x_divergence = sdiag(1 / V) * D1 * sdiag(S) if not self.is_symmetric: self._face_x_divergence = ( self._face_x_divergence * self._deflation_matrix("Fx", as_ones=True).T ) return self._face_x_divergence @property def face_y_divergence(self): # NOQA D102 # Documentation inherited from discretize.base.BaseTensorMesh if getattr(self, "_face_y_divergence", None) is None: D2 = super()._face_y_divergence_stencil S = self._face_y_areas_full # self.reshape(self.face_areas, 'F', 'Fy', 'V') V = self.cell_volumes self._face_y_divergence = ( sdiag(1 / V) * D2 * sdiag(S) * self._deflation_matrix("Fy", as_ones=True).T ) return self._face_y_divergence @property def face_z_divergence(self): # NOQA D102 # Documentation inherited from discretize.base.BaseTensorMesh if getattr(self, "_face_z_divergence", None) is None: D3 = super()._face_z_divergence_stencil S = self._face_z_areas_full V = self.cell_volumes self._face_z_divergence = sdiag(1 / V) * D3 * sdiag(S) return self._face_z_divergence @property def cell_gradient_x(self): # NOQA D102 # Documentation inherited from discretize.base.BaseTensorMesh raise NotImplementedError("Cell Grad is not yet implemented.") # if getattr(self, '_cellGradx', None) is None: # G1 = super(CylindricalMesh, self).stencil_cell_gradient_x # V = self._deflation_matrix('F', withHanging='True', as_ones='True')*self.aveCC2F*self.cell_volumes # A = self.face_areas # L = (A/V)[:self._n_total_faces_x] # # L = self.reshape(L, 'F', 'Fx', 'V') # # L = A[:self.nFx] / V # self._cellGradx = self._deflation_matrix('Fx')*sdiag(L)*G1 # return self._cellGradx @property def stencil_cell_gradient_y(self): # NOQA D102 # Documentation inherited from discretize.base.BaseTensorMesh raise NotImplementedError("Cell Grad is not yet implemented.") @property def stencil_cell_gradient_z(self): # NOQA D102 # Documentation inherited from discretize.base.BaseTensorMesh raise NotImplementedError("Cell Grad is not yet implemented.") @property def stencil_cell_gradient(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh raise NotImplementedError("Cell Grad is not yet implemented.") @property def cell_gradient(self): # NOQA D102 # Documentation inherited from discretize.base.BaseTensorMesh raise NotImplementedError("Cell Grad is not yet implemented.") @property def nodal_gradient(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.is_symmetric: return None if getattr(self, "_nodal_gradient", None) is None: if self.dim == 2: Gr = sp.kron( speye(self._shape_total_nodes[1]), ddx(self.shape_cells[0]) ) Gphi = sp.kron( ddx(self.shape_cells[1]), speye(self._shape_total_nodes[0]) ) Gz = None else: Gr = kron3( speye(self._shape_total_nodes[2]), speye(self._shape_total_nodes[1]), ddx(self.shape_cells[0]), ) Gphi = kron3( speye(self._shape_total_nodes[2]), ddx(self.shape_cells[1]), speye(self._shape_total_nodes[0]), ) Gz = kron3( ddx(self.shape_cells[2]), speye(self._shape_total_nodes[1]), speye(self._shape_total_nodes[0]), ) # remove the hanging edges G = sp.vstack([Gr, Gphi, Gz])[~self._ishanging_edges] # apply inflation to map true nodes to hanging nodes with the same values G = ( sdiag(1 / self.edge_lengths) @ G @ self._deflation_matrix("nodes", as_ones=True).T ) self._nodal_gradient = G return self._nodal_gradient @property def _edge_curl_stencil(self): if self.is_symmetric: nCx, nCy, nCz = self.shape_cells # 1D Difference matricies if self.includes_zero: dr = sp.diags([-1, 1], [-1, 0], shape=(nCx, nCx), format="csr") dz = sp.diags([-1, 1], [0, 1], shape=(nCz, nCz + 1), format="csr") else: dr = sp.diags([-1, 1], [0, 1], shape=(nCx, nCx + 1), format="csr") dz = sp.diags([-1, 1], [0, 1], shape=(nCz, nCz + 1), format="csr") # 2D Difference matricies Dr = sp.kron(sp.identity(nCz + 1), dr) Dz = -sp.kron(dz, sp.identity(dr.shape[-1])) return sp.vstack((Dz, Dr)) else: stencil = super()._edge_curl_stencil P_f = self._deflation_matrix("faces") P_e = self._deflation_matrix("edges", as_ones=True) return P_f @ stencil @ P_e.T @property def edge_curl(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_edge_curl", None) is None: self._edge_curl = ( sdiag(1 / self.face_areas) * self._edge_curl_stencil * sdiag(self.edge_lengths) ) return self._edge_curl @property def average_edge_x_to_cell(self): # NOQA D102 # Documentation inherited from discretize.operators.DiffOperators if self.is_symmetric: raise AttributeError("There are no x-edges on a cyl symmetric mesh") return ( kron3( av(self.shape_cells[2]), av(self.shape_cells[1]), speye(self.shape_cells[0]), ) * self._deflation_matrix("Ex", as_ones=True).T ) @property def average_edge_y_to_cell(self): # NOQA D102 # Documentation inherited from discretize.operators.DiffOperators if self.is_symmetric: if self.includes_zero: avR = av(self.shape_cells[0])[:, 1:] else: avR = av(self.shape_cells[0]) return sp.kron(av(self.shape_cells[2]), avR, format="csr") else: return ( kron3( av(self.shape_cells[2]), speye(self.shape_cells[1]), av(self.shape_cells[0]), ) * self._deflation_matrix("Ey", as_ones=True).T ) @property def average_edge_z_to_cell(self): # NOQA D102 # Documentation inherited from discretize.operators.DiffOperators if self.is_symmetric: raise AttributeError("There are no z-edges on a cyl symmetric mesh") return ( kron3( speye(self.shape_cells[2]), av(self.shape_cells[1]), av(self.shape_cells[0]), ) * self._deflation_matrix("Ez", as_ones=True).T ) @property def average_edge_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_edge_to_cell", None) is None: # The number of cell centers in each direction # n = self.vnC if self.is_symmetric: self._average_edge_to_cell = self.aveEy2CC else: self._average_edge_to_cell = ( 1.0 / self.dim * sp.hstack( (self.aveEx2CC, self.aveEy2CC, self.aveEz2CC), format="csr" ) ) return self._average_edge_to_cell @property def average_edge_to_cell_vector(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.is_symmetric: return self.average_edge_to_cell else: if getattr(self, "_average_edge_to_cell_vector", None) is None: self._average_edge_to_cell_vector = sp.block_diag( (self.aveEx2CC, self.aveEy2CC, self.aveEz2CC), format="csr" ) return self._average_edge_to_cell_vector @property def average_edge_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.is_symmetric: nx, _, nz = self.shape_edges_y if self.includes_zero: e_to_fx = sp.kron(av(nz - 1), sp.eye(nx)) e_to_fz = sp.kron(sp.eye(nz), av(nx).toarray()[:, 1:]) else: e_to_fx = sp.kron(av(nz - 1), sp.eye(nx)) e_to_fz = sp.kron(sp.eye(nz), av(nx - 1)) return sp.vstack([e_to_fx, e_to_fz]) else: Av = super().average_edge_to_face # then need to deflate it... De = self._deflation_matrix("edges", as_ones=True) Df = self._deflation_matrix("faces", as_ones=False) return Df @ Av @ De.T @property def average_face_x_to_cell(self): # NOQA D102 # Documentation inherited from discretize.operators.DiffOperators if self.includes_zero: avR = av(self.vnC[0])[:, 1:] return kron3(speye(self.vnC[2]), speye(self.vnC[1]), avR) return super().average_face_x_to_cell @property def average_face_y_to_cell(self): # NOQA D102 # Documentation inherited from discretize.operators.DiffOperators return ( super().average_face_y_to_cell * self._deflation_matrix("Fy", as_ones=True).T ) @property def average_face_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_face_to_cell", None) is None: if self.is_symmetric: self._average_face_to_cell = 0.5 * ( sp.hstack((self.aveFx2CC, self.aveFz2CC), format="csr") ) else: self._average_face_to_cell = ( 1.0 / self.dim * ( sp.hstack( (self.aveFx2CC, self.aveFy2CC, self.aveFz2CC), format="csr" ) ) ) return self._average_face_to_cell @property def average_face_to_cell_vector(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_face_to_cell_vector", None) is None: # n = self.vnC if self.is_symmetric: self._average_face_to_cell_vector = sp.block_diag( (self.aveFx2CC, self.aveFz2CC), format="csr" ) else: self._average_face_to_cell_vector = sp.block_diag( (self.aveFx2CC, self.aveFy2CC, self.aveFz2CC), format="csr" ) return self._average_face_to_cell_vector @property def _average_node_to_face_x(self): aveN2Fx = kron3( av(self.shape_cells[2]), av(self.shape_cells[1]), speye(self._shape_total_nodes[0]), ) return aveN2Fx[~self._ishanging_faces_x] @property def _average_node_to_face_y(self): aveN2Fy = kron3( av(self.shape_cells[2]), speye(self._shape_total_nodes[1]), av(self.shape_cells[0]), ) return aveN2Fy[~self._ishanging_faces_y] @property def average_node_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_face", None) is None: ave = super().average_node_to_face ave = ave @ self._deflation_matrix("nodes", as_ones=True).T self._average_node_to_face = ave return self._average_node_to_face @property def average_node_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_cell", None) is None: ave = super().average_node_to_cell ave = ave @ self._deflation_matrix("nodes", as_ones=True).T self._average_node_to_cell = ave return self._average_node_to_cell @property def average_cell_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_cell_to_face", None) is None: if self.dim == 3: nx, ny, nz = self.shape_cells if self.is_symmetric and self.includes_zero: av_c2f_r = kron3( speye(nz), speye(ny), av_extrap(nx)[1:], ) else: av_c2f_r = kron3( speye(nz), speye(ny), av_extrap(nx), )[~self._ishanging_faces_x] if not self.is_symmetric: av_c2f_t = self._deflation_matrix("faces_y") @ kron3( speye(nz), av_extrap(ny), speye(nx), ) else: av_c2f_t = None av_c2f_z = kron3( av_extrap(nz), speye(ny), speye(nx), ) self._average_cell_to_face = sp.vstack( (av_c2f_r, av_c2f_t, av_c2f_z), format="csr", ) return self._average_cell_to_face @property def project_face_to_boundary_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh P = speye(self.n_faces) return P[self._is_boundary_face] @property def project_node_to_boundary_node(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh P = speye(self.n_nodes) return P[self._is_boundary_node] @property def project_edge_to_boundary_edge(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh P = speye(self.n_edges) return P[self._is_boundary_edge] #################################################### # Deflation Matrices #################################################### def _deflation_matrix(self, location, as_ones=False): """Construct the deflation matrix. Construct the deflation matrix to remove hanging edges / faces / nodes from the operators. """ location = self._parse_location_type(location) if location not in [ "nodes", "faces", "faces_x", "faces_y", "faces_z", "edges", "edges_x", "edges_y", "edges_z", "cell_centers", ]: raise ValueError( "Location must be a grid location, not {}".format(location) ) if not (self.is_symmetric or self.is_wrapped or self.includes_zero): return Identity() if location == "cell_centers": return speye(self.n_cells) if self.is_symmetric and location == "nodes": return sp.csr_matrix((self.n_nodes, self._n_total_nodes)) elif location in ["edges", "faces"]: if self.is_symmetric: if location == "edges": return self._deflation_matrix("edges_y", as_ones=as_ones) elif location == "faces": return sp.block_diag( [ self._deflation_matrix(location + coord, as_ones=as_ones) for coord in ["_x", "_z"] ] ) return sp.block_diag( [ self._deflation_matrix(location + coord, as_ones=as_ones) for coord in ["_x", "_y", "_z"] ] ) n_total = getattr(self, "_n_total_{}".format(location)) n_items = getattr(self, "n_{}".format(location)) if n_total == n_items: return sp.eye(n_total, format="csr") is_hanging = getattr(self, "_ishanging_{}".format(location)) hanging_dict = getattr(self, "_hanging_{}".format(location)) vs = np.ones(n_total) inds = np.empty(n_total, dtype=int) inds[~is_hanging] = np.arange(n_items) for k, v in hanging_dict.items(): if v is not None: inds[k] = v else: inds[k] = 0 vs[k] = 0 R = sp.csr_matrix((vs, (inds, np.arange(n_total))), shape=(n_items, n_total)) if not as_ones: R = sdiag(1.0 / R.sum(1)) * R return R #################################################### # Interpolation #################################################### def get_interpolation_matrix( self, loc, location_type="cell_centers", zeros_outside=False, **kwargs ): r"""Construct interpolation matrix from mesh. This method allows the user to construct a sparse linear-interpolation matrix which interpolates discrete quantities from mesh centers, nodes, edges or faces to an arbitrary set of locations in 3D space. Locations are defined in cylindrical coordinates; i.e. :math:`(r, \phi, z)`. Parameters ---------- loc : (n_pts, dim) numpy.ndarray Location of points to interpolate to in cylindrical coordinates ; i.e. :math:`(r, \phi, z)` location_type : str What discrete quantity on the mesh you are interpolating from. Options are: - 'Ex', 'edges_x' -> x-component of field defined on x edges - 'Ey', 'edges_y' -> y-component of field defined on y edges - 'Ez', 'edges_z' -> z-component of field defined on z edges - 'Fx', 'faces_x' -> x-component of field defined on x faces - 'Fy', 'faces_y' -> y-component of field defined on y faces - 'Fz', 'faces_z' -> z-component of field defined on z faces - 'N', 'nodes' -> scalar field defined on nodes - 'CC', 'cell_centers' -> scalar field defined on cell centers - 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers - 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers - 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers zeros_outside : bool If *False* , nearest neighbour is used to compute the value for locations outside the mesh. If *True* , values outside the mesh will be equal to zero. Returns ------- (n_pts, n_loc_type) scipy.sparse.csr_matrix The interpolation matrix """ if "locType" in kwargs: raise TypeError( "The locType keyword argument has been removed, please use location_type. " "This will be removed in discretize 1.0.0" ) if "zerosOutside" in kwargs: raise TypeError( "The zerosOutside keyword argument has been removed, please use zeros_outside. " "This will be removed in discretize 1.0.0" ) location_type = self._parse_location_type(location_type) if self.is_symmetric and location_type in ["edges_x", "edges_z", "faces_y"]: raise ValueError( "Symmetric CylindricalMesh does not support {0!s} interpolation, " "as this variable does not exist.".format(location_type) ) loc = as_array_n_by_dim(loc, self.dim).copy() loc[:, 1] = loc[:, 1] % (2 * np.pi) if location_type in ["cell_centers_x", "cell_centers_y", "cell_centers_z"]: Q = interpolation_matrix(loc, *self.get_tensor("cell_centers")) Z = spzeros(loc.shape[0], self.nC) if location_type[-1] == "x": Q = sp.hstack([Q, Z]) elif location_type[-1] == "y": Q = sp.hstack([Q]) elif location_type[-1] == "z": Q = sp.hstack([Z, Q]) Q = Q.tocsr() elif location_type == "nodes": rtz = [self.nodes_x, self._nodes_y_full] if self.dim == 3: rtz.append(self.nodes_z) Q = interpolation_matrix(loc, *rtz) Q = Q @ self._deflation_matrix("nodes", as_ones=True).T elif location_type == "cell_centers": rtz = [ self.cell_centers_x, ] # theta wrap around interpolation if self.is_wrapped: rtz.append( np.r_[ self.cell_centers_y[-1] - 2 * np.pi, self.cell_centers_y, self.cell_centers_y[0] + 2 * np.pi, ] ) else: rtz.append(self.cell_centers_y) if self.dim == 3: rtz.append(self.cell_centers_z) Q = interpolation_matrix(loc, *rtz) if self.is_wrapped: irs, its, izs = np.unravel_index( Q.indices, self.shape_cells + np.r_[0, 2, 0], order="F" ) its = (its - 1) % self.shape_cells[1] new_indices = np.ravel_multi_index( (irs, its, izs), self.shape_cells, order="F" ) Q = sp.csr_matrix( (Q.data, new_indices, Q.indptr), shape=(Q.shape[0], self.n_cells) ) elif location_type in [ "edges_x", "edges_y", "edges_z", "faces_x", "faces_y", "faces_z", ]: ind = {"x": 0, "y": 1, "z": 2}[location_type[-1]] if self.dim < ind: raise ValueError("mesh is not high enough dimension.") if "f" in location_type.lower(): items = (self.nFx, self.nFy, self.nFz)[: self.dim] else: items = (self.nEx, self.nEy, self.nEz)[: self.dim] components = [spzeros(loc.shape[0], n) for n in items] if location_type == "faces_x": if self.includes_zero and not self.is_symmetric: nodes_x = self.nodes_x[1:] else: nodes_x = self.nodes_x rtz = [ nodes_x, ] # theta wrap around interpolation if self.is_wrapped: rtz.append( np.r_[ self.cell_centers_y[-1] - 2 * np.pi, self.cell_centers_y, self.cell_centers_y[0] + 2 * np.pi, ], ) else: rtz.append(self.cell_centers_y) if self.dim == 3: rtz.append(self.cell_centers_z) Q = interpolation_matrix(loc, *rtz) # unwrap the theta indices if self.is_wrapped: irs, its, izs = np.unravel_index( Q.indices, self.shape_faces_x + np.r_[0, 2, 0], order="F" ) its = (its - 1) % self.shape_faces_x[1] new_indices = np.ravel_multi_index( (irs, its, izs), self.shape_faces_x, order="F" ) Q = sp.csr_matrix( (Q.data, new_indices, Q.indptr), shape=(Q.shape[0], self.n_faces_x), ) components[0] = Q elif location_type == "faces_y": rtz = [ self.cell_centers_x, self._nodes_y_full, ] if self.dim == 3: rtz.append(self.cell_centers_z) Q = interpolation_matrix(loc, *rtz) Q = Q @ self._deflation_matrix("faces_y", as_ones=True).T components[1] = Q elif location_type == "faces_z": rtz = [ self.cell_centers_x, ] # theta wrap around interpolation if self.is_wrapped: rtz.append( np.r_[ self.cell_centers_y[-1] - 2 * np.pi, self.cell_centers_y, self.cell_centers_y[0] + 2 * np.pi, ], ) else: rtz.append(self.cell_centers_y) rtz.append(self.nodes_z) Q = interpolation_matrix(loc, *rtz) if self.is_wrapped: # unwrap the theta indices irs, its, izs = np.unravel_index( Q.indices, self.shape_faces_z + np.r_[0, 2, 0], order="F" ) its = (its - 1) % self.shape_faces_z[1] new_indices = np.ravel_multi_index( (irs, its, izs), self.shape_faces_z, order="F" ) Q = sp.csr_matrix( (Q.data, new_indices, Q.indptr), shape=(Q.shape[0], self.n_faces_z), ) components[2] = Q elif location_type == "edges_x": rtz = [ self.cell_centers_x, self._nodes_y_full, ] if self.dim == 3: rtz.append(self.nodes_z) Q = interpolation_matrix(loc, *rtz) Q = Q @ self._deflation_matrix("edges_x", as_ones=True).T components[0] = Q elif location_type == "edges_y": # theta wrap around if self.is_symmetric or not self.includes_zero: nodes_x = self.nodes_x else: nodes_x = self.nodes_x[1:] rtz = [nodes_x] if self.is_wrapped: rtz.append( np.r_[ self.cell_centers_y[-1] - 2 * np.pi, self.cell_centers_y, self.cell_centers_y[0] + 2 * np.pi, ], ) else: rtz.append(self.cell_centers_y) if self.dim == 3: rtz.append(self.nodes_z) Q = interpolation_matrix(loc, *rtz) if self.is_wrapped: irs, its, izs = np.unravel_index( Q.indices, self.shape_edges_y + np.r_[0, 2, 0], order="F" ) its = (its - 1) % self.shape_edges_y[1] new_indices = np.ravel_multi_index( (irs, its, izs), self.shape_edges_y, order="F" ) Q = sp.csr_matrix( (Q.data, new_indices, Q.indptr), shape=(Q.shape[0], self.n_edges_y), ) components[1] = Q elif location_type == "edges_z": rtz = [self.nodes_x, self._nodes_y_full, self.cell_centers_z] Q = interpolation_matrix(loc, *rtz) Q = Q @ self._deflation_matrix("edges_z", as_ones=True).T components[2] = Q # remove any zero blocks (hstack complains) Q = sp.hstack([comp for comp in components if comp.shape[1] > 0]) else: raise ValueError("Unrecognized location type") if zeros_outside: Q[~self.is_inside(loc), :] = 0 return Q def cartesian_grid(self, location_type="cell_centers", theta_shift=None, **kwargs): """Return the specified grid in cartesian coordinates. Takes a grid location ('CC', 'N', 'Ex', 'Ey', 'Ez', 'Fx', 'Fy', 'Fz') and returns that grid in cartesian coordinates Parameters ---------- location_type : {'CC', 'N', 'Ex', 'Ey', 'Ez', 'Fx', 'Fy', 'Fz'} grid location theta_shift : float, optional shift for theta Returns ------- (n_items, dim) numpy.ndarray cartesian coordinates for the cylindrical grid """ if "locType" in kwargs: raise TypeError( "The locType keyword argument has been removed, please use location_type. " "This will be removed in discretize 1.0.0" ) try: grid = getattr(self, location_type).copy() except AttributeError: grid = getattr(self, f"grid{location_type}").copy() if theta_shift is not None: grid[:, 1] = grid[:, 1] - theta_shift return cyl2cart(grid) # TODO: account for cartesian origin def get_interpolation_matrix_cartesian_mesh( self, Mrect, location_type="cell_centers", location_type_to=None, **kwargs ): """Construct projection matrix from ``CylindricalMesh`` to other mesh. This method is used to construct a sparse linear interpolation matrix from gridded locations on the cylindrical mesh to gridded locations on a different mesh type. That is, an interpolation from the centers, nodes, faces or edges of the cylindrical mesh to the centers, nodes, faces or edges of another mesh. This method is generally used to interpolate from cylindrical meshes to meshes defined in Cartesian coordinates; e.g. :class:`~discretize.TensorMesh`, :class:`~discretize.TreeMesh` or :class:`~discretize.CurvilinearMesh`. Parameters ---------- Mrect : discretize.base.BaseMesh the mesh we are interpolating onto location_type : {'CC', 'N', 'Ex', 'Ey', 'Ez', 'Fx', 'Fy', 'Fz'} gridded locations of the cylindrical mesh. location_type_to : {None, 'CC', 'N', 'Ex', 'Ey', 'Ez', 'Fx', 'Fy', 'Fz'} gridded locations being interpolated to on the other mesh. If *None*, this method will use the same type as *location_type*. Returns ------- scipy.sparse.csr_matrix interpolation matrix from gridded locations on cylindrical mesh to gridded locations on another mesh """ if "locType" in kwargs: raise TypeError( "The locType keyword argument has been removed, please use location_type. " "This will be removed in discretize 1.0.0" ) if "locTypeTo" in kwargs: raise TypeError( "The locTypeTo keyword argument has been removed, please use location_type_to. " "This will be removed in discretize 1.0.0" ) location_type = self._parse_location_type(location_type) if not self.is_symmetric: raise NotImplementedError( "Currently we have not taken into account other projections " "for more complicated CylindricalMeshes" ) if location_type_to is None: location_type_to = location_type location_type_to = self._parse_location_type(location_type_to) if location_type == "faces": # do this three times for each component X = self.get_interpolation_matrix_cartesian_mesh( Mrect, location_type="faces_x", location_type_to=location_type_to + "_x" ) Y = self.get_interpolation_matrix_cartesian_mesh( Mrect, location_type="faces_y", location_type_to=location_type_to + "_y" ) Z = self.get_interpolation_matrix_cartesian_mesh( Mrect, location_type="faces_z", location_type_to=location_type_to + "_z" ) return sp.vstack((X, Y, Z)) if location_type == "edges": X = self.get_interpolation_matrix_cartesian_mesh( Mrect, location_type="edges_x", location_type_to=location_type_to + "_x" ) Y = self.get_interpolation_matrix_cartesian_mesh( Mrect, location_type="edges_y", location_type_to=location_type_to + "_y" ) Z = spzeros(getattr(Mrect, "n_" + location_type_to + "_z"), self.n_edges) return sp.vstack((X, Y, Z)) grid = getattr(Mrect, location_type_to) # This is unit circle stuff, 0 to 2*pi, starting at x-axis, rotating # counter clockwise in an x-y slice theta = ( -np.arctan2( grid[:, 0] - self.cartesian_origin[0], grid[:, 1] - self.cartesian_origin[1], ) + np.pi / 2 ) theta[theta < 0] += np.pi * 2.0 r = ( (grid[:, 0] - self.cartesian_origin[0]) ** 2 + (grid[:, 1] - self.cartesian_origin[1]) ** 2 ) ** 0.5 if location_type in ["cell_centers", "nodes", "faces_z", "edges_z"]: G, proj = np.c_[r, theta, grid[:, 2]], np.ones(r.size) else: dotMe = { "faces_x": Mrect.face_normals[: Mrect.nFx, :], "faces_y": Mrect.face_normals[Mrect.nFx : (Mrect.nFx + Mrect.nFy), :], "faces_z": Mrect.face_normals[-Mrect.nFz :, :], "edges_x": Mrect.edge_tangents[: Mrect.nEx, :], "edges_y": Mrect.edge_tangents[Mrect.nEx : (Mrect.nEx + Mrect.nEy), :], "edges_z": Mrect.edge_tangents[-Mrect.nEz :, :], }[location_type_to] if "faces" in location_type: normals = np.c_[np.cos(theta), np.sin(theta), np.zeros(theta.size)] proj = (normals * dotMe).sum(axis=1) elif "edges" in location_type: tangents = np.c_[-np.sin(theta), np.cos(theta), np.zeros(theta.size)] proj = (tangents * dotMe).sum(axis=1) G = np.c_[r, theta, grid[:, 2]] interp_type = location_type if interp_type == "faces_y": interp_type = "faces_x" elif interp_type == "edges_x": interp_type = "edges_y" Pc2r = self.get_interpolation_matrix(G, interp_type) Proj = sdiag(proj) return Proj * Pc2r # DEPRECATIONS areaFx = deprecate_property( "face_x_areas", "areaFx", removal_version="1.0.0", error=True ) areaFy = deprecate_property( "face_y_areas", "areaFy", removal_version="1.0.0", error=True ) areaFz = deprecate_property( "face_z_areas", "areaFz", removal_version="1.0.0", error=True ) edgeEx = deprecate_property( "edge_x_lengths", "edgeEx", removal_version="1.0.0", error=True ) edgeEy = deprecate_property( "edge_y_lengths", "edgeEy", removal_version="1.0.0", error=True ) edgeEz = deprecate_property( "edge_z_lengths", "edgeEz", removal_version="1.0.0", error=True ) isSymmetric = deprecate_property( "is_symmetric", "isSymmetric", removal_version="1.0.0", error=True ) cartesianOrigin = deprecate_property( "cartesian_origin", "cartesianOrigin", removal_version="1.0.0", error=True ) getInterpolationMatCartMesh = deprecate_method( "get_interpolation_matrix_cartesian_mesh", "getInterpolationMatCartMesh", removal_version="1.0.0", error=True, ) cartesianGrid = deprecate_method( "cartesian_grid", "cartesianGrid", removal_version="1.0.0", error=True ) @deprecate_class(removal_version="1.0.0", error=True) class CylMesh(CylindricalMesh): """Deprecated calling of `discretize.CylindricalMesh`.""" pass ================================================ FILE: discretize/meson.build ================================================ python_sources = [ '__init__.py', 'curvilinear_mesh.py', 'cylindrical_mesh.py', 'tensor_cell.py', 'tensor_mesh.py', 'tests.py', 'tree_mesh.py', 'unstructured_mesh.py', 'View.py', ] py.install_sources( python_sources, subdir: 'discretize' ) subdir('base') subdir('_extensions') subdir('mixins') subdir('operators') subdir('utils') subdir('Tests') ================================================ FILE: discretize/mixins/__init__.py ================================================ """ ================================== Mixins (:mod:`discretize.mixins`) ================================== .. currentmodule:: discretize.mixins The ``mixins`` module provides a set of tools for interfacing ``discretize`` with external libraries such as VTK, OMF, and matplotlib. These modules are only imported if those external packages are available in the active Python environment and provide extra functionality that different finite volume meshes can inherit. Mixin Classes ------------- .. autosummary:: :toctree: generated/ TensorMeshIO TreeMeshIO InterfaceMPL InterfaceVTK InterfaceOMF Other Optional Classes ---------------------- .. autosummary:: :toctree: generated/ Slicer """ import importlib.util from .mesh_io import TensorMeshIO, TreeMeshIO, SimplexMeshIO AVAILABLE_MIXIN_CLASSES = [] SIMPLEX_MIXIN_CLASSES = [] if importlib.util.find_spec("vtk"): from .vtk_mod import InterfaceVTK AVAILABLE_MIXIN_CLASSES.append(InterfaceVTK) if importlib.util.find_spec("omf"): from .omf_mod import InterfaceOMF AVAILABLE_MIXIN_CLASSES.append(InterfaceOMF) # keep this one last in defaults in case anything else wants to overwrite # plot commands if importlib.util.find_spec("matplotlib"): from .mpl_mod import Slicer, InterfaceMPL AVAILABLE_MIXIN_CLASSES.append(InterfaceMPL) # # Python 3 friendly class InterfaceMixins(*AVAILABLE_MIXIN_CLASSES): """Class to handle all the avaialble mixins that can be inherrited directly onto ``discretize.base.BaseMesh`` """ pass ================================================ FILE: discretize/mixins/mesh_io.py ================================================ """Module for reading and writing meshes to text files. The text files representing meshes are often in the `UBC` format. """ import os import numpy as np from discretize.utils import mkvc from discretize.utils.code_utils import deprecate_method try: from discretize.mixins.vtk_mod import ( InterfaceTensorread_vtk, InterfaceSimplexReadVTK, ) except ImportError: InterfaceSimplexReadVTK = InterfaceTensorread_vtk = object class TensorMeshIO(InterfaceTensorread_vtk): """Class for managing the input/output of tensor meshes and models. The ``TensorMeshIO`` class contains a set of class methods specifically for the :class:`~discretize.TensorMesh` class. These include: - Read/write tensor meshes to file - Read/write models defined on tensor meshes """ @classmethod def _readUBC_3DMesh(cls, file_name): """Read 3D tensor mesh from UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted mesh file Returns ------- discretize.TensorMesh The tensor mesh """ # Read the file as line strings, remove lines with comment = ! msh = np.genfromtxt(file_name, delimiter="\n", dtype=str, comments="!") # Interal function to read cell size lines for the UBC mesh files. def readCellLine(line): line_list = [] for seg in line.split(): if "*" in seg: sp = seg.split("*") seg_arr = np.ones((int(sp[0]),)) * float(sp[1]) else: seg_arr = np.array([float(seg)], float) line_list.append(seg_arr) return np.concatenate(line_list) # Fist line is the size of the model # sizeM = np.array(msh[0].split(), dtype=float) # Second line is the South-West-Top corner coordinates. origin = np.array(msh[1].split(), dtype=float) # Read the cell sizes h1 = readCellLine(msh[2]) h2 = readCellLine(msh[3]) h3temp = readCellLine(msh[4]) # Invert the indexing of the vector to start from the bottom. h3 = h3temp[::-1] # Adjust the reference point to the bottom south west corner origin[2] = origin[2] - np.sum(h3) # Make the mesh tensMsh = cls([h1, h2, h3], origin=origin) return tensMsh @classmethod def _readUBC_2DMesh(cls, file_name): """Read 2D tensor mesh from UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted mesh file Returns ------- discretize.TensorMesh The tensor mesh """ fopen = open(file_name, "r") # Read down the file and unpack dx vector def unpackdx(fid, nrows): for ii in range(nrows): line = fid.readline() var = np.array(line.split(), dtype=float) if ii == 0: x0 = var[0] xvec = np.ones(int(var[2])) * (var[1] - var[0]) / int(var[2]) xend = var[1] else: xvec = np.hstack( (xvec, np.ones(int(var[1])) * (var[0] - xend) / int(var[1])) ) xend = var[0] return x0, xvec # Start with dx block # First line specifies the number of rows for x-cells line = fopen.readline() # Strip comments lines while line.startswith("!"): line = fopen.readline() nl = np.array(line.split(), dtype=int) [x0, dx] = unpackdx(fopen, nl[0]) # Move down the file until reaching the z-block line = fopen.readline() if not line: line = fopen.readline() # End with dz block # First line specifies the number of rows for z-cells line = fopen.readline() nl = np.array(line.split(), dtype=int) [z0, dz] = unpackdx(fopen, nl[0]) # Flip z0 to be the bottom of the mesh for SimPEG z0 = -(z0 + sum(dz)) dz = dz[::-1] # Make the mesh tensMsh = cls([dx, dz], origin=(x0, z0)) fopen.close() return tensMsh @classmethod def read_UBC(cls, file_name, directory=None): """Read 2D or 3D tensor mesh from UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted mesh file or just its name if directory is specified directory : str, optional directory where the UBC-GIF file lives Returns ------- discretize.TensorMesh The tensor mesh """ # Check the expected mesh dimensions if directory is None: directory = "" fname = os.path.join(directory, file_name) # Read the file as line strings, remove lines with comment = ! msh = np.genfromtxt(fname, delimiter="\n", dtype=str, comments="!", max_rows=1) # Fist line is the size of the model sizeM = np.array(msh.ravel()[0].split(), dtype=float) # Check if the mesh is a UBC 2D mesh if sizeM.shape[0] == 1: Tnsmsh = cls._readUBC_2DMesh(fname) # Check if the mesh is a UBC 3D mesh elif sizeM.shape[0] == 3: Tnsmsh = cls._readUBC_3DMesh(fname) else: raise Exception("File format not recognized") return Tnsmsh def _readModelUBC_2D(mesh, file_name): """Read UBC-GIF formatted model file for 2D tensor mesh. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted model file Returns ------- (n_cells) numpy.ndarray The model defined on the 2D tensor mesh """ # Open file and skip header... assume that we know the mesh already obsfile = np.genfromtxt(file_name, delimiter=" \n", dtype=str, comments="!") dim = tuple(np.array(obsfile[0].split(), dtype=int)) if mesh.shape_cells != dim: raise Exception("Dimension of the model and mesh mismatch") model = [] for line in obsfile[1:]: model.extend([float(val) for val in line.split()]) model = np.asarray(model) if not len(model) == mesh.nC: raise Exception( """Something is not right, expected size is {:d} but unwrap vector is size {:d}""".format( mesh.nC, len(model) ) ) return model.reshape(mesh.vnC, order="F")[:, ::-1].reshape(-1, order="F") def _readModelUBC_3D(mesh, file_name): """Read UBC-GIF formatted model file for 3D tensor mesh. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted model file Returns ------- (n_cells) numpy.ndarray The model defined on the 3D tensor mesh """ f = open(file_name, "r") model = np.array(list(map(float, f.readlines()))) f.close() nCx, nCy, nCz = mesh.shape_cells model = np.reshape(model, (nCz, nCx, nCy), order="F") model = model[::-1, :, :] model = np.transpose(model, (1, 2, 0)) model = mkvc(model) return model def read_model_UBC(mesh, file_name, directory=None): """Read UBC-GIF formatted model file for 2D or 3D tensor mesh. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted model file or just its name if directory is specified directory : str, optional directory where the UBC-GIF file lives Returns ------- (n_cells) numpy.ndarray The model defined on the mesh """ if directory is None: directory = "" fname = os.path.join(directory, file_name) if mesh.dim == 3: model = mesh._readModelUBC_3D(fname) elif mesh.dim == 2: model = mesh._readModelUBC_2D(fname) else: raise Exception("mesh must be a Tensor Mesh 2D or 3D") return model def write_model_UBC(mesh, file_name, model, directory=None): """Write 2D or 3D tensor model to UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path for the output mesh file or just its name if directory is specified model : (n_cells) numpy.ndarray The model to write out. directory : str, optional output directory """ if directory is None: directory = "" fname = os.path.join(directory, file_name) if mesh.dim == 3: # Reshape model to a matrix modelMat = mesh.reshape(model, "CC", "CC", "M") # Transpose the axes modelMatT = modelMat.transpose((2, 0, 1)) # Flip z to positive down modelMatTR = mkvc(modelMatT[::-1, :, :]) np.savetxt(fname, modelMatTR.ravel()) elif mesh.dim == 2: modelMat = mesh.reshape(model, "CC", "CC", "M").T[::-1] f = open(fname, "w") f.write("{:d} {:d}\n".format(*mesh.shape_cells)) f.close() f = open(fname, "ab") np.savetxt(f, modelMat) f.close() else: raise Exception("mesh must be a Tensor Mesh 2D or 3D") def _writeUBC_3DMesh(mesh, file_name, comment_lines=""): """Write 3D tensor mesh to UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path for the output mesh file comment_lines : str, optional comment lines preceded are preceeded with '!' """ if not mesh.dim == 3: raise Exception("Mesh must be 3D") s = comment_lines s += "{0:d} {1:d} {2:d}\n".format(*tuple(mesh.vnC)) # Have to it in the same operation or use mesh.origin.copy(), # otherwise the mesh.origin is updated. origin = mesh.origin + np.array([0, 0, mesh.h[2].sum()]) nCx, nCy, nCz = mesh.shape_cells s += "{0:.6f} {1:.6f} {2:.6f}\n".format(*tuple(origin)) s += ("%.6f " * nCx + "\n") % tuple(mesh.h[0]) s += ("%.6f " * nCy + "\n") % tuple(mesh.h[1]) s += ("%.6f " * nCz + "\n") % tuple(mesh.h[2][::-1]) f = open(file_name, "w") f.write(s) f.close() def _writeUBC_2DMesh(mesh, file_name, comment_lines=""): """Write 2D tensor mesh to UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path for the output mesh file comment_lines : str, optional comment lines preceded are preceeded with '!' """ if not mesh.dim == 2: raise Exception("Mesh must be 2D") def writeF(fx, outStr=""): # Init i = 0 origin = True x0 = fx[i] f = fx[i] number_segment = 0 auxStr = "" while True: i = i + 1 if i >= fx.size: break dx = -f + fx[i] f = fx[i] n = 1 for j in range(i + 1, fx.size): if -f + fx[j] == dx: n += 1 i += 1 f = fx[j] else: break number_segment += 1 if origin: auxStr += "{:.10f} {:.10f} {:d} \n".format(x0, f, n) origin = False else: auxStr += "{:.10f} {:d} \n".format(f, n) auxStr = "{:d}\n".format(number_segment) + auxStr outStr += auxStr return outStr # Grab face coordinates fx = mesh.nodes_x fz = -mesh.nodes_y[::-1] # Create the string outStr = comment_lines outStr = writeF(fx, outStr=outStr) outStr += "\n" outStr = writeF(fz, outStr=outStr) # Write file f = open(file_name, "w") f.write(outStr) f.close() def write_UBC(mesh, file_name, models=None, directory=None, comment_lines=""): """Write 2D or 3D tensor mesh (and models) to UBC-GIF formatted file(s). Parameters ---------- file_name : str or file name full path for the output mesh file or just its name if directory is specified models : dict of [str, (n_cells) numpy.ndarray], optional The dictionary key is a string representing the model's name. Each model is an (n_cells) array. directory : str, optional output directory comment_lines : str, optional comment lines preceded are preceeded with '!' """ if directory is None: directory = "" fname = os.path.join(directory, file_name) if mesh.dim == 3: mesh._writeUBC_3DMesh(fname, comment_lines=comment_lines) elif mesh.dim == 2: mesh._writeUBC_2DMesh(fname, comment_lines=comment_lines) else: raise Exception("mesh must be a Tensor Mesh 2D or 3D") if models is None: return if not isinstance(models, dict): raise TypeError("models must be a dict") for key in models: if not isinstance(key, str): raise TypeError( "The dict key must be a string representing the file name" ) mesh.write_model_UBC(key, models[key], directory=directory) # DEPRECATED @classmethod def readUBC(TensorMesh, file_name, directory=""): """Read 2D or 3D tensor mesh from UBC-GIF formatted file. *readUBC* has been deprecated and replaced by *read_UBC* See Also -------- read_UBC """ raise NotImplementedError( "TensorMesh.readUBC has been removed and this be removed in" "discretize 1.0.0. please use TensorMesh.read_UBC", ) readModelUBC = deprecate_method( "read_model_UBC", "readModelUBC", removal_version="1.0.0", error=True ) writeUBC = deprecate_method( "write_UBC", "writeUBC", removal_version="1.0.0", error=True ) writeModelUBC = deprecate_method( "write_model_UBC", "writeModelUBC", removal_version="1.0.0", error=True ) class TreeMeshIO(object): """Class for managing the input/output of tree meshes and models. The ``TreeMeshIO`` class contains a set of class methods specifically for the :class:`~discretize.TreeMesh` class. These include: - Read/write tree meshes to file - Read/write models defined on tree meshes """ @classmethod def read_UBC(TreeMesh, file_name, directory=None): """Read 3D tree mesh (OcTree mesh) from UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted mesh file or just its name if directory is specified directory : str, optional directory where the UBC-GIF file lives Returns ------- discretize.TreeMesh The tree mesh """ if directory is None: directory = "" fname = os.path.join(directory, file_name) fileLines = np.genfromtxt(fname, dtype=str, delimiter="\n", comments="!") nCunderMesh = np.array(fileLines[0].split("!")[0].split(), dtype=int) tswCorn = np.array(fileLines[1].split("!")[0].split(), dtype=float) smallCell = np.array(fileLines[2].split("!")[0].split(), dtype=float) # Read the index array indArr = np.genfromtxt( (line.encode("utf8") for line in fileLines[4::]), dtype=np.int64 ) nCunderMesh = nCunderMesh[: len(tswCorn)] # remove information related to core hs = [np.ones(nr) * sz for nr, sz in zip(nCunderMesh, smallCell)] origin = tswCorn origin[-1] -= np.sum(hs[-1]) ls = np.log2(nCunderMesh).astype(int) # if all ls are equal if min(ls) == max(ls): max_level = ls[0] else: max_level = min(ls) + 1 mesh = TreeMesh(hs, origin=origin, diagonal_balance=False) levels = indArr[:, -1] indArr = indArr[:, :-1] indArr -= 1 # shift by 1.... indArr = 2 * indArr + levels[:, None] # get cell center index indArr[:, -1] = 2 * nCunderMesh[-1] - indArr[:, -1] # switch direction of iz levels = max_level - np.log2(levels) # calculate level mesh.__setstate__((indArr, levels)) return mesh def read_model_UBC(mesh, file_name, directory=None): """Read UBC-GIF formatted file model file for 3D tree mesh (OcTree). Parameters ---------- file_name : str or list of str full path to the UBC-GIF formatted model file or just its name if directory is specified. It can also be a list of file_names. directory : str directory where the UBC-GIF file(s) lives (optional) Returns ------- (n_cells) numpy.ndarray or dict of [str, (n_cells) numpy.ndarray] The model defined on the mesh. If **file_name** is a ``dict``, it is a dictionary of models indexed by the file names. """ if directory is None: directory = "" if type(file_name) is list: out = {} for f in file_name: out[f] = mesh.read_model_UBC(f, directory=directory) return out modArr = np.loadtxt(file_name) ubc_order = mesh._ubc_order # order_ubc will re-order from treemesh ordering to UBC ordering # need the opposite operation un_order = np.empty_like(ubc_order) un_order[ubc_order] = np.arange(len(ubc_order)) model = modArr[un_order].copy() # ensure a contiguous array return model def write_UBC(mesh, file_name, models=None, directory=None): """Write OcTree mesh (and models) to UBC-GIF formatted files. Parameters ---------- file_name : str full path for the output mesh file or just its name if directory is specified models : dict of [str, (n_cells) numpy.ndarray], optional The dictionary key is a string representing the model's name. Each model is a 1D numpy array of size (n_cells). directory : str, optional output directory (optional) """ if directory is None: directory = "" uniform_hs = np.array([np.allclose(h, h[0]) for h in mesh.h]) if np.any(~uniform_hs): raise Exception("UBC form does not support variable cell widths") nCunderMesh = np.array([h.size for h in mesh.h], dtype=np.int64) tswCorn = mesh.origin.copy() tswCorn[-1] += np.sum(mesh.h[-1]) smallCell = np.array([h[0] for h in mesh.h]) nrCells = mesh.nC indArr, levels = mesh._ubc_indArr ubc_order = mesh._ubc_order indArr = indArr[ubc_order] levels = levels[ubc_order] # Write the UBC octree mesh file head = " ".join([f"{int(n)}" for n in nCunderMesh]) + " \n" head += " ".join([f"{v:.4f}" for v in tswCorn]) + " \n" head += " ".join([f"{v:.3f}" for v in smallCell]) + " \n" head += f"{int(nrCells)}" np.savetxt(file_name, np.c_[indArr, levels], fmt="%i", header=head, comments="") # Print the models if models is None: return if not isinstance(models, dict): raise TypeError("models must be a dict") for key in models: mesh.write_model_UBC(key, models[key], directory=directory) def write_model_UBC(mesh, file_name, model, directory=None): """Write 3D tree model (OcTree) to UBC-GIF formatted file. Parameters ---------- file_name : str full path for the output mesh file or just its name if directory is specified model : (n_cells) numpy.ndarray model values defined for each cell directory : str output directory (optional) """ if directory is None: directory = "" if type(file_name) is list: for f, m in zip(file_name, model): mesh.write_model_UBC(f, m) else: ubc_order = mesh._ubc_order fname = os.path.join(directory, file_name) m = model[ubc_order] np.savetxt(fname, m) # DEPRECATED @classmethod def readUBC(TreeMesh, file_name, directory=""): """Read 3D Tree mesh from UBC-GIF formatted file. *readUBC* has been deprecated and replaced by *read_UBC* See Also -------- read_UBC """ raise NotImplementedError( "TensorMesh.readUBC has been removed and this be removed in" "discretize 1.0.0. please use TensorMesh.read_UBC", ) readModelUBC = deprecate_method( "read_model_UBC", "readModelUBC", removal_version="1.0.0", error=True ) writeUBC = deprecate_method( "write_UBC", "writeUBC", removal_version="1.0.0", error=True ) writeModelUBC = deprecate_method( "write_model_UBC", "writeModelUBC", removal_version="1.0.0", error=True ) class SimplexMeshIO(InterfaceSimplexReadVTK): """Empty class for future text based IO of a SimplexMesh.""" pass ================================================ FILE: discretize/mixins/meson.build ================================================ python_sources = [ '__init__.py', 'mesh_io.py', 'mpl_mod.py', 'omf_mod.py', 'vtk_mod.py', ] py.install_sources( python_sources, subdir: 'discretize/mixins' ) ================================================ FILE: discretize/mixins/mpl_mod.py ================================================ """Module for ``matplotlib`` interaction with ``discretize``.""" import numpy as np import warnings from discretize.utils import mkvc, ndgrid from discretize.utils.code_utils import deprecate_method import discretize def load_matplotlib(): """Lazy load principal matplotlib routines. This is not beautiful. But if matplotlib is installed, but never used, it reduces load time significantly. """ import matplotlib import matplotlib.pyplot as plt return matplotlib, plt class InterfaceMPL(object): """Class for plotting ``discretize`` meshes with matplotlib. This interface adds three plotting methods to all ``discretize`` meshes. :py:attr:`~InterfaceMPL.plot_grid` will plot gridded points for 2D and 3D meshes. :py:attr:`~InterfaceMPL.plot_image` is used for plotting models, scalars and vectors defined on a given mesh. And :py:attr:`~InterfaceMPL.plot_slice` is used for plotting models, scalars and vectors on a 2D slice through a 3D mesh. """ def plot_grid( self, ax=None, nodes=False, faces=False, centers=False, edges=False, lines=True, show_it=False, **kwargs, ): """Plot the grid for nodal, cell-centered and staggered grids. For 2D and 3D meshes, this method plots the mesh grid. Additionally, the user can choose to denote edge, face, node and cell center locations. This function is built upon the ``matplotlib.pyplot.plot`` function and will accept associated keyword arguments. Parameters ---------- ax : matplotlib.axes.Axes or None, optional The axes to draw on. *None* produces a new axes. nodes, faces, centers, edges, lines : bool, optional Whether to plot the corresponding item show_it : bool, optional whether to call plt.show() color : Color or str, optional If lines=True, defines the color of the grid lines. linewidth : float, optional If lines=True, defines the thickness of the grid lines. Returns ------- matplotlib.axes.Axes Axes handle for the plot Other Parameters ---------------- edges_x, edges_y, edges_z, faces_x, faces_y, faces_z : bool, optional When plotting a ``TreeMesh``, these are also options to plot the individual component items. cell_line : bool, optional When plotting a ``TreeMesh``, you can also plot a line through the cell centers in order. slice : {'both', 'theta', 'z'} When plotting a ``CylindricalMesh``, which dimension to slice over. Notes ----- Excess arguments are passed on to `plot` Examples -------- Plotting a 2D TensorMesh grid >>> from matplotlib import pyplot as plt >>> import discretize >>> import numpy as np >>> h1 = np.linspace(.1, .5, 3) >>> h2 = np.linspace(.1, .5, 5) >>> mesh = discretize.TensorMesh([h1, h2]) >>> mesh.plot_grid(nodes=True, faces=True, centers=True, lines=True) >>> plt.show() Plotting a 3D TensorMesh grid >>> from matplotlib import pyplot as plt >>> import discretize >>> import numpy as np >>> h1 = np.linspace(.1, .5, 3) >>> h2 = np.linspace(.1, .5, 5) >>> h3 = np.linspace(.1, .5, 3) >>> mesh = discretize.TensorMesh([h1, h2, h3]) >>> mesh.plot_grid(nodes=True, faces=True, centers=True, lines=True) >>> plt.show() Plotting a 2D CurvilinearMesh >>> from matplotlib import pyplot as plt >>> import discretize >>> X, Y = discretize.utils.example_curvilinear_grid([10, 10], 'rotate') >>> M = discretize.CurvilinearMesh([X, Y]) >>> M.plot_grid() >>> plt.show() Plotting a 3D CurvilinearMesh >>> from matplotlib import pyplot as plt >>> import discretize >>> X, Y, Z = discretize.utils.example_curvilinear_grid([5, 5, 5], 'rotate') >>> M = discretize.CurvilinearMesh([X, Y, Z]) >>> M.plot_grid() >>> plt.show() Plotting a 2D TreeMesh >>> from matplotlib import pyplot as plt >>> import discretize >>> M = discretize.TreeMesh([32, 32]) >>> M.insert_cells([[0.25, 0.25]], [4]) >>> M.plot_grid() >>> plt.show() Plotting a 3D TreeMesh >>> from matplotlib import pyplot as plt >>> import discretize >>> M = discretize.TreeMesh([32, 32, 32]) >>> M.insert_cells([[0.3, 0.75, 0.22]], [4]) >>> M.plot_grid() >>> plt.show() """ matplotlib, plt = load_matplotlib() from matplotlib import rc_params # lazy loaded mesh_type = self._meshType.lower() plotters = { "tree": self.__plot_grid_tree, "tensor": self.__plot_grid_tensor, "curv": self.__plot_grid_curv, "cyl": self.__plot_grid_cyl, "simplex": self.__plot_grid_simp, } try: plotter = plotters[mesh_type] except KeyError: raise NotImplementedError( "Mesh type `{}` does not have a plot_grid implementation.".format( type(self).__name__ ) ) if "showIt" in kwargs: show_it = kwargs.pop("showIt") warnings.warn( "showIt has been deprecated, please use show_it", FutureWarning, stacklevel=2, ) if ax is not None: ax_test = ax if not isinstance(ax, (list, tuple, np.ndarray)): ax_test = (ax,) for a in ax_test: if not isinstance(a, matplotlib.axes.Axes): raise TypeError("ax must be an matplotlib.axes.Axes") elif mesh_type != "cyl": axOpts = {"projection": "3d"} if self.dim == 3 else {} plt.figure() ax = plt.subplot(111, **axOpts) rcParams = rc_params() if lines: kwargs["color"] = kwargs.get("color", rcParams["lines.color"]) kwargs["linewidth"] = kwargs.get("linewidth", rcParams["lines.linewidth"]) out = plotter( ax=ax, nodes=nodes, faces=faces, centers=centers, edges=edges, lines=lines, **kwargs, ) if show_it: plt.show() return out def plot_image( self, v, v_type="CC", grid=False, view="real", ax=None, clim=None, show_it=False, pcolor_opts=None, stream_opts=None, grid_opts=None, range_x=None, range_y=None, sample_grid=None, stream_thickness=None, stream_threshold=None, **kwargs, ): """Plot quantities defined on a given mesh. This method is primarily used to plot models, scalar quantities and vector quantities defined on 2D meshes. For 3D :class:`discretize.TensorMesh` however, this method will plot the quantity for every slice of the 3D mesh. Parameters ---------- v : numpy.ndarray Gridded values being plotted. The length of the array depends on the quantity being plotted; e.g. if the quantity is a scalar value defined on mesh nodes, the length must be equal to the number of mesh nodes. v_type : {'CC','CCV', 'N', 'F', 'Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', 'Ez'} Defines the input parameter *v*. view : {'real', 'imag', 'abs', 'vec'} For complex scalar quantities, options are included to image the real, imaginary or absolute value. For vector quantities, *view* must be set to 'vec'. ax : matplotlib.axes.Axes, optional The axes to draw on. *None* produces a new Axes. clim : tuple of float, optional length 2 tuple of (vmin, vmax) for the color limits range_x, range_y : tuple of float, optional length 2 tuple of (min, max) for the bounds of the plot axes. pcolor_opts : dict, optional Arguments passed on to ``pcolormesh`` grid : bool, optional Whether to plot the edges of the mesh cells. grid_opts : dict, optional If ``grid`` is true, arguments passed on to ``plot`` for grid sample_grid : tuple of numpy.ndarray, optional If ``view`` == 'vec', mesh cell widths (hx, hy) to interpolate onto for vector plotting stream_opts : dict, optional If ``view`` == 'vec', arguments passed on to ``streamplot`` stream_thickness : float, optional If ``view`` == 'vec', linewidth for ``streamplot`` stream_threshold : float, optional If ``view`` == 'vec', only plots vectors with magnitude above this threshold show_it : bool, optional Whether to call plt.show() numbering : bool, optional For 3D :class:`~discretize.TensorMesh` only, show the numbering of the slices annotation_color : Color or str, optional For 3D :class:`~discretize.TensorMesh` only, color of the annotation Examples -------- 2D ``TensorMesh`` plotting >>> from matplotlib import pyplot as plt >>> import discretize >>> import numpy as np >>> M = discretize.TensorMesh([20, 20]) >>> v = np.sin(M.gridCC[:, 0]*2*np.pi)*np.sin(M.gridCC[:, 1]*2*np.pi) >>> M.plot_image(v) >>> plt.show() 3D ``TensorMesh`` plotting >>> import discretize >>> import numpy as np >>> M = discretize.TensorMesh([20, 20, 20]) >>> v = np.sin(M.gridCC[:, 0]*2*np.pi)*np.sin(M.gridCC[:, 1]*2*np.pi)*np.sin(M.gridCC[:, 2]*2*np.pi) >>> M.plot_image(v, annotation_color='k') >>> plt.show() """ matplotlib, plt = load_matplotlib() mesh_type = self._meshType.lower() plotters = { "tree": self.__plot_image_tree, "tensor": self.__plot_image_tensor, "curv": self.__plot_image_curv, "cyl": self.__plot_image_cyl, "simplex": self.__plot_image_simp, } try: plotter = plotters[mesh_type] except KeyError: raise NotImplementedError( "Mesh type `{}` does not have a plot_image implementation.".format( type(self).__name__ ) ) if "pcolorOpts" in kwargs: pcolor_opts = kwargs.pop("pcolorOpts") warnings.warn( "pcolorOpts has been deprecated, please use pcolor_opts", FutureWarning, stacklevel=2, ) if "streamOpts" in kwargs: stream_opts = kwargs.pop("streamOpts") warnings.warn( "streamOpts has been deprecated, please use stream_opts", FutureWarning, stacklevel=2, ) if "gridOpts" in kwargs: grid_opts = kwargs.pop("gridOpts") warnings.warn( "gridOpts has been deprecated, please use grid_opts", FutureWarning, stacklevel=2, ) if "showIt" in kwargs: show_it = kwargs.pop("showIt") warnings.warn( "showIt has been deprecated, please use show_it", FutureWarning, stacklevel=2, ) if "vType" in kwargs: v_type = kwargs.pop("vType") warnings.warn( "vType has been deprecated, please use v_type", FutureWarning, stacklevel=2, ) # Some Error checking and common defaults if pcolor_opts is None: pcolor_opts = {} if stream_opts is None: stream_opts = {"color": "k"} if grid_opts is None: if grid: grid_opts = {"color": "k"} else: grid_opts = {} v_typeOptsCC = ["N", "CC", "Fx", "Fy", "Ex", "Ey"] v_typeOptsV = ["CCv", "F", "E"] v_typeOpts = v_typeOptsCC + v_typeOptsV if view == "vec": if v_type not in v_typeOptsV: raise ValueError( "v_type must be in ['{0!s}'] when view='vec'".format( "', '".join(v_typeOptsV) ) ) if v_type not in v_typeOpts: raise ValueError( "v_type must be in ['{0!s}']".format("', '".join(v_typeOpts)) ) viewOpts = ["real", "imag", "abs", "vec"] if view not in viewOpts: raise ValueError("view must be in ['{0!s}']".format("', '".join(viewOpts))) if v.dtype == complex and view == "vec": raise NotImplementedError("Can not plot a complex vector.") if ax is None: plt.figure() ax = plt.subplot(111) else: if not isinstance(ax, matplotlib.axes.Axes): raise TypeError("ax must be an Axes!") if clim is not None: pcolor_opts["vmin"] = clim[0] pcolor_opts["vmax"] = clim[1] out = plotter( v, v_type=v_type, view=view, ax=ax, range_x=range_x, range_y=range_y, pcolor_opts=pcolor_opts, grid=grid, grid_opts=grid_opts, sample_grid=sample_grid, stream_opts=stream_opts, stream_threshold=stream_threshold, stream_thickness=stream_thickness, **kwargs, ) if show_it: plt.show() return out def plot_slice( self, v, v_type="CC", normal="Z", ind=None, slice_loc=None, grid=False, view="real", ax=None, clim=None, show_it=False, pcolor_opts=None, stream_opts=None, grid_opts=None, range_x=None, range_y=None, sample_grid=None, stream_threshold=None, stream_thickness=None, **kwargs, ): """Plot a slice of fields on the given 3D mesh. Parameters ---------- v : numpy.ndarray values to plot v_type : {'CC','CCV', 'N', 'F', 'Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', 'Ez'}, or tuple of these options Where the values of v are defined. normal : {'Z', 'X', 'Y'} Normal direction of slicing plane. ind : None, optional index along dimension of slice. Defaults to the center index. slice_loc : None, optional Value along dimension of slice. Defaults to the center of the mesh. view : {'real', 'imag', 'abs', 'vec'} How to view the array. ax : matplotlib.axes.Axes, optional The axes to draw on. None produces a new Axes. Must be None if ``v_type`` is a tuple. clim : tuple of float, optional length 2 tuple of (vmin, vmax) for the color limits range_x, range_y : tuple of float, optional length 2 tuple of (min, max) for the bounds of the plot axes. pcolor_opts : dict, optional Arguments passed on to ``pcolormesh`` grid : bool, optional Whether to plot the edges of the mesh cells. grid_opts : dict, optional If ``grid`` is true, arguments passed on to ``plot`` for the edges sample_grid : tuple of numpy.ndarray, optional If ``view`` == 'vec', mesh cell widths (hx, hy) to interpolate onto for vector plotting stream_opts : dict, optional If ``view`` == 'vec', arguments passed on to ``streamplot`` stream_thickness : float, optional If ``view`` == 'vec', linewidth for ``streamplot`` stream_threshold : float, optional If ``view`` == 'vec', only plots vectors with magnitude above this threshold show_it : bool, optional Whether to call plt.show() Examples -------- Plot a slice of a 3D `TensorMesh` solution to a Laplace's equaiton. First build the mesh: >>> from matplotlib import pyplot as plt >>> import discretize >>> from scipy.sparse.linalg import spsolve >>> hx = [(5, 2, -1.3), (2, 4), (5, 2, 1.3)] >>> hy = [(2, 2, -1.3), (2, 6), (2, 2, 1.3)] >>> hz = [(2, 2, -1.3), (2, 6), (2, 2, 1.3)] >>> M = discretize.TensorMesh([hx, hy, hz]) then build the necessary parts of the PDE: >>> q = np.zeros(M.vnC) >>> q[[4, 4], [4, 4], [2, 6]]=[-1, 1] >>> q = discretize.utils.mkvc(q) >>> A = M.face_divergence * M.cell_gradient >>> b = spsolve(A, q) and finaly, plot the vector values of the result, which are defined on faces >>> M.plot_slice(M.cell_gradient*b, 'F', view='vec', grid=True, pcolor_opts={'alpha':0.8}) >>> plt.show() We can use the `slice_loc kwarg to tell `plot_slice` where to slice the mesh. Let's create a mesh with a random model and plot slice of it. The `slice_loc` kwarg automatically determines the indices for slicing the mesh along a plane with the given normal. >>> M = discretize.TensorMesh([32, 32, 32]) >>> v = discretize.utils.random_model(M.vnC, random_seed=789).reshape(-1, order='F') >>> x_slice, y_slice, z_slice = 0.75, 0.25, 0.9 >>> plt.figure(figsize=(7.5, 3)) >>> ax = plt.subplot(131) >>> M.plot_slice(v, normal='X', slice_loc=x_slice, ax=ax) >>> ax = plt.subplot(132) >>> M.plot_slice(v, normal='Y', slice_loc=y_slice, ax=ax) >>> ax = plt.subplot(133) >>> M.plot_slice(v, normal='Z', slice_loc=z_slice, ax=ax) >>> plt.tight_layout() >>> plt.show() This also works for `TreeMesh`. We create a mesh here that is refined within three boxes, along with a base level of refinement. >>> TM = discretize.TreeMesh([32, 32, 32]) >>> TM.refine(3, finalize=False) >>> BSW = [[0.25, 0.25, 0.25], [0.15, 0.15, 0.15], [0.1, 0.1, 0.1]] >>> TNE = [[0.75, 0.75, 0.75], [0.85, 0.85, 0.85], [0.9, 0.9, 0.9]] >>> levels = [6, 5, 4] >>> TM.refine_box(BSW, TNE, levels) >>> v_TM = discretize.utils.volume_average(M, TM, v) >>> plt.figure(figsize=(7.5, 3)) >>> ax = plt.subplot(131) >>> TM.plot_slice(v_TM, normal='X', slice_loc=x_slice, ax=ax) >>> ax = plt.subplot(132) >>> TM.plot_slice(v_TM, normal='Y', slice_loc=y_slice, ax=ax) >>> ax = plt.subplot(133) >>> TM.plot_slice(v_TM, normal='Z', slice_loc=z_slice, ax=ax) >>> plt.tight_layout() >>> plt.show() """ matplotlib, plt = load_matplotlib() mesh_type = self._meshType.lower() plotters = { "tree": self.__plot_slice_tree, "tensor": self.__plot_slice_tensor, # 'curv': self.__plot_slice_curv, # 'cyl': self.__plot_slice_cyl, } try: plotter = plotters[mesh_type] except KeyError: raise NotImplementedError( "Mesh type `{}` does not have a plot_slice implementation.".format( type(self).__name__ ) ) normal = normal.upper() if "pcolorOpts" in kwargs: pcolor_opts = kwargs["pcolorOpts"] warnings.warn( "pcolorOpts has been deprecated, please use pcolor_opts", FutureWarning, stacklevel=2, ) if "streamOpts" in kwargs: stream_opts = kwargs["streamOpts"] warnings.warn( "streamOpts has been deprecated, please use stream_opts", FutureWarning, stacklevel=2, ) if "gridOpts" in kwargs: grid_opts = kwargs["gridOpts"] warnings.warn( "gridOpts has been deprecated, please use grid_opts", FutureWarning, stacklevel=2, ) if "showIt" in kwargs: show_it = kwargs["showIt"] warnings.warn( "showIt has been deprecated, please use show_it", FutureWarning, stacklevel=2, ) if "vType" in kwargs: v_type = kwargs["vType"] warnings.warn( "vType has been deprecated, please use v_type", FutureWarning, stacklevel=2, ) if pcolor_opts is None: pcolor_opts = {} if stream_opts is None: stream_opts = {"color": "k"} if grid_opts is None: if grid: grid_opts = {"color": "k"} else: grid_opts = {} if type(v_type) in [list, tuple]: if ax is not None: raise TypeError("cannot specify an axis to plot on with this function.") fig, axs = plt.subplots(1, len(v_type)) out = [] for v_typeI, ax in zip(v_type, axs): out += [ self.plot_slice( v, v_type=v_typeI, normal=normal, ind=ind, slice_loc=slice_loc, grid=grid, view=view, ax=ax, clim=clim, show_it=False, pcolor_opts=pcolor_opts, stream_opts=stream_opts, grid_opts=grid_opts, stream_threshold=stream_threshold, stream_thickness=stream_thickness, ) ] return out viewOpts = ["real", "imag", "abs", "vec"] normalOpts = ["X", "Y", "Z"] v_typeOpts = [ "CC", "CCv", "N", "F", "E", "Fx", "Fy", "Fz", "E", "Ex", "Ey", "Ez", ] # Some user error checking if v_type not in v_typeOpts: raise ValueError( "v_type must be in ['{0!s}']".format("', '".join(v_typeOpts)) ) if not self.dim == 3: raise TypeError("Must be a 3D mesh. Use plot_image.") if view not in viewOpts: raise ValueError("view must be in ['{0!s}']".format("', '".join(viewOpts))) if normal not in normalOpts: raise ValueError( "normal must be in ['{0!s}']".format("', '".join(normalOpts)) ) if not isinstance(grid, bool): raise TypeError("grid must be a boolean") if v.dtype == complex and view == "vec": raise NotImplementedError("Can not plot a complex vector.") if self.dim == 2: raise NotImplementedError("Must be a 3D mesh. Use plot_image.") # slice_loc errors if (ind is not None) and (slice_loc is not None): raise Warning("Both ind and slice_loc are defined. Behavior undefined.") # slice_loc implement if slice_loc is not None: if normal == "X": ind = int(np.argmin(np.abs(self.cell_centers_x - slice_loc))) if normal == "Y": ind = int(np.argmin(np.abs(self.cell_centers_y - slice_loc))) if normal == "Z": ind = int(np.argmin(np.abs(self.cell_centers_z - slice_loc))) if ax is None: plt.figure() ax = plt.subplot(111) else: if not isinstance(ax, matplotlib.axes.Axes): raise TypeError("ax must be an matplotlib.axes.Axes") if clim is not None: pcolor_opts["vmin"] = clim[0] pcolor_opts["vmax"] = clim[1] out = plotter( v, v_type=v_type, normal=normal, ind=ind, grid=grid, view=view, ax=ax, pcolor_opts=pcolor_opts, stream_opts=stream_opts, grid_opts=grid_opts, range_x=range_x, range_y=range_y, sample_grid=sample_grid, stream_threshold=stream_threshold, stream_thickness=stream_thickness, **kwargs, ) if show_it: plt.show() return out def plot_3d_slicer( self, v, xslice=None, yslice=None, zslice=None, v_type="CC", view="real", axis="xy", transparent=None, clim=None, xlim=None, ylim=None, zlim=None, aspect="auto", grid=(2, 2, 1), pcolor_opts=None, fig=None, **kwargs, ): """Plot slices of a 3D volume, interactively (scroll wheel). If called from a notebook, make sure to set %matplotlib notebook See the class `discretize.View.Slicer` for more information. It returns nothing. However, if you need the different figure handles you can get it via `fig = plt.gcf()` and subsequently its children via `fig.get_children()` and recursively deeper, e.g., `fig.get_children()[0].get_children()`. One can also provide an existing figure instance, which can be useful for interactive widgets in Notebooks. The provided figure is cleared first. """ _, plt = load_matplotlib() mesh_type = self._meshType.lower() if mesh_type != "tensor": raise NotImplementedError( "plot_3d_slicer has only been implemented for a TensorMesh" ) # Initiate figure if fig is None: fig = plt.figure() else: fig.clf() if "pcolorOpts" in kwargs: pcolor_opts = kwargs["pcolorOpts"] warnings.warn( "pcolorOpts has been deprecated, please use pcolor_opts", FutureWarning, stacklevel=2, ) # Populate figure tracker = Slicer( self, v, xslice, yslice, zslice, v_type, view, axis, transparent, clim, xlim, ylim, zlim, aspect, grid, pcolor_opts, ) # Connect figure to scrolling fig.canvas.mpl_connect("scroll_event", tracker.onscroll) # TensorMesh plotting def __plot_grid_tensor( self, ax=None, nodes=False, faces=False, centers=False, edges=False, lines=True, color="C0", linewidth=1.0, **kwargs, ): if self.dim == 1: if nodes: ax.plot( self.gridN, np.ones(self.nN), color="C0", marker="s", linestyle="" ) if centers: ax.plot( self.gridCC, np.ones(self.nC), color="C1", marker="o", linestyle="" ) if lines: ax.plot(self.gridN, np.ones(self.nN), color="C0", linestyle="-") ax.set_xlabel("x1") elif self.dim == 2: if nodes: ax.plot( self.gridN[:, 0], self.gridN[:, 1], color="C0", marker="s", linestyle="", ) if centers: ax.plot( self.gridCC[:, 0], self.gridCC[:, 1], color="C1", marker="o", linestyle="", ) if faces: ax.plot( self.gridFx[:, 0], self.gridFx[:, 1], color="C2", marker=">", linestyle="", ) ax.plot( self.gridFy[:, 0], self.gridFy[:, 1], color="C2", marker="^", linestyle="", ) if edges: ax.plot( self.gridEx[:, 0], self.gridEx[:, 1], color="C3", marker=">", linestyle="", ) ax.plot( self.gridEy[:, 0], self.gridEy[:, 1], color="C3", marker="^", linestyle="", ) # Plot the grid lines if lines: NN = self.reshape(self.gridN, "N", "N", "M") nCx, nCy = self.shape_cells X1 = np.c_[ mkvc(NN[0][0, :]), mkvc(NN[0][nCx, :]), mkvc(NN[0][0, :]) * np.nan ].flatten() Y1 = np.c_[ mkvc(NN[1][0, :]), mkvc(NN[1][nCx, :]), mkvc(NN[1][0, :]) * np.nan ].flatten() X2 = np.c_[ mkvc(NN[0][:, 0]), mkvc(NN[0][:, nCy]), mkvc(NN[0][:, 0]) * np.nan ].flatten() Y2 = np.c_[ mkvc(NN[1][:, 0]), mkvc(NN[1][:, nCy]), mkvc(NN[1][:, 0]) * np.nan ].flatten() X = np.r_[X1, X2] Y = np.r_[Y1, Y2] ax.plot(X, Y, color=color, linestyle="-", lw=linewidth) ax.set_xlabel("x1") ax.set_ylabel("x2") elif self.dim == 3: if nodes: ax.plot( self.gridN[:, 0], self.gridN[:, 1], color="C0", marker="s", linestyle="", zs=self.gridN[:, 2], ) if centers: ax.plot( self.gridCC[:, 0], self.gridCC[:, 1], color="C1", marker="o", linestyle="", zs=self.gridCC[:, 2], ) if faces: ax.plot( self.gridFx[:, 0], self.gridFx[:, 1], color="C2", marker=">", linestyle="", zs=self.gridFx[:, 2], ) ax.plot( self.gridFy[:, 0], self.gridFy[:, 1], color="C2", marker="<", linestyle="", zs=self.gridFy[:, 2], ) ax.plot( self.gridFz[:, 0], self.gridFz[:, 1], color="C2", marker="^", linestyle="", zs=self.gridFz[:, 2], ) if edges: ax.plot( self.gridEx[:, 0], self.gridEx[:, 1], color="C3", marker=">", linestyle="", zs=self.gridEx[:, 2], ) ax.plot( self.gridEy[:, 0], self.gridEy[:, 1], color="C3", marker="<", linestyle="", zs=self.gridEy[:, 2], ) ax.plot( self.gridEz[:, 0], self.gridEz[:, 1], color="C3", marker="^", linestyle="", zs=self.gridEz[:, 2], ) # Plot the grid lines if lines: nCx, nCy, nCz = self.shape_cells NN = self.reshape(self.gridN, "N", "N", "M") X1 = np.c_[ mkvc(NN[0][0, :, :]), mkvc(NN[0][nCx, :, :]), mkvc(NN[0][0, :, :]) * np.nan, ].flatten() Y1 = np.c_[ mkvc(NN[1][0, :, :]), mkvc(NN[1][nCx, :, :]), mkvc(NN[1][0, :, :]) * np.nan, ].flatten() Z1 = np.c_[ mkvc(NN[2][0, :, :]), mkvc(NN[2][nCx, :, :]), mkvc(NN[2][0, :, :]) * np.nan, ].flatten() X2 = np.c_[ mkvc(NN[0][:, 0, :]), mkvc(NN[0][:, nCy, :]), mkvc(NN[0][:, 0, :]) * np.nan, ].flatten() Y2 = np.c_[ mkvc(NN[1][:, 0, :]), mkvc(NN[1][:, nCy, :]), mkvc(NN[1][:, 0, :]) * np.nan, ].flatten() Z2 = np.c_[ mkvc(NN[2][:, 0, :]), mkvc(NN[2][:, nCy, :]), mkvc(NN[2][:, 0, :]) * np.nan, ].flatten() X3 = np.c_[ mkvc(NN[0][:, :, 0]), mkvc(NN[0][:, :, nCz]), mkvc(NN[0][:, :, 0]) * np.nan, ].flatten() Y3 = np.c_[ mkvc(NN[1][:, :, 0]), mkvc(NN[1][:, :, nCz]), mkvc(NN[1][:, :, 0]) * np.nan, ].flatten() Z3 = np.c_[ mkvc(NN[2][:, :, 0]), mkvc(NN[2][:, :, nCz]), mkvc(NN[2][:, :, 0]) * np.nan, ].flatten() X = np.r_[X1, X2, X3] Y = np.r_[Y1, Y2, Y3] Z = np.r_[Z1, Z2, Z3] ax.plot(X, Y, color=color, linestyle="-", lw=linewidth, zs=Z) ax.set_xlabel("x1") ax.set_ylabel("x2") ax.set_zlabel("x3") ax.grid(True) return ax def __plot_image_tensor( self, v, v_type="CC", grid=False, view="real", ax=None, pcolor_opts=None, stream_opts=None, grid_opts=None, numbering=True, annotation_color="w", range_x=None, range_y=None, sample_grid=None, stream_threshold=None, **kwargs, ): if "annotationColor" in kwargs: annotation_color = kwargs.pop("annotationColor") warnings.warn( "annotationColor has been deprecated, please use annotation_color", FutureWarning, stacklevel=2, ) if self.dim == 1: if v_type == "CC": ph = ax.plot( self.cell_centers_x, v, linestyle="-", color="C1", marker="o" ) elif v_type == "N": ph = ax.plot(self.nodes_x, v, linestyle="-", color="C0", marker="s") ax.set_xlabel("x") ax.axis("tight") elif self.dim == 2: return self.__plot_image_tensor2D( v, v_type=v_type, grid=grid, view=view, ax=ax, pcolor_opts=pcolor_opts, stream_opts=stream_opts, grid_opts=grid_opts, range_x=range_x, range_y=range_y, sample_grid=sample_grid, stream_threshold=stream_threshold, ) elif self.dim == 3: # get copy of image and average to cell-centers is necessary if v_type == "CC": vc = v.reshape(self.vnC, order="F") elif v_type == "N": vc = (self.aveN2CC * v).reshape(self.vnC, order="F") elif v_type in ["Fx", "Fy", "Fz", "Ex", "Ey", "Ez"]: aveOp = "ave" + v_type[0] + "2CCV" # n = getattr(self, 'vn'+v_type[0]) # if 'x' in v_type: v = np.r_[v, np.zeros(n[1]), np.zeros(n[2])] # if 'y' in v_type: v = np.r_[np.zeros(n[0]), v, np.zeros(n[2])] # if 'z' in v_type: v = np.r_[np.zeros(n[0]), np.zeros(n[1]), v] v = getattr(self, aveOp) * v # average to cell centers ind_xyz = {"x": 0, "y": 1, "z": 2}[v_type[1]] vc = self.reshape(v.reshape((self.nC, -1), order="F"), "CC", "CC", "M")[ ind_xyz ] nCx, nCy, nCz = self.shape_cells # determine number oE slices in x and y dimension nX = int(np.ceil(np.sqrt(nCz))) nY = int(np.ceil(nCz / nX)) # allocate space for montage C = np.zeros((nX * nCx, nY * nCy)) for iy in range(int(nY)): for ix in range(int(nX)): iz = ix + iy * nX if iz < nCz: C[ix * nCx : (ix + 1) * nCx, iy * nCy : (iy + 1) * nCy] = vc[ :, :, iz ] else: C[ix * nCx : (ix + 1) * nCx, iy * nCy : (iy + 1) * nCy] = np.nan C = np.ma.masked_where(np.isnan(C), C) xx = np.r_[0, np.cumsum(np.kron(np.ones((nX, 1)), self.h[0]).ravel())] yy = np.r_[0, np.cumsum(np.kron(np.ones((nY, 1)), self.h[1]).ravel())] # Plot the mesh ph = ax.pcolormesh(xx, yy, C.T, **pcolor_opts) # Plot the lines gx = np.arange(nX + 1) * (self.nodes_x[-1] - self.origin[0]) gy = np.arange(nY + 1) * (self.nodes_y[-1] - self.origin[1]) # Repeat and seperate with NaN gxX = np.c_[gx, gx, gx + np.nan].ravel() gxY = np.kron( np.ones((nX + 1, 1)), np.array([0, sum(self.h[1]) * nY, np.nan]) ).ravel() gyX = np.kron( np.ones((nY + 1, 1)), np.array([0, sum(self.h[0]) * nX, np.nan]) ).ravel() gyY = np.c_[gy, gy, gy + np.nan].ravel() ax.plot(gxX, gxY, annotation_color + "-", linewidth=2) ax.plot(gyX, gyY, annotation_color + "-", linewidth=2) ax.axis("tight") if numbering: pad = np.sum(self.h[0]) * 0.04 for iy in range(int(nY)): for ix in range(int(nX)): iz = ix + iy * nX if iz < nCz: ax.text( (ix + 1) * (self.nodes_x[-1] - self.origin[0]) - pad, (iy) * (self.nodes_y[-1] - self.origin[1]) + pad, "#{0:.0f}".format(iz), color=annotation_color, verticalalignment="bottom", horizontalalignment="right", size="x-large", ) ax.set_title(v_type) return (ph,) def __plot_image_tensor2D( self, v, v_type="CC", grid=False, view="real", ax=None, pcolor_opts=None, stream_opts=None, grid_opts=None, range_x=None, range_y=None, sample_grid=None, stream_threshold=None, stream_thickness=None, ): # Common function for plotting an image of a TensorMesh matplotlib, plt = load_matplotlib() if ax is None: plt.figure() ax = plt.subplot(111) else: if not isinstance(ax, matplotlib.axes.Axes): raise AssertionError("ax must be an matplotlib.axes.Axes") # Reshape to a cell centered variable if v_type == "CC": pass elif v_type == "CCv": if view != "vec": raise AssertionError("Other types for CCv not supported") elif v_type in ["F", "E", "N"]: aveOp = "ave" + v_type + ("2CCV" if view == "vec" else "2CC") v = getattr(self, aveOp) * v # average to cell centers (might be a vector) elif v_type in ["Fx", "Fy", "Ex", "Ey"]: aveOp = "ave" + v_type[0] + "2CCV" v = getattr(self, aveOp) * v # average to cell centers (might be a vector) xORy = {"x": 0, "y": 1}[v_type[1]] v = v.reshape((self.nC, -1), order="F")[:, xORy] out = () if view in ["real", "imag", "abs"]: v = self.reshape(v, "CC", "CC", "M") v = getattr(np, view)(v) # e.g. np.real(v) v = np.ma.masked_where(np.isnan(v), v) out += ( ax.pcolormesh( self.nodes_x, self.nodes_y, v.T, **{**pcolor_opts, **grid_opts}, ), ) elif view in ["vec"]: # Matplotlib seems to not support irregular # spaced vectors at the moment. So we will # Interpolate down to a regular mesh at the # smallest mesh size in this 2D slice. if sample_grid is not None: hxmin = sample_grid[0] hymin = sample_grid[1] else: hxmin = self.h[0].min() hymin = self.h[1].min() if range_x is not None: dx = range_x[1] - range_x[0] nxi = int(dx / hxmin) hx = np.ones(nxi) * dx / nxi origin_x = range_x[0] else: nxi = int(self.h[0].sum() / hxmin) hx = np.ones(nxi) * self.h[0].sum() / nxi origin_x = self.origin[0] if range_y is not None: dy = range_y[1] - range_y[0] nyi = int(dy / hymin) hy = np.ones(nyi) * dy / nyi origin_y = range_y[0] else: nyi = int(self.h[1].sum() / hymin) hy = np.ones(nyi) * self.h[1].sum() / nyi origin_y = self.origin[1] U, V = self.reshape(v.reshape((self.nC, -1), order="F"), "CC", "CC", "M") tMi = self.__class__(h=[hx, hy], origin=np.r_[origin_x, origin_y]) P = self.get_interpolation_matrix(tMi.gridCC, "CC", zeros_outside=True) Ui = tMi.reshape(P * mkvc(U), "CC", "CC", "M") Vi = tMi.reshape(P * mkvc(V), "CC", "CC", "M") # End Interpolation x = self.nodes_x y = self.nodes_y if range_x is not None: x = tMi.nodes_x if range_y is not None: y = tMi.nodes_y if range_x is not None or range_y is not None: # use interpolated values U = Ui V = Vi if stream_threshold is not None: mask_me = np.sqrt(Ui**2 + Vi**2) <= stream_threshold Ui = np.ma.masked_where(mask_me, Ui) Vi = np.ma.masked_where(mask_me, Vi) if stream_thickness is not None: scaleFact = np.copy(stream_thickness) # Calculate vector amplitude vecAmp = np.sqrt(U**2 + V**2).T # Form bounds to knockout the top and bottom 10% vecAmp_sort = np.sort(vecAmp.ravel()) nVecAmp = vecAmp.size tenPercInd = int(np.ceil(0.1 * nVecAmp)) lowerBound = vecAmp_sort[tenPercInd] upperBound = vecAmp_sort[-tenPercInd] lowInds = np.where(vecAmp < lowerBound) vecAmp[lowInds] = lowerBound highInds = np.where(vecAmp > upperBound) vecAmp[highInds] = upperBound # Normalize amplitudes 0-1 norm_thickness = vecAmp / vecAmp.max() # Scale by user defined thickness factor stream_thickness = scaleFact * norm_thickness # Add linewidth to stream_opts stream_opts.update({"linewidth": stream_thickness}) out += ( ax.pcolormesh( x, y, np.sqrt(U**2 + V**2).T, **{**pcolor_opts, **grid_opts}, ), ) out += ( ax.streamplot( tMi.cell_centers_x, tMi.cell_centers_y, Ui.T, Vi.T, **stream_opts, ), ) ax.set_xlabel("x") ax.set_ylabel("y") if range_x is not None: ax.set_xlim(*range_x) else: ax.set_xlim(*self.nodes_x[[0, -1]]) if range_y is not None: ax.set_ylim(*range_y) else: ax.set_ylim(*self.nodes_y[[0, -1]]) return out def __plot_slice_tensor( self, v, v_type="CC", normal="z", ind=None, grid=False, view="real", ax=None, pcolor_opts=None, stream_opts=None, grid_opts=None, range_x=None, range_y=None, sample_grid=None, stream_threshold=None, stream_thickness=None, **kwargs, ): dim_ind = {"X": 0, "Y": 1, "Z": 2}[normal] szSliceDim = self.shape_cells[dim_ind] #: Size of the sliced dimension if ind is None: ind = szSliceDim // 2 if not isinstance(ind, (np.integer, int)): raise TypeError("ind must be an integer") def getIndSlice(v): if normal == "X": v = v[ind, :, :] elif normal == "Y": v = v[:, ind, :] elif normal == "Z": v = v[:, :, ind] return v def doSlice(v): if v_type == "CC": return getIndSlice(self.reshape(v, "CC", "CC", "M")) elif v_type == "CCv": if view != "vec": raise AssertionError("Other types for CCv not supported") else: # Now just deal with 'F' and 'E' (x, y, z, maybe...) aveOp = "ave" + v_type + ("2CCV" if view == "vec" else "2CC") Av = getattr(self, aveOp) if v.size == Av.shape[1]: v = Av * v else: v = self.reshape(v, v_type[0], v_type) # get specific component v = Av * v # we should now be averaged to cell centers (might be a vector) v = self.reshape(v.reshape((self.nC, -1), order="F"), "CC", "CC", "M") if view == "vec": outSlice = [] if "X" not in normal: outSlice.append(getIndSlice(v[0])) if "Y" not in normal: outSlice.append(getIndSlice(v[1])) if "Z" not in normal: outSlice.append(getIndSlice(v[2])) return np.r_[mkvc(outSlice[0]), mkvc(outSlice[1])] else: return getIndSlice(self.reshape(v, "CC", "CC", "M")) h2d = [] x2d = [] if "X" not in normal: h2d.append(self.h[0]) x2d.append(self.origin[0]) if "Y" not in normal: h2d.append(self.h[1]) x2d.append(self.origin[1]) if "Z" not in normal: h2d.append(self.h[2]) x2d.append(self.origin[2]) tM = self.__class__(h=h2d, origin=x2d) #: Temp Mesh v2d = doSlice(v) out = tM.__plot_image_tensor2D( v2d, v_type=("CCv" if view == "vec" else "CC"), grid=grid, view=view, ax=ax, pcolor_opts=pcolor_opts, stream_opts=stream_opts, grid_opts=grid_opts, range_x=range_x, range_y=range_y, sample_grid=sample_grid, stream_threshold=stream_threshold, stream_thickness=stream_thickness, ) ax.set_xlabel("y" if normal == "X" else "x") ax.set_ylabel("y" if normal == "Z" else "z") ax.set_title("Slice {0:.0f}".format(ind)) return out # CylindricalMesh plotting def __plotCylTensorMesh(self, plotType, *args, **kwargs): matplotlib, plt = load_matplotlib() if not self.is_symmetric: raise NotImplementedError("We have not yet implemented this type of view.") if plotType not in ["plot_image", "plot_grid"]: raise TypeError("plotType must be either 'plot_grid' or 'plot_image'.") if len(args) > 0: val = args[0] v_type = kwargs.get("v_type", None) mirror = kwargs.pop("mirror", None) mirror_data = kwargs.pop("mirror_data", None) if mirror_data is not None and mirror is None: mirror = True if v_type is not None: if v_type.upper() != "CCV": if v_type.upper() == "F": val = mkvc(self.aveF2CCV * val) if mirror_data is not None: mirror_data = mkvc(self.aveF2CCV * mirror_data) kwargs["v_type"] = "CCv" # now the vector is cell centered if v_type.upper() == "E": val = mkvc(self.aveE2CCV * val) if mirror_data is not None: mirror_data = mkvc(self.aveE2CCV * mirror_data) args = (val,) + args[1:] if mirror: # create a mirrored mesh hx = np.hstack([np.flipud(self.h[0]), self.h[0]]) origin0 = self.origin[0] - self.h[0].sum() M = discretize.TensorMesh([hx, self.h[2]], origin=[origin0, self.origin[2]]) if mirror_data is None: mirror_data = val if len(val) == self.nC: # only a single value at cell centers val = val.reshape(self.vnC[0], self.vnC[2], order="F") mirror_val = mirror_data.reshape(self.vnC[0], self.vnC[2], order="F") val = mkvc(np.vstack([np.flipud(mirror_val), val])) elif len(val) == 2 * self.nC: val_x = val[: self.nC].reshape(self.vnC[0], self.vnC[2], order="F") val_z = val[self.nC :].reshape(self.vnC[0], self.vnC[2], order="F") mirror_x = mirror_data[: self.nC].reshape( self.vnC[0], self.vnC[2], order="F" ) mirror_z = mirror_data[self.nC :].reshape( self.vnC[0], self.vnC[2], order="F" ) val_x = mkvc( np.vstack([-1.0 * np.flipud(mirror_x), val_x]) ) # by symmetry val_z = mkvc(np.vstack([np.flipud(mirror_z), val_z])) val = np.hstack([val_x, val_z]) args = (val,) + args[1:] else: M = discretize.TensorMesh( [self.h[0], self.h[2]], origin=[self.origin[0], self.origin[2]] ) ax = kwargs.get("ax", None) if ax is None: plt.figure() ax = plt.subplot(111) kwargs["ax"] = ax else: if not isinstance(ax, matplotlib.axes.Axes): raise AssertionError("ax must be an matplotlib.axes.Axes") out = getattr(M, plotType)(*args, **kwargs) ax.set_xlabel("x") ax.set_ylabel("z") return out def __plot_grid_cyl(self, *args, **kwargs): _, plt = load_matplotlib() if self.is_symmetric: return self.__plotCylTensorMesh("plot_grid", *args, **kwargs) # allow a slice to be provided for the mesh slc = kwargs.pop("slice", None) if isinstance(slc, str): slc = slc.lower() if slc not in ["theta", "z", "both", None]: raise ValueError( "slice must be either 'theta','z', or 'both' not {}".format(slc) ) # if slc is None, provide slices in both the theta and z directions if slc == "theta": return self.__plot_gridThetaSlice(*args, **kwargs) elif slc == "z": return self.__plot_gridZSlice(*args, **kwargs) else: ax = kwargs.pop("ax", None) if ax is not None: if not isinstance(ax, list) or len(ax) != 2: warnings.warn( "two axes handles must be provided to plot both theta " "and z slices through the mesh. Over-writing the axes.", stacklevel=2, ) ax = None else: # find the one with a polar projection and pass it to the # theta slice, other one to the z-slice polarax = [ a for a in ax if a.__class__.__name__ in ["PolarAxesSubplot", "PolarAxes"] ] if len(polarax) != 1: warnings.warn( """ No polar axes provided. Over-writing the axes. If you prefer to create your own, please use `ax = plt.subplot(121, projection='polar')` for reference, see: http://matplotlib.org/examples/pylab_examples/polar_demo.html https://github.com/matplotlib/matplotlib/issues/312 """, stacklevel=2, ) ax = None else: polarax = polarax[0] cartax = [a for a in ax if a != polarax][0] # ax may have been None to start with or set to None if ax is None: plt.figure(figsize=(12, 5)) polarax = plt.subplot(121, projection="polar") cartax = plt.subplot(122) # update kwargs with respective axes handles kwargspolar = kwargs.copy() kwargspolar["ax"] = polarax kwargscart = kwargs.copy() kwargscart["ax"] = cartax ax = [] ax.append(self.__plot_gridZSlice(*args, **kwargspolar)) ax.append(self.__plot_gridThetaSlice(*args, **kwargscart)) plt.tight_layout() return ax def __plot_gridThetaSlice(self, *args, **kwargs): # make a cyl symmetric mesh h2d = [self.h[0], 1, self.h[2]] mesh2D = self.__class__(h=h2d, origin=self.origin) return mesh2D.plot_grid(*args, **kwargs) def __plot_gridZSlice(self, *args, **kwargs): _, plt = load_matplotlib() # https://github.com/matplotlib/matplotlib/issues/312 ax = kwargs.get("ax", None) if ax is not None: print(ax.__class__.__name__) if ax.__class__.__name__ not in ["PolarAxesSubplot", "PolarAxes"]: warnings.warn( """ Creating new axes with Polar projection. If you prefer to create your own, please use `ax = plt.subplot(121, projection='polar')` for reference, see: http://matplotlib.org/examples/pylab_examples/polar_demo.html https://github.com/matplotlib/matplotlib/issues/312 """, stacklevel=2, ) ax = plt.subplot(111, projection="polar") else: ax = plt.subplot(111, projection="polar") # radial lines NN = ndgrid(self.nodes_x, self.nodes_y, np.r_[0])[:, :2] NN = NN.reshape((self.vnN[0], self.vnN[1], 2), order="F") NN = [NN[:, :, 0], NN[:, :, 1]] X1 = np.c_[ mkvc(NN[0][0, :]), mkvc(NN[0][self.shape_cells[0], :]), mkvc(NN[0][0, :]) * np.nan, ].flatten() Y1 = np.c_[ mkvc(NN[1][0, :]), mkvc(NN[1][self.shape_cells[0], :]), mkvc(NN[1][0, :]) * np.nan, ].flatten() color = kwargs.get("color", "C0") linewidth = kwargs.get("linewidth", 1.0) ax.plot(Y1, X1, linestyle="-", color=color, lw=linewidth) # circles n = 100 if self.is_wrapped: th = np.linspace(0, 2 * np.pi, n) else: th = np.linspace(self.nodes_y[0], self.nodes_y[-1], n) for r in self.nodes_x: ax.plot( th, r * np.ones(n), linestyle="-", color=color, lw=linewidth, ) return ax def __plot_image_cyl(self, *args, **kwargs): return self.__plotCylTensorMesh("plot_image", *args, **kwargs) # CurvilinearMesh plotting: def __plot_grid_curv( self, ax=None, nodes=False, faces=False, centers=False, edges=False, lines=True, color="C0", linewidth=1.0, **kwargs, ): NN = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: if lines: X1 = np.c_[ mkvc(NN[0][:-1, :]), mkvc(NN[0][1:, :]), mkvc(NN[0][:-1, :]) * np.nan, ].flatten() Y1 = np.c_[ mkvc(NN[1][:-1, :]), mkvc(NN[1][1:, :]), mkvc(NN[1][:-1, :]) * np.nan, ].flatten() X2 = np.c_[ mkvc(NN[0][:, :-1]), mkvc(NN[0][:, 1:]), mkvc(NN[0][:, :-1]) * np.nan, ].flatten() Y2 = np.c_[ mkvc(NN[1][:, :-1]), mkvc(NN[1][:, 1:]), mkvc(NN[1][:, :-1]) * np.nan, ].flatten() X = np.r_[X1, X2] Y = np.r_[Y1, Y2] ax.plot(X, Y, color=color, linewidth=linewidth, linestyle="-", **kwargs) elif self.dim == 3: X1 = np.c_[ mkvc(NN[0][:-1, :, :]), mkvc(NN[0][1:, :, :]), mkvc(NN[0][:-1, :, :]) * np.nan, ].flatten() Y1 = np.c_[ mkvc(NN[1][:-1, :, :]), mkvc(NN[1][1:, :, :]), mkvc(NN[1][:-1, :, :]) * np.nan, ].flatten() Z1 = np.c_[ mkvc(NN[2][:-1, :, :]), mkvc(NN[2][1:, :, :]), mkvc(NN[2][:-1, :, :]) * np.nan, ].flatten() X2 = np.c_[ mkvc(NN[0][:, :-1, :]), mkvc(NN[0][:, 1:, :]), mkvc(NN[0][:, :-1, :]) * np.nan, ].flatten() Y2 = np.c_[ mkvc(NN[1][:, :-1, :]), mkvc(NN[1][:, 1:, :]), mkvc(NN[1][:, :-1, :]) * np.nan, ].flatten() Z2 = np.c_[ mkvc(NN[2][:, :-1, :]), mkvc(NN[2][:, 1:, :]), mkvc(NN[2][:, :-1, :]) * np.nan, ].flatten() X3 = np.c_[ mkvc(NN[0][:, :, :-1]), mkvc(NN[0][:, :, 1:]), mkvc(NN[0][:, :, :-1]) * np.nan, ].flatten() Y3 = np.c_[ mkvc(NN[1][:, :, :-1]), mkvc(NN[1][:, :, 1:]), mkvc(NN[1][:, :, :-1]) * np.nan, ].flatten() Z3 = np.c_[ mkvc(NN[2][:, :, :-1]), mkvc(NN[2][:, :, 1:]), mkvc(NN[2][:, :, :-1]) * np.nan, ].flatten() X = np.r_[X1, X2, X3] Y = np.r_[Y1, Y2, Y3] Z = np.r_[Z1, Z2, Z3] ax.plot(X, Y, Z, color=color, linewidth=linewidth, linestyle="-", **kwargs) ax.set_zlabel("x3") if nodes: ax.plot(*self.gridN.T, color=color, linestyle="", marker="s") if centers: ax.plot(*self.gridCC.T, color="C1", linestyle="", marker="o") if faces: ax.plot(*self.gridFx.T, color="C2", marker=">", linestyle="") ax.plot(*self.gridFy.T, color="C2", marker="<", linestyle="") if self.dim == 3: ax.plot(*self.gridFz.T, color="C2", marker="^", linestyle="") if edges: ax.plot(*self.gridEx.T, color="C3", marker=">", linestyle="") ax.plot(*self.gridEy.T, color="C3", marker="<", linestyle="") if self.dim == 3: ax.plot(*self.gridEz.T, color="C3", marker="^", linestyle="") ax.grid(True) ax.set_xlabel("x1") ax.set_ylabel("x2") return ax def __plot_image_curv( self, v, v_type="CC", grid=False, view="real", ax=None, pcolor_opts=None, grid_opts=None, range_x=None, range_y=None, **kwargs, ): if self.dim == 3: raise NotImplementedError("This is not yet done!") if view == "vec": raise NotImplementedError( "Vector ploting is not supported on CurvilinearMesh (yet)" ) if view in ["real", "imag", "abs"]: v = getattr(np, view)(v) # e.g. np.real(v) if v_type == "CC": I = v elif v_type == "N": I = self.aveN2CC * v elif v_type in ["Fx", "Fy", "Ex", "Ey"]: aveOp = "ave" + v_type[0] + "2CCV" ind_xy = {"x": 0, "y": 1}[v_type[1]] I = (getattr(self, aveOp) * v).reshape(2, self.nC)[ ind_xy ] # average to cell centers I = np.ma.masked_where(np.isnan(I), I) X, Y = (x.T for x in self.node_list) out = ax.pcolormesh( X, Y, I.reshape(self.vnC[::-1]), antialiased=True, **pcolor_opts, **grid_opts, ) ax.set_xlabel("x") ax.set_ylabel("y") return (out,) def __plot_grid_tree( self, ax=None, nodes=False, faces=False, centers=False, edges=False, lines=True, cell_line=False, faces_x=False, faces_y=False, faces_z=False, edges_x=False, edges_y=False, edges_z=False, **kwargs, ): from matplotlib.collections import LineCollection # Lazy loaded from mpl_toolkits.mplot3d.art3d import Line3DCollection # Lazy loaded if faces: faces_x = faces_y = True if self.dim == 3: faces_z = True if edges: edges_x = edges_y = True if self.dim == 3: edges_z = True if lines or nodes: grid_n_full = np.r_[self.nodes, self.hanging_nodes] if nodes: ax.plot(*grid_n_full.T, color="C0", marker="s", linestyle="") # Hanging Nodes ax.plot( *self.gridhN.T, color="C0", marker="s", linestyle="", markersize=10, markerfacecolor="none", markeredgecolor="C0", ) if centers: ax.plot(*self.cell_centers.T, color="C1", marker="o", linestyle="") if cell_line: ax.plot(*self.cell_centers.T, color="C1", linestyle=":") ax.plot( self.cell_centers[[0, -1], 0], self.cell_centers[[0, -1], 1], color="C1", marker="o", linestyle="", ) y_mark = "<" if self.dim == 3 else "^" if faces_x: ax.plot( *np.r_[self.faces_x, self.hanging_faces_x].T, color="C2", marker=">", linestyle="", ) # Hanging Faces x ax.plot( *self.hanging_faces_x.T, color="C2", marker="s", linestyle="", markersize=10, markerfacecolor="none", markeredgecolor="C2", ) if faces_y: ax.plot( *np.r_[self.faces_y, self.hanging_faces_y].T, color="C2", marker=y_mark, linestyle="", ) # Hanging Faces y ax.plot( *self.hanging_faces_y.T, color="C2", marker="s", linestyle="", markersize=10, markerfacecolor="none", markeredgecolor="C2", ) if faces_z: ax.plot( *np.r_[self.faces_z, self.hanging_faces_z].T, color="C2", marker="^", linestyle="", ) # Hangin Faces z ax.plot( *self.hanging_faces_z.T, color="C2", marker="s", linestyle="", markersize=10, markerfacecolor="none", markeredgecolor="C2", ) if edges_x: ax.plot( *np.r_[self.edges_x, self.hanging_edges_x].T, color="C3", marker=">", linestyle="", ) # Hanging Edges x ax.plot( *self.hanging_edges_x.T, color="C3", marker="s", linestyle="", markersize=10, markerfacecolor="none", markeredgecolor="C3", ) if edges_y: ax.plot( *np.r_[self.edges_y, self.hanging_edges_y].T, color="C3", marker=y_mark, linestyle="", ) # Hanging Edges y ax.plot( *self.hanging_edges_y.T, color="C3", marker="s", linestyle="", markersize=10, markerfacecolor="none", markeredgecolor="C3", ) if edges_z: ax.plot( *np.r_[self.edges_z, self.hanging_edges_z].T, color="C3", marker="^", linestyle="", ) # Hanging Edges z ax.plot( *self.hanging_edges_z.T, color="C3", marker="s", linestyle="", markersize=10, markerfacecolor="none", markeredgecolor="C3", ) if lines: edge_nodes = self.edge_nodes lines = np.r_[grid_n_full[edge_nodes[0]], grid_n_full[edge_nodes[1]]] if self.dim == 2: line_segments = LineCollection(lines, **kwargs) else: lines = np.r_[lines, grid_n_full[edge_nodes[2]]] line_segments = Line3DCollection(lines, **kwargs) ax.add_collection(line_segments) ax.autoscale() ax.set_xlabel("x1") ax.set_ylabel("x2") if self.dim == 3: ax.set_zlabel("x3") ax.grid(True) return ax def __plot_image_tree( self, v, v_type="CC", grid=False, view="real", ax=None, pcolor_opts=None, grid_opts=None, range_x=None, range_y=None, quiver_opts=None, **kwargs, ): from matplotlib.collections import PolyCollection # lazy loaded if self.dim == 3: raise NotImplementedError( "plot_image is not implemented for 3D TreeMesh, please use plot_slice" ) # reshape to cell_centered thing if v_type == "CC": pass if v_type == "CCv": if view != "vec": raise ValueError("Other types for CCv not supported") elif v_type in ["F", "E", "N"]: aveOp = aveOp = "ave" + v_type + ("2CCV" if view == "vec" else "2CC") v = getattr(self, aveOp) * v if view == "vec": v = v.reshape((self.n_cells, 2), order="F") elif v_type in ["Fx", "Fy", "Ex", "Ey"]: aveOp = "ave" + v_type[0] + "2CCV" v = getattr(self, aveOp) * v ind_xy = {"x": 0, "y": 1}[v_type[1]] v = v.reshape(2, self.n_cells)[ind_xy] if view in ["real", "imag", "abs"]: I = getattr(np, view)(v) # e.g. np.real(v) elif view == "vec": I = np.linalg.norm(v, axis=1) isnot_nans = ~np.isnan(I) # pcolormesh call signature # def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, # vmax=None, shading='flat', antialiased=False, **kwargs): # make a shallow copy so we can pop items off for the pass to PolyCollection pcolor_opts = pcolor_opts.copy() alpha = pcolor_opts.pop("alpha", None) norm = pcolor_opts.pop("norm", None) cmap = pcolor_opts.pop("cmap", None) vmin = pcolor_opts.pop("vmin", None) vmax = pcolor_opts.pop("vmax", None) pcolor_opts.pop("shading", "flat") # polycollection does not support shading antialiased = pcolor_opts.pop("antialiased", False) node_grid = np.r_[self.nodes, self.hanging_nodes] cell_nodes = self.cell_nodes[:, (0, 1, 3, 2)] cell_nodes = cell_nodes[isnot_nans] cell_verts = node_grid[cell_nodes] # Below taken from pcolormesh source code with QuadMesh exchanged to PolyCollection collection = PolyCollection( cell_verts, antialiased=antialiased, **{**pcolor_opts, **grid_opts} ) collection.set_alpha(alpha) collection.set_array(I[isnot_nans]) collection.set_cmap(cmap) collection.set_norm(norm) try: collection._scale_norm(norm, vmin, vmax) except AttributeError: collection.set_clim(vmin, vmax) collection.autoscale_None() ax.grid(False) ax.add_collection(collection, autolim=False) if range_x is not None: minx, maxx = range_x else: minx, maxx = self.nodes_x[[0, -1]] if range_y is not None: miny, maxy = range_y else: miny, maxy = self.nodes_y[[0, -1]] collection.sticky_edges.x[:] = [minx, maxx] collection.sticky_edges.y[:] = [miny, maxy] corners = (minx, miny), (maxx, maxy) ax.update_datalim(corners) ax._request_autoscale_view() out = (collection,) if view == "vec": if quiver_opts is None: quiver_opts = {} # make a copy so we can set some defaults without modifying the original quiver_opts = quiver_opts.copy() quiver_opts.setdefault("pivot", "mid") v = v.reshape(2, self.n_cells) qvr = ax.quiver( self.cell_centers[:, 0], self.cell_centers[:, 1], v[0], v[1], **quiver_opts, ) out = (collection, qvr) return out def __plot_slice_tree( self, v, v_type="CC", normal="Z", ind=None, grid=False, view="real", ax=None, pcolor_opts=None, stream_opts=None, grid_opts=None, range_x=None, range_y=None, quiver_opts=None, **kwargs, ): normalInd = {"X": 0, "Y": 1, "Z": 2}[normal] antiNormalInd = {"X": [1, 2], "Y": [0, 2], "Z": [0, 1]}[normal] h2d = (self.h[antiNormalInd[0]], self.h[antiNormalInd[1]]) x2d = (self.origin[antiNormalInd[0]], self.origin[antiNormalInd[1]]) #: Size of the sliced dimension szSliceDim = len(self.h[normalInd]) if ind is None: ind = szSliceDim // 2 if not isinstance(ind, (np.integer, int)): raise ValueError("ind must be an integer") cc_tensor = [self.cell_centers_x, self.cell_centers_y, self.cell_centers_z] slice_loc = cc_tensor[normalInd][ind] slice_origin = self.origin.copy() slice_origin[normalInd] = slice_loc normal = [0, 0, 0] normal[normalInd] = 1 # create a temporary TreeMesh with the slice through temp_mesh = discretize.TreeMesh(h2d, x2d, diagonal_balance=False) level_diff = self.max_level - temp_mesh.max_level # get list of cells which intersect the slicing plane inds = self.get_cells_on_plane(slice_origin, normal) levels = self._cell_levels_by_indexes(inds) - level_diff grid2d = self.cell_centers[inds][:, antiNormalInd] temp_mesh.insert_cells(grid2d, levels) tm_gridboost = np.empty((temp_mesh.n_cells, 3)) tm_gridboost[:, antiNormalInd] = temp_mesh.cell_centers tm_gridboost[:, normalInd] = slice_loc # interpolate values to self.gridCC if not "CC" or "CCv" if v_type[:2] != "CC": aveOp = "ave" + v_type + "2CC" if view == "vec": aveOp += "V" Av = getattr(self, aveOp) if v.shape[0] == Av.shape[1]: v = Av * v if view == "vec": v = v.reshape((self.n_cells, 3), order="F") elif len(v_type) == 2: # was one of Fx, Fy, Fz, Ex, Ey, Ez # assuming v has all three components in these cases vec_ind = {"x": 0, "y": 1, "z": 2}[v_type[1]] if v_type[0] == "E": i_s = np.cumsum([0, self.nEx, self.nEy, self.nEz]) elif v_type[0] == "F": i_s = np.cumsum([0, self.nFx, self.nFy, self.nFz]) v = v[i_s[vec_ind] : i_s[vec_ind + 1]] v = Av * v elif v_type == "CCv": if view != "vec": raise ValueError("Other types for CCv not supported") slice_view = view if view == "vec": slice_view = "real" vecs = v[:, antiNormalInd] v = np.linalg.norm(v, axis=1) # interpolate values from self.gridCC to grid2d ind_3d_to_2d = self.get_containing_cells(tm_gridboost) v2d = v[ind_3d_to_2d] out = temp_mesh.plot_image( v2d, v_type="CC", grid=grid, view=slice_view, ax=ax, pcolor_opts=pcolor_opts, grid_opts=grid_opts, range_x=range_x, range_y=range_y, ) ax.set_xlabel("y" if normal == "X" else "x") ax.set_ylabel("y" if normal == "Z" else "z") ax.set_title("Slice {0:d}, {1!s} = {2:4.2f}".format(ind, normal, slice_loc)) if view == "vec": if quiver_opts is None: quiver_opts = {} # make a copy so we can set some defaults without modifying the original quiver_opts = quiver_opts.copy() quiver_opts.setdefault("pivot", "mid") vecs = vecs[ind_3d_to_2d] qvr = ax.quiver( temp_mesh.cell_centers[:, 0], temp_mesh.cell_centers[:, 1], vecs[:, 0], vecs[:, 1], **quiver_opts, ) out = ( out, qvr, ) return out def __plot_grid_simp( self, ax=None, nodes=False, faces=False, centers=False, edges=False, lines=True, show_it=False, color="C0", linewidth=1.0, ): if lines: if self.dim == 2: ax.triplot( *self.nodes.T, self.simplices, color=color, linewidth=linewidth ) elif self.dim == 3: edge_nodes = self._edges n_edges = edge_nodes.shape[0] to_plot = np.full((3 * n_edges, 3), np.nan) to_plot[::3] = self.nodes[edge_nodes[:, 0]] to_plot[1::3] = self.nodes[edge_nodes[:, 1]] ax.plot(*to_plot.T, color=color, linewidth=linewidth) if nodes: ax.plot(*self.nodes.T, color="C0", marker="s", linestyle="") if centers: ax.plot(*self.cell_centers.T, color="C1", marker="o", linestyle="") if faces: ax.plot(*self.faces.T, color="C2", marker=">", linestyle="") if edges: ax.plot(*self.edges.T, color="C3", marker="^", linestyle="") ax.set_xlabel("x1") ax.set_ylabel("x2") if self.dim == 3: ax.set_zlabel("x3") return ax def __plot_image_simp( self, v, v_type="CC", grid=False, view="real", ax=None, pcolor_opts=None, grid_opts=None, range_x=None, range_y=None, quiver_opts=None, **kwargs, ): if self.dim == 3: raise NotImplementedError( "plot_image is not implemented for 3D SimplexMesh." ) v = np.squeeze(v) # reshape to cell_centered thing if v_type == "CCv": if view != "vec": raise ValueError("Other types for CCv not supported") if "F" in v_type: aveOp = "average_face_to_cell" if view == "vec" or "x" in v_type or "y" in v_type: aveOp += "_vector" v = getattr(self, aveOp) * v elif "E" in v_type: aveOp = "average_edge_to_cell" if view == "vec" or "x" in v_type or "y" in v_type: aveOp += "_vector" v = getattr(self, aveOp) * v if view == "vec": v = v.reshape((self.n_cells, 2), order="F") elif "x" in v_type: v = v.reshape((self.n_cells, 2), order="F") v = v[:, 0] elif "y" in v_type: v = v.reshape((self.n_cells, 2), order="F") v = v[:, 1] if view in ["real", "imag", "abs"]: image_data = getattr(np, view)(v) # e.g. np.real(v) elif view == "vec": image_data = np.linalg.norm(v, axis=1) shading = "gouraud" if v_type == "N" else "flat" trip = ax.tripcolor( *self.nodes.T, self._simplices, image_data, shading=shading, **pcolor_opts ) if range_x is None: range_x = (self.nodes[:, 0].min(), self.nodes[:, 0].max()) if range_y is None: range_y = (self.nodes[:, 1].min(), self.nodes[:, 1].max()) ax.set_xlabel("x") ax.set_ylabel("y") ax.set_xlim(*range_x) ax.set_ylim(*range_y) out = (trip,) if view == "vec": if quiver_opts is None: quiver_opts = {} # make a copy so we can set some defaults without modifying the original quiver_opts = quiver_opts.copy() quiver_opts.setdefault("pivot", "mid") qvr = ax.quiver( self.cell_centers[:, 0], self.cell_centers[:, 1], v[:, 0], v[:, 1], **quiver_opts, ) out = (trip, qvr) return out plotGrid = deprecate_method( "plot_grid", "plotGrid", removal_version="1.0.0", future_warn=True ) plotImage = deprecate_method( "plot_image", "plotImage", removal_version="1.0.0", future_warn=True ) plotSlice = deprecate_method( "plot_slice", "plotSlice", removal_version="1.0.0", future_warn=True ) class Slicer(object): """Plot slices of a 3D volume, interactively (scroll wheel). If called from a notebook, make sure to set %matplotlib notebook Parameters ---------- v : (n_cells) numpy.ndarray Data array xslice, yslice, zslice : float, optional Initial slice locations (in meter); defaults to the middle of the volume. v_type: {'CC', 'Fx', 'Fy', 'Fz', 'Ex', 'Ey', 'Ez'} Type of visualization. view : {'real', 'imag', 'abs'} Which component to show. axis : {'xy', 'yx'} 'xy': horizontal axis is x, vertical axis is y. Reversed otherwise. transparent : 'slider' or list of float or pairs of float, optional Values to be removed. E.g. air, water. If single value, only exact matches are removed. Pairs are treated as ranges. E.g. [0.3, [1, 4], [-np.infty, -10]] removes all values equal to 0.3, all values between 1 and 4, and all values smaller than -10. If 'slider' is provided it will plot an interactive slider to choose the shown range. clim : None or list of [min, max] For `pcolormesh` (`vmin`, `vmax`). Note: if you use a `norm` (e.g., `LogNorm`) then `vmin`/`vmax` have to be provided in the norm. xlim, ylim, zlim : None or list of [min, max] Axis limits. aspect : 'auto', 'equal', or num Aspect ratio of subplots. Defaults to 'auto'. A list of two values can be provided. The first will be for the XY-plot, the second for the XZ- and YZ-plots, e.g. ['equal', 2] to have the vertical dimension exaggerated by a factor of 2. WARNING: For anything else than 'auto', unexpected things might happen when zooming, and the subplot-arrangement won't look pretty. grid : (3) list of int Number of cells occupied by x, y, and z dimension on plt.subplot2grid. pcolor_opts : dictionary Passed to `pcolormesh`. Examples -------- The straight forward usage for the Slicer is through, e.g., a `TensorMesh`-mesh, by accessing its `mesh.plot_3d_slicer`. If you, however, call this class directly, you have first to initiate a figure, and afterwards connect it: >>> fig = plt.figure() Then you have to get the tracker from the Slicer >>> tracker = discretize.View.Slicer(mesh, Lpout) Finally you have to connect the tracker to the figure >>> fig.canvas.mpl_connect('scroll_event', tracker.onscroll) >>> plt.show() """ def __init__( self, mesh, v, xslice=None, yslice=None, zslice=None, v_type="CC", view="real", axis="xy", transparent=None, clim=None, xlim=None, ylim=None, zlim=None, aspect="auto", grid=(2, 2, 1), pcolor_opts=None, **kwargs, ): """Initialize interactive figure.""" _, plt = load_matplotlib() from matplotlib.widgets import Slider # Lazy loaded from matplotlib.colors import Normalize # 0. Some checks, not very extensive if "pcolorOpts" in kwargs: pcolor_opts = kwargs["pcolorOpts"] warnings.warn( "pcolorOpts has been deprecated, please use pcolor_opts", FutureWarning, stacklevel=2, ) # Add pcolor_opts to self self.pc_props = pcolor_opts if pcolor_opts is not None else {} # (a) Mesh dimensionality if mesh.dim != 3: err = "Must be a 3D mesh. Use plot_image instead." err += " Mesh provided has {} dimension(s).".format(mesh.dim) raise ValueError(err) # (b) v_type # Not yet working for ['CCv'] v_typeOpts = ["CC", "Fx", "Fy", "Fz", "Ex", "Ey", "Ez"] if v_type not in v_typeOpts: err = "v_type must be in ['{0!s}'].".format("', '".join(v_typeOpts)) err += " v_type provided: '{0!s}'.".format(v_type) raise ValueError(err) if v_type != "CC": aveOp = "ave" + v_type + "2CC" Av = getattr(mesh, aveOp) if v.size == Av.shape[1]: v = Av * v else: v = mesh.reshape(v, v_type[0], v_type) # get specific component v = Av * v # (c) vOpts # Not yet working for 'vec' # Backwards compatibility if view in ["xy", "yx"]: axis = view view = "real" viewOpts = ["real", "imag", "abs"] if view in viewOpts: v = getattr(np, view)(v) # e.g. np.real(v) else: err = "view must be in ['{0!s}'].".format("', '".join(viewOpts)) err += " view provided: '{0!s}'.".format(view) raise ValueError(err) # 1. Store relevant data # Store data in self as (nx, ny, nz) self.v = mesh.reshape(v.reshape((mesh.nC, -1), order="F"), "CC", "CC", "M") self.v = np.ma.masked_array(self.v, np.isnan(self.v)) # Store relevant information from mesh in self self.x = mesh.nodes_x # x-node locations self.y = mesh.nodes_y # y-node locations self.z = mesh.nodes_z # z-node locations self.xc = mesh.cell_centers_x # x-cell center locations self.yc = mesh.cell_centers_y # y-cell center locations self.zc = mesh.cell_centers_z # z-cell center locations # Axis: Default ('xy'): horizontal axis is x, vertical axis is y. # Reversed otherwise. self.yx = axis == "yx" # Store initial slice indices; if not provided, takes the middle. if xslice is not None: self.xind = np.argmin(np.abs(self.xc - xslice)) else: self.xind = self.xc.size // 2 if yslice is not None: self.yind = np.argmin(np.abs(self.yc - yslice)) else: self.yind = self.yc.size // 2 if zslice is not None: self.zind = np.argmin(np.abs(self.zc - zslice)) else: self.zind = self.zc.size // 2 # Aspect ratio if isinstance(aspect, (list, tuple)): aspect1 = aspect[0] aspect2 = aspect[1] else: aspect1 = aspect aspect2 = aspect if aspect2 in ["auto", "equal"]: aspect3 = aspect2 else: aspect3 = 1.0 / aspect2 # Ensure a consistent color normalization for the three plots. if (norm := self.pc_props.get("norm", None)) is None: # Create a default normalizer norm = Normalize() if clim is not None: norm.vmin, norm.vmax = clim self.pc_props["norm"] = norm else: if clim is not None: raise ValueError( "Passing a Normalize instance simultaneously with clim is not supported. " "Please pass vmin/vmax directly to the norm when creating it." ) # Auto scales None values for norm.vmin and norm.vmax. norm.autoscale_None(self.v[~self.v.mask].reshape(-1, order="A")) # 2. Start populating figure # Get plot2grid dimension figgrid = (grid[0] + grid[2], grid[1] + grid[2]) # Create subplots self.fig = plt.gcf() self.fig.subplots_adjust(wspace=0.075, hspace=0.1) # To capture mouse scroll in notebooks (otherwise it is hard to slice through volume) self.fig.canvas.capture_scroll = True # X-Y self.ax1 = plt.subplot2grid( figgrid, (0, 0), colspan=grid[1], rowspan=grid[0], aspect=aspect1 ) if self.yx: self.ax1.set_ylabel("x") if ylim is not None: self.ax1.set_xlim([ylim[0], ylim[1]]) if xlim is not None: self.ax1.set_ylim([xlim[0], xlim[1]]) else: self.ax1.set_ylabel("y") if xlim is not None: self.ax1.set_xlim([xlim[0], xlim[1]]) if ylim is not None: self.ax1.set_ylim([ylim[0], ylim[1]]) self.ax1.xaxis.set_ticks_position("top") plt.setp(self.ax1.get_xticklabels(), visible=False) # X-Z self.ax2 = plt.subplot2grid( figgrid, (grid[0], 0), colspan=grid[1], rowspan=grid[2], sharex=self.ax1, aspect=aspect2, ) self.ax2.yaxis.set_ticks_position("both") if self.yx: self.ax2.set_xlabel("y") if ylim is not None: self.ax2.set_xlim([ylim[0], ylim[1]]) else: self.ax2.set_xlabel("x") if xlim is not None: self.ax2.set_xlim([xlim[0], xlim[1]]) self.ax2.set_ylabel("z") if zlim is not None: self.ax2.set_ylim([zlim[0], zlim[1]]) # Z-Y self.ax3 = plt.subplot2grid( figgrid, (0, grid[1]), colspan=grid[2], rowspan=grid[0], sharey=self.ax1, aspect=aspect3, ) self.ax3.yaxis.set_ticks_position("right") self.ax3.xaxis.set_ticks_position("both") self.ax3.invert_xaxis() plt.setp(self.ax3.get_yticklabels(), visible=False) if self.yx: if xlim is not None: self.ax3.set_ylim([xlim[0], xlim[1]]) else: if ylim is not None: self.ax3.set_ylim([ylim[0], ylim[1]]) if zlim is not None: self.ax3.set_xlim([zlim[1], zlim[0]]) # Cross-line properties # We have two lines, a thick white one, and in the middle a thin black # one, to assure that the lines can be seen on dark and on bright # spots. self.clpropsw = {"c": "w", "lw": 2, "zorder": 10} self.clpropsk = {"c": "k", "lw": 1, "zorder": 11} # Initial draw self.update_xy() self.update_xz() self.update_zy() # Create colorbar plt.colorbar(self.zy_pc, pad=0.15) # Remove transparent value if isinstance(transparent, str) and transparent.lower() == "slider": clim = (norm.vmin, norm.vmax) # Sliders self.ax_smin = plt.axes([0.7, 0.11, 0.15, 0.03]) self.ax_smax = plt.axes([0.7, 0.15, 0.15, 0.03]) # Limits slightly below/above actual limits, clips otherwise self.smin = Slider(self.ax_smin, "Min", *clim, valinit=clim[0]) self.smax = Slider(self.ax_smax, "Max", *clim, valinit=clim[1]) def update(val): self.v.mask = False # Re-set self.v = np.ma.masked_outside(self.v.data, self.smin.val, self.smax.val) # Update plots self.update_xy() self.update_xz() self.update_zy() self.smax.on_changed(update) self.smin.on_changed(update) elif transparent is not None: # Loop over values for value in transparent: # If value is a list/tuple, we treat is as a range if isinstance(value, (list, tuple)): self.v = np.ma.masked_inside(self.v, value[0], value[1]) else: # Exact value self.v = np.ma.masked_equal(self.v, value) # Update plots self.update_xy() self.update_xz() self.update_zy() # 3. Keep depth in X-Z and Z-Y in sync def do_adjust(): """Return True if z-axis in X-Z and Z-Y are different.""" one = np.array(self.ax2.get_ylim()) two = np.array(self.ax3.get_xlim())[::-1] return sum(abs(one - two)) > 0.001 # Difference at least 1 m. def on_ylims_changed(ax): """Adjust Z-Y if X-Z changed.""" if do_adjust(): self.ax3.set_xlim([self.ax2.get_ylim()[1], self.ax2.get_ylim()[0]]) def on_xlims_changed(ax): """Adjust X-Z if Z-Y changed.""" if do_adjust(): self.ax2.set_ylim([self.ax3.get_xlim()[1], self.ax3.get_xlim()[0]]) self.ax3.callbacks.connect("xlim_changed", on_xlims_changed) self.ax2.callbacks.connect("ylim_changed", on_ylims_changed) def onscroll(self, event): """Update index and data when scrolling.""" _, plt = load_matplotlib() # Get scroll direction if event.button == "up": pm = 1 else: pm = -1 # Update slice index depending on subplot over which mouse is if event.inaxes == self.ax1: # X-Y self.zind = (self.zind + pm) % self.zc.size self.update_xy() elif event.inaxes == self.ax2: # X-Z if self.yx: self.xind = (self.xind + pm) % self.xc.size else: self.yind = (self.yind + pm) % self.yc.size self.update_xz() elif event.inaxes == self.ax3: # Z-Y if self.yx: self.yind = (self.yind + pm) % self.yc.size else: self.xind = (self.xind + pm) % self.xc.size self.update_zy() plt.draw() def update_xy(self): """Update plot for change in Z-index.""" # Clean up self._clear_elements(["xy_pc", "xz_ahw", "xz_ahk", "zy_avw", "zy_avk"]) # Draw X-Y slice if self.yx: zdat = np.rot90(self.v[:, :, self.zind].transpose()) hor = self.y ver = self.x else: zdat = self.v[:, :, self.zind].transpose() hor = self.x ver = self.y self.xy_pc = self.ax1.pcolormesh(hor, ver, zdat, **self.pc_props) # Draw Z-slice intersection in X-Z plot self.xz_ahw = self.ax2.axhline(self.zc[self.zind], **self.clpropsw) self.xz_ahk = self.ax2.axhline(self.zc[self.zind], **self.clpropsk) # Draw Z-slice intersection in Z-Y plot self.zy_avw = self.ax3.axvline(self.zc[self.zind], **self.clpropsw) self.zy_avk = self.ax3.axvline(self.zc[self.zind], **self.clpropsk) def update_xz(self): """Update plot for change in Y-index.""" # Clean up self._clear_elements(["xz_pc", "zy_ahk", "zy_ahw", "xy_ahk", "xy_ahw"]) # Draw X-Z slice if self.yx: ydat = self.v[-self.xind, :, :].transpose() hor = self.y ver = self.z ind = self.xc[self.xind] else: ydat = self.v[:, self.yind, :].transpose() hor = self.x ver = self.z ind = self.yc[self.yind] self.xz_pc = self.ax2.pcolormesh(hor, ver, ydat, **self.pc_props) # Draw X-slice intersection in X-Y plot self.xy_ahw = self.ax1.axhline(ind, **self.clpropsw) self.xy_ahk = self.ax1.axhline(ind, **self.clpropsk) # Draw X-slice intersection in Z-Y plot self.zy_ahw = self.ax3.axhline(ind, **self.clpropsw) self.zy_ahk = self.ax3.axhline(ind, **self.clpropsk) def update_zy(self): """Update plot for change in X-index.""" # Clean up self._clear_elements(["zy_pc", "xz_avw", "xz_avk", "xy_avw", "xy_avk"]) # Draw Z-Y slice if self.yx: xdat = np.flipud(self.v[:, self.yind, :]) hor = self.z ver = self.x ind = self.yc[self.yind] else: xdat = self.v[self.xind, :, :] hor = self.z ver = self.y ind = self.xc[self.xind] self.zy_pc = self.ax3.pcolormesh(hor, ver, xdat, **self.pc_props) # Draw Y-slice intersection in X-Y plot self.xy_avw = self.ax1.axvline(ind, **self.clpropsw) self.xy_avk = self.ax1.axvline(ind, **self.clpropsk) # Draw Y-slice intersection in X-Z plot self.xz_avw = self.ax2.axvline(ind, **self.clpropsw) self.xz_avk = self.ax2.axvline(ind, **self.clpropsk) def _clear_elements(self, names): """Remove elements from list from plot if they exists.""" for element in names: if hasattr(self, element): getattr(self, element).remove() ================================================ FILE: discretize/mixins/omf_mod.py ================================================ """Module for ``omf`` interaction with ``discretize``.""" import numpy as np import discretize def omf(): """Lazy loading omf.""" import omf return omf def _ravel_data_array(arr, nx, ny, nz): """Ravel an array from discretize ordering to omf ordering. Converts a 1D numpy array from ``discretize`` ordering (x, y, z) to a flattened 1D numpy array with ``OMF`` ordering (z, y, x) In ``discretize``, three-dimensional data are frequently organized within a 1D numpy array whose elements are ordered along the x-axis, then the y-axis, then the z-axis. **_ravel_data_array** converts the input array (discretize format) to a 1D numpy array ordered according to the open mining format; which is ordered along the z-axis, then the y-axis, then the x-axis. Parameters ---------- arr : numpy.ndarray A 1D vector or nD array ordered along the x, then y, then z axes nx : int Number of cells along the x-axis ny : int Number of cells along the y-axis nz : int Number of cells along the z-axis Returns ------- numpy.ndarray (n_cells) A flattened 1D array ordered according to the open mining format Examples -------- To demonstrate the reordering, we design a small 3D tensor mesh. We print a numpy array with the xyz locations of cell the centers using the original ordering (discretize). We then re-order the cell locations according to OMF. >>> from discretize import TensorMesh >>> import numpy as np >>> hx = np.ones(4) >>> hy = 2*np.ones(3) >>> hz = 3*np.ones(2) >>> mesh = TensorMesh([hx, hy, hz]) >>> dim = mesh.shape_cells[::-1] # OMF orderting >>> xc = np.reshape(mesh.cell_centers[:, 0], dim, order="C").ravel(order="F") >>> yc = np.reshape(mesh.cell_centers[:, 1], dim, order="C").ravel(order="F") >>> zc = np.reshape(mesh.cell_centers[:, 2], dim, order="C").ravel(order="F") >>> mesh.cell_centers array([[0.5, 1. , 1.5], [1.5, 1. , 1.5], [2.5, 1. , 1.5], [3.5, 1. , 1.5], [0.5, 3. , 1.5], [1.5, 3. , 1.5], [2.5, 3. , 1.5], [3.5, 3. , 1.5], [0.5, 5. , 1.5], [1.5, 5. , 1.5], [2.5, 5. , 1.5], [3.5, 5. , 1.5], [0.5, 1. , 4.5], [1.5, 1. , 4.5], [2.5, 1. , 4.5], [3.5, 1. , 4.5], [0.5, 3. , 4.5], [1.5, 3. , 4.5], [2.5, 3. , 4.5], [3.5, 3. , 4.5], [0.5, 5. , 4.5], [1.5, 5. , 4.5], [2.5, 5. , 4.5], [3.5, 5. , 4.5]]) >>> np.c_[xc, yc, zc] array([[0.5, 1. , 1.5], [0.5, 1. , 4.5], [0.5, 3. , 1.5], [0.5, 3. , 4.5], [0.5, 5. , 1.5], [0.5, 5. , 4.5], [1.5, 1. , 1.5], [1.5, 1. , 4.5], [1.5, 3. , 1.5], [1.5, 3. , 4.5], [1.5, 5. , 1.5], [1.5, 5. , 4.5], [2.5, 1. , 1.5], [2.5, 1. , 4.5], [2.5, 3. , 1.5], [2.5, 3. , 4.5], [2.5, 5. , 1.5], [2.5, 5. , 4.5], [3.5, 1. , 1.5], [3.5, 1. , 4.5], [3.5, 3. , 1.5], [3.5, 3. , 4.5], [3.5, 5. , 1.5], [3.5, 5. , 4.5]]) """ dim = (nz, ny, nx) return np.reshape(arr, dim, order="C").ravel(order="F") def _unravel_data_array(arr, nx, ny, nz): """Unravel an array from omf ordering to discretize ordering. Converts a 1D numpy array from ``OMF`` ordering (z, y, x) to a flattened 1D numpy array with ``discretize`` ordering (x, y, z) In ``OMF``, three-dimensional data are organized within a 1D numpy array whose elements are ordered along the z-axis, then the y-axis, then the x-axis. **_unravel_data_array** converts the input array (OMF format) to a 1D numpy array ordered according to ``discretize``; which is ordered along the x-axis, then the y-axis, then the y-axis. Parameters ---------- arr : numpy.ndarray A 1D vector or nD array ordered along the z, then y, then x axes nx : int Number of cells along the x-axis ny : int Number of cells along the y-axis nz : int Number of cells along the z-axis Returns ------- (n_cells) numpy.ndarray A flattened 1D array ordered according to the discretize format """ dim = (nz, ny, nx) return np.reshape(arr, dim, order="F").ravel(order="C") class InterfaceOMF(object): """Convert between ``omf`` and ``discretize`` objects. The ``InterfaceOMF`` class was designed for easy conversion between ``discretize`` objects and `open mining format `__ (OMF) objects. Examples include: meshes, models and data arrays. """ def _tensor_mesh_to_omf(mesh, models=None): """Convert a TensorMesh to an omf object. Constructs an :class:`omf.VolumeElement` object of this tensor mesh and the given models as cell data of that grid. Parameters ---------- mesh : discretize.TensorMesh The tensor mesh to convert to a :class:`omf.VolumeElement` models : dict(numpy.ndarray) Name('s) and array('s). Match number of cells """ if models is None: models = {} # Make the geometry geometry = omf().VolumeGridGeometry() # Set tensors tensors = mesh.h if len(tensors) < 1: raise RuntimeError( "Your mesh is empty... fill it out before converting to OMF" ) elif len(tensors) == 1: geometry.tensor_u = tensors[0] geometry.tensor_v = np.array( [ 0.0, ] ) geometry.tensor_w = np.array( [ 0.0, ] ) elif len(tensors) == 2: geometry.tensor_u = tensors[0] geometry.tensor_v = tensors[1] geometry.tensor_w = np.array( [ 0.0, ] ) elif len(tensors) == 3: geometry.tensor_u = tensors[0] geometry.tensor_v = tensors[1] geometry.tensor_w = tensors[2] else: raise RuntimeError("This mesh is too high-dimensional for OMF") # Set rotation axes orientation = mesh.orientation geometry.axis_u = orientation[0] geometry.axis_v = orientation[1] geometry.axis_w = orientation[2] # Set the origin geometry.origin = mesh.origin # Make sure the geometry is built correctly geometry.validate() # Make the volume element (the OMF object) omfmesh = omf().VolumeElement( geometry=geometry, ) # Add model data arrays onto the cells of the mesh omfmesh.data = [] for name, arr in models.items(): data = omf().ScalarData( name=name, array=_ravel_data_array(arr, *mesh.shape_cells), location="cells", ) omfmesh.data.append(data) # Validate to make sure a proper OMF object is returned to the user omfmesh.validate() return omfmesh def _tree_mesh_to_omf(mesh, models=None): raise NotImplementedError("Not possible until OMF v2 is released.") def _curvilinear_mesh_to_omf(mesh, models=None): raise NotImplementedError("Not currently possible.") def _cyl_mesh_to_omf(mesh, models=None): raise NotImplementedError("Not currently possible.") def to_omf(mesh, models=None): """Convert to an ``omf`` data object. Convert this mesh object to its proper ``omf`` data object with the given model dictionary as the cell data of that dataset. Parameters ---------- models : dict of [str, (n_cells) numpy.ndarray], optional Name('s) and array('s). Returns ------- omf.volume.VolumeElement """ # TODO: mesh.validate() converters = { # TODO: 'tree' : InterfaceOMF._tree_mesh_to_omf, "tensor": InterfaceOMF._tensor_mesh_to_omf, # TODO: 'curv' : InterfaceOMF._curvilinear_mesh_to_omf, # TODO: 'CylindricalMesh' : InterfaceOMF._cyl_mesh_to_omf, } key = mesh._meshType.lower() try: convert = converters[key] except KeyError: raise RuntimeError( "Mesh type `{}` is not currently supported for OMF conversion.".format( key ) ) # Convert the data object return convert(mesh, models=models) @staticmethod def _omf_volume_to_tensor(element): """Convert an :class:`omf.VolumeElement` to :class:`discretize.TensorMesh`.""" geometry = element.geometry h = [geometry.tensor_u, geometry.tensor_v, geometry.tensor_w] orientation = np.array( [ geometry.axis_u, geometry.axis_v, geometry.axis_w, ] ) mesh = discretize.TensorMesh(h, origin=geometry.origin, orientation=orientation) data_dict = {} for data in element.data: # NOTE: this is agnostic about data location - i.e. nodes vs cells data_dict[data.name] = _unravel_data_array( np.array(data.array), *mesh.shape_cells ) # Return TensorMesh and data dictionary return mesh, data_dict @staticmethod def from_omf(element): """Convert an ``omf`` object to a ``discretize`` mesh. Convert an OMF element to it's proper ``discretize`` type. Automatically determines the output type. Returns both the mesh and a dictionary of model arrays. Parameters ---------- element : omf.volume.VolumeElement The open mining format volume element object Returns ------- mesh : discretize.TensorMesh The returned mesh type will be appropriately based on the input `element`. models : dict of [str, (n_cells) numpy.ndarray] The models contained in `element` Notes ----- Currently only :class:discretize.TensorMesh is supported. """ element.validate() converters = { omf().VolumeElement.__name__: InterfaceOMF._omf_volume_to_tensor, } key = element.__class__.__name__ try: convert = converters[key] except KeyError: raise RuntimeError( "OMF type `{}` is not currently supported for conversion.".format(key) ) # Convert the data object return convert(element) ================================================ FILE: discretize/mixins/vtk_mod.py ================================================ """Module for ``vtk`` interaction with ``discretize``. This module provides a way for ``discretize`` meshes to be converted to VTK data objects (and back when possible) if the `VTK Python package`_ is available. The :class:`discretize.mixins.vtk_mod.InterfaceVTK` class becomes inherrited by all mesh objects and allows users to directly convert any given mesh by calling that mesh's ``to_vtk()`` method (note that this method will not be available if VTK is not available). .. _`VTK Python package`: https://pypi.org/project/vtk/ This functionality was originally developed so that discretize could be interoperable with PVGeo_, providing a direct interface for discretize meshes within ParaView and other VTK powered platforms. This interoperablity allows users to visualize their finite volume meshes and model data from discretize along side all their other georeferenced datasets in a common rendering environment. .. _PVGeo: http://pvgeo.org .. _pyvista: http://docs.pyvista.org Another notable VTK powered software platforms is ``pyvista`` (see pyvista_ docs) which provides a direct interface to the VTK software library through accesible Python data structures and NumPy arrays:: pip install pyvista By default, the ``to_vtk()`` method will return a ``pyvista`` data object so that users can immediately start visualizing their data in 3D. See :ref:`pyvista_demo_ref` for an example of the types of integrated visualizations that are possible leveraging the link between discretize, pyvista_, and PVGeo_: .. image:: ../images/pyvista_laguna_del_maule.png :target: http://pvgeo.org :alt: PVGeo Example Visualization .. admonition:: Laguna del Maule Bouguer Gravity :class: note This data scene is was produced from the `Laguna del Maule Bouguer Gravity`_ example provided by Craig Miller (see Maule volcanic field, Chile. Refer to Miller et al 2016 EPSL for full details.) The rendering below shows several data sets and a model integrated together: * `Point Data`: the Bouguer gravity anomalies * Topography Surface * `Inverted Model`: The model has been both sliced and thresholded for low values .. _`Laguna del Maule Bouguer Gravity`: http://docs.simpeg.xyz/content/examples/20-published/plot_laguna_del_maule_inversion.html """ import os import numpy as np from discretize.utils import cyl2cart import warnings def load_vtk(extra=None): """Lazy load principal VTK routines. This is not beautiful. But if VTK is installed, but never used, it reduces load time significantly. """ import vtk as _vtk import vtk.util.numpy_support as _nps if extra: if isinstance(extra, str): return _vtk, _nps, getattr(_vtk, extra) else: return _vtk, _nps, [getattr(_vtk, e) for e in extra] else: return _vtk, _nps def assign_cell_data(vtkDS, models=None): """Assign the model(s) to the VTK dataset as ``CellData``. Parameters ---------- vtkDS : pyvista.Common Any given VTK data object that has cell data models : dict of str:numpy.ndarray Name('s) and array('s). Match number of cells """ _, _nps = load_vtk() nc = vtkDS.GetNumberOfCells() if models is not None: for name, mod in models.items(): # Convert numpy array if mod.shape[0] != nc: raise RuntimeError( 'Number of model cells ({}) (first axis of model array) for "{}" does not match number of mesh cells ({}).'.format( mod.shape[0], name, nc ) ) vtkDoubleArr = _nps.numpy_to_vtk(mod, deep=1) vtkDoubleArr.SetName(name) vtkDS.GetCellData().AddArray(vtkDoubleArr) return vtkDS class InterfaceVTK(object): """VTK interface for ``discretize`` meshes. Class enabling straight forward conversion between ``discretize`` meshes and their corresponding `VTK `__ or `PyVista `__ data objects. Since ``InterfaceVTK`` is inherritted by the :class:`~discretize.base.BaseMesh` class, this functionality can be called directly from any ``discretize`` mesh! Currently this functionality is implemented for :class:`~discretize.CurvilinearMesh`, :class:`~discretize.TreeMesh` and :class:`discretize.TensorMesh` classes; not implemented for :class:`~discretize.CylindricalMesh`. It should be noted that if your mesh is defined on a reference frame that is **not** the traditional system with vectors of :math:`(1,0,0)`, :math:`(0,1,0)`, and :math:`(0,0,1)`, then the mesh in VTK will be rotated so that it is plotted on the traditional reference frame; see examples below. Examples -------- The following are examples which use the VTK interface to convert discretize meshes to VTK data objects and write to VTK formatted files. In the first example example, a tensor mesh whose axes lie on the traditional reference frame is converted to a :class:`pyvista.RectilinearGrid` object. >>> import discretize >>> import numpy as np >>> h1 = np.linspace(.1, .5, 3) >>> h2 = np.linspace(.1, .5, 5) >>> h3 = np.linspace(.1, .8, 3) >>> mesh = discretize.TensorMesh([h1, h2, h3]) Get a VTK data object >>> dataset = mesh.to_vtk() Save this mesh to a VTK file >>> mesh.write_vtk('sample_mesh') Here, the reference frame of the mesh is rotated. In this case, conversion to VTK produces a :class:`pyvista.StructuredGrid` object. >>> axis_u = (1,-1,0) >>> axis_v = (-1,-1,0) >>> axis_w = (0,0,1) >>> mesh.orientation = np.array([ ... axis_u, ... axis_v, ... axis_w ... ]) Yield the rotated vtkStructuredGrid >>> dataset_r = mesh.to_vtk() or write it out to a VTK format >>> mesh.write_vtk('sample_rotated') The two above code snippets produced a :class:`pyvista.RectilinearGrid` and a :class:`pyvista.StructuredGrid` respecitvely. To demonstarte the difference, we have plotted the two datasets next to each other where the first mesh is in green and its data axes are parrallel to the traditional cartesian reference frame. The second, rotated mesh is shown in red and its data axes are rotated from the traditional Cartesian reference frame as specified by the *orientation* property. >>> import pyvista >>> pyvista.set_plot_theme('document') >>> p = pyvista.BackgroundPlotter() >>> p.add_mesh(dataset, color='green', show_edges=True) >>> p.add_mesh(dataset_r, color='maroon', show_edges=True) >>> p.show_grid() >>> p.screenshot('vtk-rotated-example.png') .. image:: ../../images/vtk-rotated-example.png """ def __tree_mesh_to_vtk(mesh, models=None): """Convert the TreeMesh to a vtk object. Constructs a :class:`pyvista.UnstructuredGrid` object of this tree mesh and the given models as ``cell_arrays`` of that ``pyvista`` dataset. Parameters ---------- mesh : discretize.TreeMesh The tree mesh to convert to a :class:`pyvista.UnstructuredGrid` models : dict(numpy.ndarray) Name('s) and array('s). Match number of cells """ _vtk, _nps = load_vtk() # Make the data parts for the vtu object # Points nodes = mesh.total_nodes # Adjust if result was 2D (voxels are pixels in 2D): VTK_CELL_TYPE = _vtk.VTK_VOXEL if mesh.dim == 3 else _vtk.VTK_PIXEL # Rotate the points to the cartesian system nodes = np.dot(nodes, mesh.rotation_matrix) if mesh.dim == 2: nodes = np.pad(nodes, ((0, 0), (0, 1))) # Grab the points vtkPts = _vtk.vtkPoints() vtkPts.SetData(_nps.numpy_to_vtk(nodes, deep=True)) # Cells cellConn = mesh.cell_nodes cellsMat = np.concatenate( (np.ones((cellConn.shape[0], 1), dtype=int) * cellConn.shape[1], cellConn), axis=1, ).ravel() cellsArr = _vtk.vtkCellArray() cellsArr.SetNumberOfCells(cellConn.shape[0]) cellsArr.SetCells( cellConn.shape[0], _nps.numpy_to_vtk(cellsMat, deep=True, array_type=_vtk.VTK_ID_TYPE), ) # Make the object output = _vtk.vtkUnstructuredGrid() output.SetPoints(vtkPts) output.SetCells(VTK_CELL_TYPE, cellsArr) # Add the level of refinement as a cell array cell_levels = mesh._cell_levels_by_indexes() refineLevelArr = _nps.numpy_to_vtk(cell_levels, deep=1) refineLevelArr.SetName("octreeLevel") output.GetCellData().AddArray(refineLevelArr) ubc_order = mesh._ubc_order # order_ubc will re-order from treemesh ordering to UBC ordering # need the opposite operation un_order = np.empty_like(ubc_order) un_order[ubc_order] = np.arange(len(ubc_order)) order = _nps.numpy_to_vtk(un_order) order.SetName("index_cell_corner") output.GetCellData().AddArray(order) # Assign the model('s) to the object return assign_cell_data(output, models=models) def __simplex_mesh_to_vtk(mesh, models=None): """Convert the SimplexMesh to a vtk object. Constructs a :class:`pyvista.UnstructuredGrid` object of this simplex mesh and the given models as ``cell_arrays`` of that ``pyvista`` dataset. Parameters ---------- mesh : discretize.SimplexMesh The simplex mesh to convert to a :class:`pyvista.UnstructuredGrid` models : dict(numpy.ndarray) Name('s) and array('s). Match number of cells """ _vtk, _nps = load_vtk() # Make the data parts for the vtu object # Points pts = mesh.nodes if mesh.dim == 2: cell_type = _vtk.VTK_TRIANGLE pts = np.c_[pts, np.zeros(mesh.n_nodes)] elif mesh.dim == 3: cell_type = _vtk.VTK_TETRA vtk_pts = _vtk.vtkPoints() vtk_pts.SetData(_nps.numpy_to_vtk(pts, deep=True)) cell_con_array = np.c_[np.full(mesh.n_cells, mesh.dim + 1), mesh.simplices] cells = _vtk.vtkCellArray() cells.SetNumberOfCells(mesh.n_cells) cells.SetCells( mesh.n_cells, _nps.numpy_to_vtk( cell_con_array.reshape(-1), deep=True, array_type=_vtk.VTK_ID_TYPE ), ) output = _vtk.vtkUnstructuredGrid() output.SetPoints(vtk_pts) output.SetCells(cell_type, cells) # Assign the model('s) to the object return assign_cell_data(output, models=models) @staticmethod def __create_structured_grid(ptsMat, dims, models=None): """Build a structured grid (an internal helper).""" _vtk, _nps = load_vtk() # Adjust if result was 2D: if ptsMat.shape[1] == 2: # Figure out which dim is null nullDim = dims.index(None) ptsMat = np.insert(ptsMat, nullDim, np.zeros(ptsMat.shape[0]), axis=1) if ptsMat.shape[1] != 3: raise RuntimeError("Points of the mesh are improperly defined.") # Convert the points vtkPts = _vtk.vtkPoints() vtkPts.SetData(_nps.numpy_to_vtk(ptsMat, deep=True)) # Uncover hidden dimension dims = tuple(0 if dim is None else dim + 1 for dim in dims) output = _vtk.vtkStructuredGrid() output.SetDimensions(dims[0], dims[1], dims[2]) # note this subtracts 1 output.SetPoints(vtkPts) # Assign the model('s) to the object return assign_cell_data(output, models=models) def __get_rotated_nodes(mesh): """Rotate mesh nodes (a helper routine).""" nodes = mesh.gridN if mesh.dim == 1: nodes = np.c_[mesh.gridN, np.zeros((mesh.nN, 2))] elif mesh.dim == 2: nodes = np.c_[mesh.gridN, np.zeros((mesh.nN, 1))] # Now garuntee nodes are correct if nodes.shape != (mesh.nN, 3): raise RuntimeError("Nodes of the grid are improperly defined.") # Rotate the points based on the axis orientations return np.dot(nodes, mesh.rotation_matrix) def __tensor_mesh_to_vtk(mesh, models=None): """Convert the TensorMesh to a vtk object. Constructs a :class:`pyvista.RectilinearGrid` (or a :class:`pyvista.StructuredGrid`) object of this tensor mesh and the given models as ``cell_arrays`` of that grid. If the mesh is defined on a normal cartesian system then a rectilinear grid is generated. Otherwise, a structured grid is generated. Parameters ---------- mesh : discretize.TensorMesh The tensor mesh to convert to a :class:`pyvista.RectilinearGrid` models : dict(numpy.ndarray) Name('s) and array('s). Match number of cells """ _vtk, _nps = load_vtk() # Deal with dimensionalities if mesh.dim >= 1: vX = mesh.nodes_x xD = len(vX) yD, zD = 1, 1 vY, vZ = np.array([0, 0]) if mesh.dim >= 2: vY = mesh.nodes_y yD = len(vY) if mesh.dim == 3: vZ = mesh.nodes_z zD = len(vZ) # If axis orientations are standard then use a vtkRectilinearGrid if not mesh.reference_is_rotated: # Use rectilinear VTK grid. # Assign the spatial information. output = _vtk.vtkRectilinearGrid() output.SetDimensions(xD, yD, zD) output.SetXCoordinates(_nps.numpy_to_vtk(vX, deep=1)) output.SetYCoordinates(_nps.numpy_to_vtk(vY, deep=1)) output.SetZCoordinates(_nps.numpy_to_vtk(vZ, deep=1)) return assign_cell_data(output, models=models) # Use a structured grid where points are rotated to the cartesian system ptsMat = InterfaceVTK.__get_rotated_nodes(mesh) # Assign the model('s) to the object return InterfaceVTK.__create_structured_grid( ptsMat, mesh.shape_cells, models=models ) def __curvilinear_mesh_to_vtk(mesh, models=None): """Convert the CurvilinearMesh to a vtk object. Constructs a :class:`pyvista.StructuredGrid` of this mesh and the given models as ``cell_arrays`` of that object. Parameters ---------- mesh : discretize.CurvilinearMesh The curvilinear mesh to convert to a :class:`pyvista.StructuredGrid` models : dict(numpy.ndarray) Name('s) and array('s). Match number of cells """ ptsMat = InterfaceVTK.__get_rotated_nodes(mesh) return InterfaceVTK.__create_structured_grid( ptsMat, mesh.shape_cells, models=models ) def __cyl_mesh_to_vtk(mesh, models=None): """Convert the CylindricalMesh to a vtk object. Constructs an vtkUnstructuredGrid made of rational Bezier hexahedrons and wedges. Wedges happen on the very internal layer about r=0, and hexes occur elsewhere. """ if np.any(mesh.h[1] >= np.pi): raise NotImplementedError( "Exporting cylindrical meshes to vtk with angles larger than 180 degrees" " is not yet supported." ) # # Points deflate_nodes = mesh._n_total_nodes != mesh.n_nodes if deflate_nodes: inds = np.empty(mesh._n_total_nodes, dtype=int) is_hanging_nodes = mesh._ishanging_nodes inds[~is_hanging_nodes] = np.arange(mesh.n_nodes) inds[is_hanging_nodes] = list(mesh._hanging_nodes.values()) # calculate control points dphis_half = mesh.h[1] / 2 if mesh.is_wrapped: phi_controls = mesh.nodes_y + dphis_half else: phi_controls = mesh.nodes_y[:-1] + dphis_half if mesh.nodes_x[0] == 0.0: Rs, Phis, Zs = np.meshgrid( mesh.nodes_x[1:], phi_controls, mesh.nodes_z, indexing="ij" ) else: Rs, Phis, Zs = np.meshgrid( mesh.nodes_x, phi_controls, mesh.nodes_z, indexing="ij" ) Rs /= np.cos(dphis_half)[None, :, None] control_nodes = np.c_[ Rs.reshape(-1, order="F"), Phis.reshape(-1, order="F"), Zs.reshape(-1, order="F"), ] cells = np.arange(mesh.n_cells).reshape(mesh.shape_cells, order="F") if mesh.includes_zero: wedge_cells = cells[0].reshape(-1, order="F") hex_cells = (cells[1:]).reshape(-1, order="F") else: hex_cells = cells.reshape(-1, order="F") # Hex Cells... # calculate indices ir, it, iz = np.unravel_index(hex_cells, shape=mesh.shape_cells, order="F") irs = np.stack([ir, ir, ir + 1, ir + 1, ir, ir, ir + 1, ir + 1], axis=-1) its = np.stack([it + 1, it, it, it + 1, it + 1, it, it, it + 1], axis=-1) izs = np.stack([iz, iz, iz, iz, iz + 1, iz + 1, iz + 1, iz + 1], axis=-1) i_hex_nodes = np.ravel_multi_index( (irs, its, izs), mesh._shape_total_nodes, order="F" ) if deflate_nodes: i_hex_nodes = inds[i_hex_nodes] if mesh.includes_zero: irs = np.stack([ir - 1, ir, ir - 1, ir], axis=-1) else: irs = np.stack([ir, ir + 1, ir, ir + 1], axis=-1) its = np.stack([it, it, it, it], axis=-1) izs = np.stack([iz, iz, iz + 1, iz + 1], axis=-1) i_hex_control_nodes = np.ravel_multi_index((irs, its, izs), Rs.shape, order="F") if mesh.includes_zero: # Wedge Cells nodes # put control points along radial edge for halfway points on the edges... Phis, Zs = np.meshgrid(mesh.nodes_y, mesh.nodes_z, indexing="ij") Rhalfs = np.full_like(Phis, mesh.nodes_x[1] * 0.5) wedge_control_nodes = np.c_[ Rhalfs.reshape(-1, order="F"), Phis.reshape(-1, order="F"), Zs.reshape(-1, order="F"), ] # indices for wedge nodes: for cell 0 ir, it, iz = np.unravel_index( wedge_cells, shape=mesh.shape_cells, order="F" ) irs = np.stack([ir, ir + 1, ir + 1, ir, ir + 1, ir + 1], axis=-1) its = np.stack([it, it, it + 1, it, it, it + 1], axis=-1) izs = np.stack([iz, iz, iz, iz + 1, iz + 1, iz + 1], axis=-1) i_wedge_nodes = np.ravel_multi_index( (irs, its, izs), mesh._shape_total_nodes, order="F" ) if deflate_nodes: i_wedge_nodes = inds[i_wedge_nodes] its = np.stack([it, it + 1, it, it + 1], axis=-1) izs = np.stack([iz, iz, iz + 1, iz + 1], axis=-1) # wrap mode for the duplicated wedge control nodes i_wscn = np.ravel_multi_index( (its, izs), Rhalfs.shape, order="F", mode="wrap" ) + len(control_nodes) irs = np.stack([ir, ir], axis=-1) its = np.stack([it, it], axis=-1) izs = np.stack([iz, iz + 1], axis=-1) i_wccn = np.ravel_multi_index((irs, its, izs), Rs.shape, order="F") i_wedge_control_nodes = np.c_[ i_wscn[:, 0], i_wccn[:, 0], i_wscn[:, 1], i_wscn[:, 2], i_wccn[:, 1], i_wscn[:, 3], ] _vtk, _nps = load_vtk() #### # assemble cells cell_types = np.empty(mesh.n_cells, dtype=np.uint8) cell_types[hex_cells] = _vtk.VTK_BEZIER_HEXAHEDRON cell_con = np.empty((mesh.n_cells, 13), dtype=int) cell_con[:, 0] = 12 cell_con[hex_cells, 1:9] = i_hex_nodes cell_con[hex_cells, 9:] = i_hex_control_nodes + mesh.n_nodes nodes_cyl = np.r_[mesh.nodes, control_nodes] # calculate weights for control points if mesh.includes_zero: control_weights = ( np.sin(np.pi / 2 - dphis_half)[None, :, None] * np.ones((mesh.shape_nodes[0] - 1, mesh.shape_nodes[2]))[:, None, :] ) else: control_weights = ( np.sin(np.pi / 2 - dphis_half)[None, :, None] * np.ones((mesh.shape_nodes[0], mesh.shape_nodes[2]))[:, None, :] ) rational_weights = np.r_[ np.ones(mesh.n_nodes), control_weights.reshape(-1, order="F") ] higher_order_degrees = np.empty((mesh.n_cells, 3)) higher_order_degrees[hex_cells, :] = [2, 1, 1] if mesh.includes_zero: cell_types[wedge_cells] = _vtk.VTK_BEZIER_WEDGE cell_con[wedge_cells, 1:7] = i_wedge_nodes cell_con[wedge_cells, 7:] = i_wedge_control_nodes + mesh.n_nodes nodes_cyl = np.r_[nodes_cyl, wedge_control_nodes] rational_weights = np.r_[ rational_weights, np.ones(len(wedge_control_nodes)) ] higher_order_degrees[wedge_cells] = [2, 2, 1] nodes = cyl2cart(nodes_cyl) vtk_pts = _vtk.vtkPoints() vtk_pts.SetData(_nps.numpy_to_vtk(nodes, deep=True)) cells = _vtk.vtkCellArray() cells.SetNumberOfCells(mesh.n_cells) cells.SetCells( mesh.n_cells, _nps.numpy_to_vtk( cell_con.reshape(-1), deep=True, array_type=_vtk.VTK_ID_TYPE ), ) cell_types = _nps.numpy_to_vtk(cell_types, deep=True) output = _vtk.vtkUnstructuredGrid() output.SetPoints(vtk_pts) output.SetCells(cell_types, cells) vtk_rational_weights = _nps.numpy_to_vtk(rational_weights) vtk_higher_order_degrees = _nps.numpy_to_vtk(higher_order_degrees) output.GetPointData().SetRationalWeights(vtk_rational_weights) output.GetCellData().SetHigherOrderDegrees(vtk_higher_order_degrees) # Assign the model('s) to the object return assign_cell_data(output, models=models) def to_vtk(mesh, models=None): """Convert mesh (and models) to corresponding VTK or PyVista data object. This method converts a ``discretize`` mesh (and associated models) to its corresponding `VTK `__ or `PyVista `__ data object. Parameters ---------- models : dict of [str, (n_cells) numpy.ndarray], optional Models are supplied as a dictionary where the keys are the model names. Each model is a 1D :class:`numpy.ndarray` of size (n_cells). Returns ------- pyvista.UnstructuredGrid, pyvista.RectilinearGrid or pyvista.StructuredGrid The corresponding VTK or PyVista data object for the mesh and its models """ converters = { "tree": InterfaceVTK.__tree_mesh_to_vtk, "tensor": InterfaceVTK.__tensor_mesh_to_vtk, "curv": InterfaceVTK.__curvilinear_mesh_to_vtk, "simplex": InterfaceVTK.__simplex_mesh_to_vtk, "cyl": InterfaceVTK.__cyl_mesh_to_vtk, } key = mesh._meshType.lower() try: convert = converters[key] except KeyError: raise RuntimeError( "Mesh type `{}` is not currently supported for VTK conversion.".format( key ) ) # Convert the data object then attempt a wrapping with `pyvista` cvtd = convert(mesh, models=models) try: import pyvista cvtd = pyvista.wrap(cvtd) except ImportError: warnings.warn( "For easier use of VTK objects, you should install `pyvista` (the VTK interface): pip install pyvista", stacklevel=1, ) return cvtd def toVTK(mesh, models=None): """Convert mesh (and models) to corresponding VTK or PyVista data object. *toVTK* has been deprecated and replaced by *to_vtk*. See Also -------- to_vtk """ warnings.warn( "Deprecation Warning: `toVTK` is deprecated, use `to_vtk` instead", category=FutureWarning, stacklevel=2, ) return InterfaceVTK.to_vtk(mesh, models=models) @staticmethod def _save_unstructured_grid(file_name, vtkUnstructGrid, directory=""): """Save an unstructured grid to a vtk file. Saves a VTK unstructured grid file (vtu) for an already generated :class:`pyvista.UnstructuredGrid` object. Parameters ---------- file_name : str path to the output vtk file or just its name if directory is specified directory : str directory where the UBC GIF file lives """ _vtk, _, _vtkUnstWriter = load_vtk("vtkXMLUnstructuredGridWriter") if not isinstance(vtkUnstructGrid, _vtk.vtkUnstructuredGrid): raise RuntimeError( "`_save_unstructured_grid` can only handle `vtkUnstructuredGrid` objects. `%s` is not supported." % vtkUnstructGrid.__class__ ) # Check the extension of the file_name fname = os.path.join(directory, file_name) ext = os.path.splitext(fname)[1] if ext == "": fname = fname + ".vtu" elif ext not in ".vtu": raise IOError("{:s} is an incorrect extension, has to be .vtu".format(ext)) # Make the writer vtuWriteFilter = _vtkUnstWriter() vtuWriteFilter.SetDataModeToBinary() vtuWriteFilter.SetInputDataObject(vtkUnstructGrid) vtuWriteFilter.SetFileName(fname) # Write the file vtuWriteFilter.Write() @staticmethod def _save_structured_grid(file_name, vtkStructGrid, directory=""): """Save a structured grid to a vtk file. Saves a VTK structured grid file (vtk) for an already generated :class:`pyvista.StructuredGrid` object. Parameters ---------- file_name : str path to the output vtk file or just its name if directory is specified directory : str directory where the UBC GIF file lives """ _vtk, _, _vtkStrucWriter = load_vtk("vtkXMLStructuredGridWriter") if not isinstance(vtkStructGrid, _vtk.vtkStructuredGrid): raise RuntimeError( "`_save_structured_grid` can only handle `vtkStructuredGrid` objects. `{}` is not supported.".format( vtkStructGrid.__class__ ) ) # Check the extension of the file_name fname = os.path.join(directory, file_name) ext = os.path.splitext(fname)[1] if ext == "": fname = fname + ".vts" elif ext not in ".vts": raise IOError("{:s} is an incorrect extension, has to be .vts".format(ext)) # Make the writer writer = _vtkStrucWriter() writer.SetDataModeToBinary() writer.SetInputDataObject(vtkStructGrid) writer.SetFileName(fname) # Write the file writer.Write() @staticmethod def _save_rectilinear_grid(file_name, vtkRectGrid, directory=""): """Save a rectilinear grid to a vtk file. Saves a VTK rectilinear file (vtr) ffor an already generated :class:`pyvista.RectilinearGrid` object. Parameters ---------- file_name : str path to the output vtk file or just its name if directory is specified directory : str directory where the UBC GIF file lives """ _vtk, _, _vtkRectWriter = load_vtk("vtkXMLRectilinearGridWriter") if not isinstance(vtkRectGrid, _vtk.vtkRectilinearGrid): raise RuntimeError( "`_save_rectilinear_grid` can only handle `vtkRectilinearGrid` objects. `{}` is not supported.".format( vtkRectGrid.__class__ ) ) # Check the extension of the file_name fname = os.path.join(directory, file_name) ext = os.path.splitext(fname)[1] if ext == "": fname = fname + ".vtr" elif ext not in ".vtr": raise IOError("{:s} is an incorrect extension, has to be .vtr".format(ext)) # Write the file. vtrWriteFilter = _vtkRectWriter() vtrWriteFilter.SetDataModeToBinary() vtrWriteFilter.SetFileName(fname) vtrWriteFilter.SetInputDataObject(vtkRectGrid) vtrWriteFilter.Write() def write_vtk(mesh, file_name, models=None, directory=""): """Convert mesh (and models) to corresponding VTK or PyVista data object then writes to file. This method converts a ``discretize`` mesh (and associated models) to its corresponding `VTK `__ or `PyVista `__ data object, then writes to file. The output structure will be one of: ``vtkUnstructuredGrid``, ``vtkRectilinearGrid`` or ``vtkStructuredGrid``. Parameters ---------- file_name : str or file name Full path for the output file or just its name if directory is specified models : dict of [str, (n_cells) numpy.ndarray], optional Models are supplied as a dictionary where the keys are the model names. Each model is a 1D :class:`numpy.ndarray` of size (n_cells). directory : str, optional output directory Returns ------- str The output of Python's *write* function """ vtkObj = InterfaceVTK.to_vtk(mesh, models=models) writers = { "vtkUnstructuredGrid": InterfaceVTK._save_unstructured_grid, "vtkRectilinearGrid": InterfaceVTK._save_rectilinear_grid, "vtkStructuredGrid": InterfaceVTK._save_structured_grid, } key = vtkObj.GetClassName() try: write = writers[key] except KeyError: raise RuntimeError("VTK data type `%s` is not currently supported." % key) return write(file_name, vtkObj, directory=directory) def writeVTK(mesh, file_name, models=None, directory=""): """Convert mesh (and models) to corresponding VTK or PyVista data object then writes to file. *writeVTK* has been deprecated and replaced by *write_vtk* See Also -------- write_vtk """ warnings.warn( "Deprecation Warning: `writeVTK` is deprecated, use `write_vtk` instead", category=FutureWarning, stacklevel=2, ) return InterfaceVTK.write_vtk( mesh, file_name, models=models, directory=directory ) class InterfaceTensorread_vtk(object): """Mixin class for converting vtk to TensorMesh. This class provides convenient methods for converting VTK Rectilinear Grid files/objects to :class:`~discretize.TensorMesh` objects. """ @classmethod def vtk_to_tensor_mesh(TensorMesh, vtrGrid): """Convert vtk object to a TensorMesh. Convert ``vtkRectilinearGrid`` or :class:`~pyvista.RectilinearGrid` object to a :class:`~discretize.TensorMesh` object. Parameters ---------- vtuGrid : ``vtkRectilinearGrid`` or :class:`~pyvista.RectilinearGrid` A VTK or PyVista rectilinear grid object Returns ------- discretize.TensorMesh A discretize tensor mesh """ _, _nps = load_vtk() # Sort information hx = np.abs(np.diff(_nps.vtk_to_numpy(vtrGrid.GetXCoordinates()))) xR = _nps.vtk_to_numpy(vtrGrid.GetXCoordinates())[0] hy = np.abs(np.diff(_nps.vtk_to_numpy(vtrGrid.GetYCoordinates()))) yR = _nps.vtk_to_numpy(vtrGrid.GetYCoordinates())[0] zD = np.diff(_nps.vtk_to_numpy(vtrGrid.GetZCoordinates())) # Check the direction of hz if np.all(zD < 0): hz = np.abs(zD[::-1]) zR = _nps.vtk_to_numpy(vtrGrid.GetZCoordinates())[-1] else: hz = np.abs(zD) zR = _nps.vtk_to_numpy(vtrGrid.GetZCoordinates())[0] origin = np.array([xR, yR, zR]) # Make the object tensMsh = TensorMesh([hx, hy, hz], origin=origin) # Grap the models models = {} for i in np.arange(vtrGrid.GetCellData().GetNumberOfArrays()): modelName = vtrGrid.GetCellData().GetArrayName(i) if np.all(zD < 0): modFlip = _nps.vtk_to_numpy(vtrGrid.GetCellData().GetArray(i)) tM = tensMsh.reshape(modFlip, "CC", "CC", "M") modArr = tensMsh.reshape(tM[:, :, ::-1], "CC", "CC", "V") else: modArr = _nps.vtk_to_numpy(vtrGrid.GetCellData().GetArray(i)) models[modelName] = modArr # Return the data return tensMsh, models @classmethod def read_vtk(TensorMesh, file_name, directory=""): """Read VTK rectilinear file (vtr or xml) and return a discretize tensor mesh (and models). This method reads a VTK rectilinear file (vtr or xml format) and returns a tuple containing the :class:`~discretize.TensorMesh` as well as a dictionary containing any models. The keys in the model dictionary correspond to the file names of the models. Parameters ---------- file_name : str full path to the VTK rectilinear file (vtr or xml) containing the mesh (and models) or just the file name if the directory is specified. directory : str, optional directory where the file lives Returns ------- mesh : discretize.TensorMesh The tensor mesh object. model_dict : dict of [str, (n_cells) numpy.ndarray] A dictionary containing the models. The keys correspond to the names of the models. """ _, _, _vtkRectReader = load_vtk("vtkXMLRectilinearGridReader") fname = os.path.join(directory, file_name) # Read the file vtrReader = _vtkRectReader() vtrReader.SetFileName(fname) vtrReader.Update() vtrGrid = vtrReader.GetOutput() return TensorMesh.vtk_to_tensor_mesh(vtrGrid) @classmethod def readVTK(TensorMesh, file_name, directory=""): """Read VTK rectilinear file (vtr or xml) and return a discretize tensor mesh (and models). *readVTK* has been deprecated and replaced by *read_vtk* See Also -------- read_vtk """ warnings.warn( "Deprecation Warning: `readVTK` is deprecated, use `read_vtk` instead", category=FutureWarning, stacklevel=2, ) return InterfaceTensorread_vtk.read_vtk( TensorMesh, file_name, directory=directory ) class InterfaceSimplexReadVTK: """Mixin class for converting vtk to SimplexMesh. This class provides convenient methods for converting VTK Unstructured Grid files/objects to :class:`~discretize.SimplexMesh` objects. """ @classmethod def vtk_to_simplex_mesh(SimplexMesh, vtuGrid): """Convert an unstructured grid of simplices to a SimplexMesh. Convert ``vtkUnstructuredGrid`` or :class:`~pyvista.UnstructuredGrid` object to a :class:`~discretize.SimplexMesh` object. Parameters ---------- vtrGrid : ``vtkUnstructuredGrid`` or :class:`~pyvista.UnstructuredGrid` A VTK or PyVista unstructured grid object Returns ------- discretize.SimplexMesh A discretize tensor mesh """ _, _nps = load_vtk() # check if all of the cells are the same type cell_types = np.unique(_nps.vtk_to_numpy(vtuGrid.GetCellTypesArray())) if len(cell_types) > 1: raise ValueError( "Incompatible unstructured grid. All cell's must have the same type." ) if cell_types[0] not in [5, 10]: raise ValueError("Cell types must be either triangular or tetrahedral") if cell_types[0] == 5: dim = 2 else: dim = 3 # then should be safe to move forward simplices = _nps.vtk_to_numpy( vtuGrid.GetCells().GetConnectivityArray() ).reshape(-1, dim + 1) points = _nps.vtk_to_numpy(vtuGrid.GetPoints().GetData()) if dim == 2: points = points[:, :-1] mesh = SimplexMesh(points, simplices) # Grap the models models = {} for i in np.arange(vtuGrid.GetCellData().GetNumberOfArrays()): modelName = vtuGrid.GetCellData().GetArrayName(i) modArr = _nps.vtk_to_numpy(vtuGrid.GetCellData().GetArray(i)) models[modelName] = modArr # Return the data return mesh, models @classmethod def read_vtk(SimplexMesh, file_name, directory=""): """Read VTK unstructured file (vtu or xml) and return a discretize simplex mesh (and models). This method reads a VTK unstructured file (vtu or xml format) and returns the :class:`~discretize.SimplexMesh` as well as a dictionary containing any models. The keys in the model dictionary correspond to the file names of the models. Parameters ---------- file_name : str full path to the VTK unstructured file (vtr or xml) containing the mesh (and models) or just the file name if the directory is specified. directory : str, optional directory where the file lives Returns ------- mesh : discretize.SimplexMesh The tensor mesh object. model_dict : dict of [str, (n_cells) numpy.ndarray] A dictionary containing the models. The keys correspond to the names of the models. """ _, _, _vtkUnstReader = load_vtk("vtkXMLUnstructuredGridReader") fname = os.path.join(directory, file_name) # Read the file vtuReader = _vtkUnstReader() vtuReader.SetFileName(fname) vtuReader.Update() vtuGrid = vtuReader.GetOutput() return SimplexMesh.vtk_to_simplex_mesh(vtuGrid) ================================================ FILE: discretize/operators/__init__.py ================================================ """ ================================================ Discrete Operators (:mod:`discretize.operators`) ================================================ .. currentmodule:: discretize.operators The ``operators`` package contains the classes discretize meshes with regular structure use to construct discrete versions of the differential operators. Operator Classes ---------------- .. autosummary:: :toctree: generated/ DiffOperators InnerProducts """ from discretize.operators.differential_operators import DiffOperators from discretize.operators.inner_products import InnerProducts ================================================ FILE: discretize/operators/differential_operators.py ================================================ """Differential operators for meshes.""" import numpy as np from scipy import sparse as sp import warnings from discretize.base import BaseMesh from discretize.utils import ( sdiag, speye, kron3, spzeros, ddx, av, av_extrap, make_boundary_bool, cross2d, ) from discretize.utils.code_utils import deprecate_method, deprecate_property def _validate_BC(bc): """Check if boundary condition 'bc' is valid. Each bc must be either 'dirichlet' or 'neumann' """ if isinstance(bc, str): bc = [bc, bc] if not isinstance(bc, list): raise TypeError("bc must be a single string or list of strings") if not len(bc) == 2: raise TypeError("bc list must have two elements, one for each side") for bc_i in bc: if not isinstance(bc_i, str): raise TypeError("each bc must be a string") if bc_i not in ["dirichlet", "neumann"]: raise ValueError("each bc must be either, 'dirichlet' or 'neumann'") return bc def _ddxCellGrad(n, bc): """Create 1D derivative operator from cell-centers to nodes. This means we go from n to n+1 For Cell-Centered **Dirichlet**, use a ghost point:: (u_1 - u_g)/hf = grad u_g u_1 u_2 * | * | * ... ^ 0 u_g = - u_1 grad = 2*u1/dx negitive on the other side. For Cell-Centered **Neumann**, use a ghost point:: (u_1 - u_g)/hf = 0 u_g u_1 u_2 * | * | * ... u_g = u_1 grad = 0; put a zero in. """ bc = _validate_BC(bc) D = sp.spdiags((np.ones((n + 1, 1)) * [-1, 1]).T, [-1, 0], n + 1, n, format="csr") # Set the first side if bc[0] == "dirichlet": D[0, 0] = 2 elif bc[0] == "neumann": D[0, 0] = 0 # Set the second side if bc[1] == "dirichlet": D[-1, -1] = -2 elif bc[1] == "neumann": D[-1, -1] = 0 return D def _ddxCellGradBC(n, bc): """Create 1D derivative operator from cell-centers to nodes. This means we go from n to n+1. For Cell-Centered **Dirichlet**, use a ghost point:: (u_1 - u_g)/hf = grad u_g u_1 u_2 * | * | * ... ^ u_b We know the value at the boundary (u_b):: (u_g+u_1)/2 = u_b (the average) u_g = 2*u_b - u_1 So plug in to gradient: (u_1 - (2*u_b - u_1))/hf = grad 2*(u_1-u_b)/hf = grad Separate, because BC are known (and can move to RHS later):: ( 2/hf )*u_1 + ( -2/hf )*u_b = grad ( ^ ) JUST RETURN THIS """ bc = _validate_BC(bc) ij = (np.array([0, n]), np.array([0, 1])) vals = np.zeros(2) # Set the first side if bc[0] == "dirichlet": vals[0] = -2 elif bc[0] == "neumann": vals[0] = 0 # Set the second side if bc[1] == "dirichlet": vals[1] = 2 elif bc[1] == "neumann": vals[1] = 0 D = sp.csr_matrix((vals, ij), shape=(n + 1, 2)) return D class DiffOperators(BaseMesh): """Class used for creating differential and averaging operators. ``DiffOperators`` is a class for managing the construction of differential and averaging operators at the highest level. The ``DiffOperator`` class is inherited by every ``discretize`` mesh class. In practice, differential and averaging operators are not constructed by creating instances of ``DiffOperators``. Instead, the operators are constructed (and sometimes stored) when called as a property of the mesh. """ _aliases = { "aveFx2CC": "average_face_x_to_cell", "aveFy2CC": "average_face_y_to_cell", "aveFz2CC": "average_face_z_to_cell", "aveEx2CC": "average_edge_x_to_cell", "aveEy2CC": "average_edge_y_to_cell", "aveEz2CC": "average_edge_z_to_cell", } ########################################################################### # # # Face Divergence # # # ########################################################################### @property def _face_x_divergence_stencil(self): """Stencil for face divergence operator in the x-direction (x-faces to cell centers).""" if self.dim == 1: Dx = ddx(self.shape_cells[0]) elif self.dim == 2: Dx = sp.kron(speye(self.shape_cells[1]), ddx(self.shape_cells[0])) elif self.dim == 3: Dx = kron3( speye(self.shape_cells[2]), speye(self.shape_cells[1]), ddx(self.shape_cells[0]), ) return Dx @property def _face_y_divergence_stencil(self): """Stencil for face divergence operator in the y-direction (y-faces to cell centers).""" if self.dim == 1: return None elif self.dim == 2: Dy = sp.kron(ddx(self.shape_cells[1]), speye(self.shape_cells[0])) elif self.dim == 3: Dy = kron3( speye(self.shape_cells[2]), ddx(self.shape_cells[1]), speye(self.shape_cells[0]), ) return Dy @property def _face_z_divergence_stencil(self): """Stencil for face divergence operator in the z-direction (z-faces to cell centers).""" if self.dim == 1 or self.dim == 2: return None elif self.dim == 3: Dz = kron3( ddx(self.shape_cells[2]), speye(self.shape_cells[1]), speye(self.shape_cells[0]), ) return Dz @property def _face_divergence_stencil(self): """Full stencil for face divergence operator (faces to cell centers).""" if self.dim == 1: D = self._face_x_divergence_stencil elif self.dim == 2: D = sp.hstack( (self._face_x_divergence_stencil, self._face_y_divergence_stencil), format="csr", ) elif self.dim == 3: D = sp.hstack( ( self._face_x_divergence_stencil, self._face_y_divergence_stencil, self._face_z_divergence_stencil, ), format="csr", ) return D @property def face_divergence(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_face_divergence", None) is None: # Get the stencil of +1, -1's D = self._face_divergence_stencil # Compute areas of cell faces & volumes S = self.face_areas V = self.cell_volumes self._face_divergence = sdiag(1 / V) * D * sdiag(S) return self._face_divergence @property def face_x_divergence(self): r"""X-derivative operator (x-faces to cell-centres). This property constructs a 2nd order x-derivative operator which maps from x-faces to cell centers. The operator is a sparse matrix :math:`\mathbf{D_x}` that can be applied as a matrix-vector product to a discrete scalar quantity :math:`\boldsymbol{\phi}` that lives on x-faces; i.e.:: dphi_dx = Dx @ phi For a discrete vector whose x-component lives on x-faces, this operator can also be used to compute the contribution of the x-component toward the divergence. Returns ------- (n_cells, n_faces_x) scipy.sparse.csr_matrix The numerical x-derivative operator from x-faces to cell centers Examples -------- Below, we demonstrate 1) how to apply the face-x divergence operator, and 2) the mapping of the face-x divergence operator and its sparsity. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl For a discrete scalar quantity :math:`\boldsymbol{\phi}` defined on the x-faces, we take the x-derivative by constructing the face-x divergence operator and multiplying as a matrix-vector product. >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], "CC") Create a discrete quantity on x-faces >>> faces_x = mesh.faces_x >>> phi = np.exp(-(faces_x[:, 0] ** 2) / 8** 2) Construct the x-divergence operator and apply to vector >>> Dx = mesh.face_x_divergence >>> dphi_dx = Dx @ phi Plot the original function and the x-divergence >>> fig = plt.figure(figsize=(13, 6)) >>> ax1 = fig.add_subplot(121) >>> w = np.r_[phi, np.ones(mesh.nFy)] # Need vector on all faces for image plot >>> mesh.plot_image(w, ax=ax1, v_type="Fx") >>> ax1.set_title("Scalar on x-faces", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(dphi_dx, ax=ax2) >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("X-derivative at cell center", fontsize=14) >>> plt.show() The discrete x-face divergence operator is a sparse matrix that maps from x-faces to cell centers. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements in the original discrete quantity :math:`\boldsymbol{\phi}}` and its x-derivative :math:`\partial \boldsymbol{\phi}}/ \partial x` as well as a spy plot. >>> mesh = TensorMesh([[(1, 6)], [(1, 3)]]) >>> fig = plt.figure(figsize=(10, 10)) >>> ax1 = fig.add_subplot(211) >>> mesh.plot_grid(ax=ax1) >>> ax1.plot( ... mesh.faces_x[:, 0], mesh.faces_x[:, 1], "g>", markersize=8 ... ) >>> for ii, loc in zip(range(mesh.nFx), mesh.faces_x): ... ax1.text(loc[0]+0.05, loc[1]+0.02, "{0:d}".format(ii), color="g") >>> ax1.plot( ... mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8 ... ) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0]+0.05, loc[1]+0.02, "{0:d}".format(ii), color="r") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.set_title("Mapping of Face-X Divergence", fontsize=14, pad=15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (x-faces)', r'$\partial \mathbf{phi}/\partial x$ (centers)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(212) >>> ax2.spy(mesh.face_x_divergence) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Cell Index", fontsize=12) >>> ax2.set_xlabel("X-Face Index", fontsize=12) >>> plt.show() """ # Compute areas of cell faces & volumes S = self.reshape(self.face_areas, "F", "Fx", "V") V = self.cell_volumes return sdiag(1 / V) * self._face_x_divergence_stencil * sdiag(S) @property def face_y_divergence(self): r"""Y-derivative operator (y-faces to cell-centres). This property constructs a 2nd order y-derivative operator which maps from y-faces to cell centers. The operator is a sparse matrix :math:`\mathbf{D_y}` that can be applied as a matrix-vector product to a discrete scalar quantity :math:`\boldsymbol{\phi}` that lives on y-faces; i.e.:: dphi_dy = Dy @ phi For a discrete vector whose y-component lives on y-faces, this operator can also be used to compute the contribution of the y-component toward the divergence. Returns ------- (n_cells, n_faces_y) scipy.sparse.csr_matrix The numerical y-derivative operator from y-faces to cell centers Examples -------- Below, we demonstrate 1) how to apply the face-y divergence operator, and 2) the mapping of the face-y divergence operator and its sparsity. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl For a discrete scalar quantity :math:`\boldsymbol{\phi}` defined on the y-faces, we take the y-derivative by constructing the face-y divergence operator and multiplying as a matrix-vector product. >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], "CC") Create a discrete quantity on x-faces >>> faces_y = mesh.faces_y >>> phi = np.exp(-(faces_y[:, 1] ** 2) / 8** 2) Construct the y-divergence operator and apply to vector >>> Dy = mesh.face_y_divergence >>> dphi_dy = Dy @ phi Plot original function and the y-divergence >>> fig = plt.figure(figsize=(13, 6)) >>> ax1 = fig.add_subplot(121) >>> w = np.r_[np.ones(mesh.nFx), phi] # Need vector on all faces for image plot >>> mesh.plot_image(w, ax=ax1, v_type="Fy") >>> ax1.set_title("Scalar on y-faces", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(dphi_dy, ax=ax2) >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Y-derivative at cell center", fontsize=14) >>> plt.show() The discrete y-face divergence operator is a sparse matrix that maps from y-faces to cell centers. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements in the original discrete quantity :math:`\boldsymbol{\phi}` and its y-derivative :math:`\partial \boldsymbol{\phi}/ \partial y` as well as a spy plot. >>> mesh = TensorMesh([[(1, 6)], [(1, 3)]]) >>> fig = plt.figure(figsize=(10, 10)) >>> ax1 = fig.add_subplot(211) >>> mesh.plot_grid(ax=ax1) >>> ax1.plot( ... mesh.faces_y[:, 0], mesh.faces_y[:, 1], "g^", markersize=8 ... ) >>> for ii, loc in zip(range(mesh.nFy), mesh.faces_y): ... ax1.text(loc[0]+0.05, loc[1]+0.02, "{0:d}".format(ii), color="g") >>> ax1.plot( ... mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8 ... ) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0]+0.05, loc[1]+0.02, "{0:d}".format(ii), color="r") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.set_title("Mapping of Face-Y Divergence", fontsize=14, pad=15) >>> ax1.legend( ... ['Mesh',r'$\mathbf{\phi}$ (y-faces)',r'$\partial_y \mathbf{\phi}/\partial y$ (centers)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(212) >>> ax2.spy(mesh.face_y_divergence) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Cell Index", fontsize=12) >>> ax2.set_xlabel("Y-Face Index", fontsize=12) >>> plt.show() """ if self.dim < 2: return None # Compute areas of cell faces & volumes S = self.reshape(self.face_areas, "F", "Fy", "V") V = self.cell_volumes return sdiag(1 / V) * self._face_y_divergence_stencil * sdiag(S) @property def face_z_divergence(self): r"""Z-derivative operator (z-faces to cell-centres). This property constructs a 2nd order z-derivative operator which maps from z-faces to cell centers. The operator is a sparse matrix :math:`\mathbf{D_z}` that can be applied as a matrix-vector product to a discrete scalar quantity :math:`\boldsymbol{\phi}` that lives on z-faces; i.e.:: dphi_dz = Dz @ phi For a discrete vector whose z-component lives on z-faces, this operator can also be used to compute the contribution of the z-component toward the divergence. Returns ------- (n_cells, n_faces_z) scipy.sparse.csr_matrix The numerical z-derivative operator from z-faces to cell centers Examples -------- Below, we demonstrate 2) how to apply the face-z divergence operator, and 2) the mapping of the face-z divergence operator and its sparsity. Our example is carried out on a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl For a discrete scalar quantity :math:`\boldsymbol{\phi}` defined on the z-faces, we take the z-derivative by constructing the face-z divergence operator and multiplying as a matrix-vector product. >>> h = np.ones(40) >>> mesh = TensorMesh([h, h, h], "CCC") Create a discrete quantity on z-faces >>> faces_z = mesh.faces_z >>> phi = np.exp(-(faces_z[:, 2] ** 2) / 8** 2) Construct the z-divergence operator and apply to vector >>> Dz = mesh.face_z_divergence >>> dphi_dz = Dz @ phi Plot the original function and the z-divergence >>> fig = plt.figure(figsize=(13, 6)) >>> ax1 = fig.add_subplot(121) >>> w = np.r_[np.ones(mesh.nFx+mesh.nFz), phi] # Need vector on all faces for image plot >>> mesh.plot_slice(w, ax=ax1, v_type="Fz", normal='Y', ind=20) >>> ax1.set_title("Scalar on z-faces (y-slice)", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_slice(dphi_dz, ax=ax2, normal='Y', ind=20) >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Z-derivative at cell center (y-slice)", fontsize=14) >>> plt.show() The discrete z-face divergence operator is a sparse matrix that maps from z-faces to cell centers. To demonstrate this, we construct a small 3D mesh. We then show the ordering of the elements in the original discrete quantity :math:`\boldsymbol{\phi}` and its z-derivative :math:`\partial \boldsymbol{\phi}/ \partial z` as well as a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 2)], [(1, 2)]]) >>> fig = plt.figure(figsize=(9, 12)) >>> ax1 = fig.add_axes([0, 0.35, 1, 0.6], projection='3d', elev=10, azim=-82) >>> mesh.plot_grid(ax=ax1) >>> ax1.plot( ... mesh.faces_z[:, 0], mesh.faces_z[:, 1], mesh.faces_z[:, 2], "g^", markersize=10 ... ) >>> for ii, loc in zip(range(mesh.nFz), mesh.faces_z): ... ax1.text(loc[0] + 0.05, loc[1] + 0.05, loc[2], "{0:d}".format(ii), color="g") >>> ax1.plot( ... mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], mesh.cell_centers[:, 2], ... "ro", markersize=10 ... ) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0] + 0.05, loc[1] + 0.05, loc[2], "{0:d}".format(ii), color="r") >>> ax1.legend( ... ['Mesh',r'$\mathbf{\phi}$ (z-faces)',r'$\partial \mathbf{\phi}/\partial z$ (centers)'], ... loc='upper right', fontsize=14 ... ) Manually make axis properties invisible >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.set_zticks([]) >>> ax1.xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.xaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.yaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.zaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) >>> ax1.set_xlabel('X', labelpad=-15, fontsize=16) >>> ax1.set_ylabel('Y', labelpad=-20, fontsize=16) >>> ax1.set_zlabel('Z', labelpad=-20, fontsize=16) >>> ax1.set_title("Mapping of Face-Z Divergence", fontsize=16, pad=-15) Spy plot the operator, >>> ax2 = fig.add_axes([0.05, 0.05, 0.9, 0.3]) >>> ax2.spy(mesh.face_z_divergence) >>> ax2.set_title("Spy Plot", fontsize=16, pad=5) >>> ax2.set_ylabel("Cell Index", fontsize=12) >>> ax2.set_xlabel("Z-Face Index", fontsize=12) >>> plt.show() """ if self.dim < 3: return None # Compute areas of cell faces & volumes S = self.reshape(self.face_areas, "F", "Fz", "V") V = self.cell_volumes return sdiag(1 / V) * self._face_z_divergence_stencil * sdiag(S) ########################################################################### # # # Nodal Diff Operators # # # ########################################################################### @property def _nodal_gradient_x_stencil(self): """Stencil for the nodal gradient in the x-direction (nodes to x-edges).""" if self.dim == 1: Gx = ddx(self.shape_cells[0]) elif self.dim == 2: Gx = sp.kron(speye(self.shape_nodes[1]), ddx(self.shape_cells[0])) elif self.dim == 3: Gx = kron3( speye(self.shape_nodes[2]), speye(self.shape_nodes[1]), ddx(self.shape_cells[0]), ) return Gx @property def _nodal_gradient_y_stencil(self): """Stencil for the nodal gradient in the y-direction (nodes to y-edges).""" if self.dim == 1: return None elif self.dim == 2: Gy = sp.kron(ddx(self.shape_cells[1]), speye(self.shape_nodes[0])) elif self.dim == 3: Gy = kron3( speye(self.shape_nodes[2]), ddx(self.shape_cells[1]), speye(self.shape_nodes[0]), ) return Gy @property def _nodal_gradient_z_stencil(self): """Stencil for the nodal gradient in the z-direction (nodes to z-edges).""" if self.dim == 1 or self.dim == 2: return None else: Gz = kron3( ddx(self.shape_cells[2]), speye(self.shape_nodes[1]), speye(self.shape_nodes[0]), ) return Gz @property def _nodal_gradient_stencil(self): """Full stencil for the nodal gradient (nodes to edges).""" # Compute divergence operator on faces if self.dim == 1: G = self._nodal_gradient_x_stencil elif self.dim == 2: G = sp.vstack( (self._nodal_gradient_x_stencil, self._nodal_gradient_y_stencil), format="csr", ) elif self.dim == 3: G = sp.vstack( ( self._nodal_gradient_x_stencil, self._nodal_gradient_y_stencil, self._nodal_gradient_z_stencil, ), format="csr", ) return G @property def nodal_gradient(self): # NOQA D102 if getattr(self, "_nodal_gradient", None) is None: G = self._nodal_gradient_stencil L = self.edge_lengths self._nodal_gradient = sdiag(1 / L) * G return self._nodal_gradient @property def _nodal_laplacian_x_stencil(self): """Stencil for the nodal Laplacian in the x-direction (nodes to nodes).""" warnings.warn( "Laplacian has not been tested rigorously.", stacklevel=3, ) Dx = ddx(self.shape_cells[0]) Lx = -Dx.T * Dx if self.dim == 2: Lx = sp.kron(speye(self.shape_nodes[1]), Lx) elif self.dim == 3: Lx = kron3(speye(self.shape_nodes[2]), speye(self.shape_nodes[1]), Lx) return Lx @property def _nodal_laplacian_y_stencil(self): """Stencil for the nodal Laplacian in the y-direction (nodes to nodes).""" warnings.warn( "Laplacian has not been tested rigorously.", stacklevel=3, ) if self.dim == 1: return None Dy = ddx(self.shape_cells[1]) Ly = -Dy.T * Dy if self.dim == 2: Ly = sp.kron(Ly, speye(self.shape_nodes[0])) elif self.dim == 3: Ly = kron3(speye(self.shape_nodes[2]), Ly, speye(self.shape_nodes[0])) return Ly @property def _nodal_laplacian_z_stencil(self): """Stencil for the nodal Laplacian in the z-direction (nodes to nodes).""" warnings.warn( "Laplacian has not been tested rigorously.", stacklevel=3, ) if self.dim == 1 or self.dim == 2: return None Dz = ddx(self.shape_cells[2]) Lz = -Dz.T * Dz return kron3(Lz, speye(self.shape_nodes[1]), speye(self.shape_nodes[0])) @property def _nodal_laplacian_x(self): """Construct the nodal Laplacian in the x-direction (nodes to nodes).""" Hx = sdiag(1.0 / self.h[0]) if self.dim == 2: Hx = sp.kron(speye(self.shape_nodes[1]), Hx) elif self.dim == 3: Hx = kron3(speye(self.shape_nodes[2]), speye(self.shape_nodes[1]), Hx) return Hx.T * self._nodal_gradient_x_stencil * Hx @property def _nodal_laplacian_y(self): """Construct the nodal Laplacian in the y-direction (nodes to nodes).""" Hy = sdiag(1.0 / self.h[1]) if self.dim == 1: return None elif self.dim == 2: Hy = sp.kron(Hy, speye(self.shape_nodes[0])) elif self.dim == 3: Hy = kron3(speye(self.shape_nodes[2]), Hy, speye(self.shape_nodes[0])) return Hy.T * self._nodal_gradient_y_stencil * Hy @property def _nodal_laplacian_z(self): """Construct the nodal Laplacian in the z-direction (nodes to nodes).""" if self.dim == 1 or self.dim == 2: return None Hz = sdiag(1.0 / self.h[2]) Hz = kron3(Hz, speye(self.shape_nodes[1]), speye(self.shape_nodes[0])) return Hz.T * self._nodal_laplacian_z_stencil * Hz @property def nodal_laplacian(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_nodal_laplacian", None) is None: warnings.warn( "Laplacian has not been tested rigorously.", stacklevel=2, ) # Compute divergence operator on faces if self.dim == 1: self._nodal_laplacian = self._nodal_laplacian_x elif self.dim == 2: self._nodal_laplacian = ( self._nodal_laplacian_x + self._nodal_laplacian_y ) elif self.dim == 3: self._nodal_laplacian = ( self._nodal_laplacian_x + self._nodal_laplacian_y + self._nodal_laplacian_z ) return self._nodal_laplacian def edge_divergence_weak_form_robin(self, alpha=0.0, beta=1.0, gamma=0.0): r"""Create Robin conditions pieces for weak form of the edge divergence operator (edges to nodes). This method returns the pieces required to impose Robin boundary conditions for the discrete weak form divergence operator that maps from edges to nodes. These pieces are needed when constructing the discrete representation of the inner product :math:`\langle \psi , \nabla \cdot \vec{u} \rangle` according to the finite volume method. To implement the boundary conditions, we assume .. math:: \vec{u} = \nabla \phi for some scalar function :math:`\phi`. Boundary conditions are imposed on the scalar function according to the Robin condition: .. math:: \alpha \phi + \beta \frac{\partial \phi}{\partial n} = \gamma The user supplies values for :math:`\alpha`, :math:`\beta` and :math:`\gamma` for all boundary nodes or faces. For the values supplied, **edge_divergence_weak_form_robin** returns the matrix :math:`\mathbf{B}` and vector :math:`\mathbf{b}` required for the discrete representation of :math:`\langle \psi , \nabla \cdot \vec{u} \rangle`. *See the notes section for a comprehensive description.* Parameters ---------- alpha : scalar or array_like Defines :math:`\alpha` for Robin boundary condition. Can be defined as a scalar or array_like. If array_like, the length of the array must be equal to the number of boundary faces or boundary nodes. beta : scalar or array_like Defines :math:`\beta` for Robin boundary condition. Can be defined as a scalar or array_like. If array_like, must have the same length as *alpha*. Cannot be zero. gamma : scalar or array_like Defines :math:`\gamma` for Robin boundary condition. If array like, *gamma* can have shape (n_boundary_xxx,). Can also have shape (n_boundary_xxx, n_rhs) if multiple systems have the same *alpha* and *beta* parameters. Returns ------- B : (n_nodes, n_nodes) scipy.sparse.dia_matrix A sparse matrix dependent on the values of *alpha*, *beta* and *gamma* supplied b : (n_nodes) numpy.ndarray or (n_nodes, n_rhs) numpy.ndarray A vector dependent on the values of *alpha*, *beta* and *gamma* supplied Notes ----- For the divergence of a vector :math:`\vec{u}`, the weak form is implemented by taking the inner product with a piecewise-constant test function :math:`\psi` and integrating over the domain: .. math:: \langle \psi , \nabla \cdot \vec{u} \rangle \; = \int_\Omega \psi \, (\nabla \cdot \vec{u}) \, dv For a discrete representation of the vector :math:`\vec{u}` that lives on mesh edges, the divergence operator must map from edges to nodes. To implement boundary conditions in this case, we must use the integration by parts to re-express the inner product as: .. math:: \langle \psi , \nabla \cdot \vec{u} \rangle \, = - \int_V \vec{u} \cdot \nabla \psi \, dV + \oint_{\partial \Omega} \psi \, (\hat{n} \cdot \vec{u}) \, da Assuming :math:`\vec{u} = \nabla \phi`, the above equation becomes: .. math:: \langle \psi , \nabla \cdot \vec{u} \rangle \, = - \int_V \nabla \phi \cdot \nabla \psi \, dV + \oint_{\partial \Omega} \psi \, \frac{\partial \phi}{\partial n} \, da Substituting in the Robin conditions: .. math:: \langle \psi , \nabla \cdot \vec{u} \rangle \, = - \int_V \nabla \phi \cdot \nabla \psi \, dV + \oint_{\partial \Omega} \psi \, \frac{(\gamma - \alpha \Phi)}{\beta} \, da therefore, `beta` cannot be zero. The discrete approximation to the above expression is given by: .. math:: \langle \psi , \nabla \cdot \vec{u} \rangle \, \approx - \boldsymbol{\psi^T \big ( G_n^T M_e G_n - B \big ) \phi + \psi^T b} where .. math:: \boldsymbol{u} = \boldsymbol{G_n \, \phi} :math:`\mathbf{G_n}` is the :py:attr:`~discretize.operators.DiffOperators.nodal_gradient` and :math:`\mathbf{M_e}` is the edge inner product matrix (see :py:attr:`~discretize.operators.InnerProducts.get_edge_inner_product`). **edge_divergence_weak_form_robin** returns the matrix :math:`\mathbf{B}` and vector :math:`\mathbf{b}` based on the parameters *alpha* , *beta* and *gamma* provided. Examples -------- Here we construct all of the pieces required for the discrete representation of :math:`\langle \psi , \nabla \cdot \vec{u} \rangle` for specified Robin boundary conditions. We define :math:`\mathbf{u}` on the edges, and :math:`\boldsymbol{\psi}` and :math:`\boldsymbol{\psi}` on the nodes. We begin by creating a small 2D tensor mesh: >>> from discretize import TensorMesh >>> import numpy as np >>> import scipy.sparse as sp >>> h = np.ones(32) >>> mesh = TensorMesh([h, h]) We then define `alpha`, `beta`, and `gamma` parameters for a zero Neumann condition on the boundary faces. This corresponds to setting: >>> alpha = 0.0 >>> beta = 1.0 >>> gamma = 0.0 Next, we construct all of the necessary pieces required to take the discrete inner product: >>> B, b = mesh.edge_divergence_weak_form_robin(alpha, beta, gamma) >>> Me = mesh.get_edge_inner_product() >>> Gn = mesh.nodal_gradient In practice, these pieces are usually re-arranged when used to solve PDEs with the finite volume method. Because the boundary conditions are applied to the scalar potential :math:`\phi`, we create a function which computes the discrete inner product for any :math:`\boldsymbol{\psi}` and :math:`\boldsymbol{\phi}` where :math:`\mathbf{u} = \boldsymbol{G \, \phi}`: >>> def inner_product(psi, phi): ... return psi @ (-Gn.T @ Me @ Gn + B) @ phi + psi @ b """ alpha = np.atleast_1d(alpha) beta = np.atleast_1d(beta) gamma = np.atleast_1d(gamma) if np.any(beta == 0.0): raise ValueError("beta cannot have a zero value") Pbn = self.project_node_to_boundary_node Pbf = self.project_face_to_boundary_face n_boundary_faces = Pbf.shape[0] n_boundary_nodes = Pbn.shape[0] if len(alpha) == 1: if len(beta) != 1: alpha = np.full(len(beta), alpha[0]) elif len(gamma) != 1: alpha = np.full(len(gamma), alpha[0]) else: alpha = np.full(n_boundary_faces, alpha[0]) if len(beta) == 1: if len(alpha) != 1: beta = np.full(len(alpha), beta[0]) if len(gamma) == 1: if len(alpha) != 1: gamma = np.full(len(alpha), gamma[0]) if len(alpha) != len(beta) or len(beta) != len(gamma): raise ValueError("alpha, beta, and gamma must have the same length") if len(alpha) not in [n_boundary_faces, n_boundary_nodes]: raise ValueError( "The arrays must be of length n_boundary_faces or n_boundary_nodes" ) AveN2F = self.average_node_to_face boundary_areas = Pbf @ self.face_areas AveBN2Bf = Pbf @ AveN2F @ Pbn.T # at the boundary, we have that u dot n = (gamma - alpha * phi)/beta if len(alpha) == n_boundary_faces: if gamma.ndim == 2: b = Pbn.T @ ( AveBN2Bf.T @ (gamma / beta[:, None] * boundary_areas[:, None]) ) else: b = Pbn.T @ (AveBN2Bf.T @ (gamma / beta * boundary_areas)) B = sp.diags(Pbn.T @ (AveBN2Bf.T @ (-alpha / beta * boundary_areas))) else: if gamma.ndim == 2: b = Pbn.T @ ( gamma / beta[:, None] * (AveBN2Bf.T @ boundary_areas)[:, None] ) else: b = Pbn.T @ (gamma / beta * (AveBN2Bf.T @ boundary_areas)) B = sp.diags(Pbn.T @ (-alpha / beta * (AveBN2Bf.T @ boundary_areas))) return B, b ########################################################################### # # # Cell Grad # # # ########################################################################### _cell_gradient_BC_list = "neumann" def set_cell_gradient_BC(self, BC): """Set boundary conditions for derivative operators acting on cell-centered quantities. This method is used to set zero Dirichlet and/or zero Neumann boundary conditions for differential operators that act on cell-centered quantities. The user may apply the same boundary conditions to all boundaries, or define the boundary conditions of boundary face (x, y and z) separately. The user may also apply boundary conditions to the lower and upper boundary face separately. Cell gradient boundary conditions are enforced when constructing the following properties: - :py:attr:`~discretize.operators.DiffOperators.cell_gradient` - :py:attr:`~discretize.operators.DiffOperators.cell_gradient_x` - :py:attr:`~discretize.operators.DiffOperators.cell_gradient_y` - :py:attr:`~discretize.operators.DiffOperators.cell_gradient_z` - :py:attr:`~discretize.operators.DiffOperators.stencil_cell_gradient` - :py:attr:`~discretize.operators.DiffOperators.stencil_cell_gradient_x` - :py:attr:`~discretize.operators.DiffOperators.stencil_cell_gradient_y` - :py:attr:`~discretize.operators.DiffOperators.stencil_cell_gradient_z` By default, the mesh assumes a zero Neumann boundary condition on the entire boundary. To define robin boundary conditions, see :py:attr:`~discretize.operators.DiffOperators.cell_gradient_weak_form_robin`. Parameters ---------- BC : str or list [dim,] Define the boundary conditions using the string 'dirichlet' for zero Dirichlet conditions and 'neumann' for zero Neumann conditions. See *examples* for several implementations. Examples -------- Here we demonstrate how to apply zero Dirichlet and/or Neumann boundary conditions for cell-centers differential operators. >>> from discretize import TensorMesh >>> mesh = TensorMesh([[(1, 20)], [(1, 20)], [(1, 20)]]) Define zero Neumann conditions for all boundaries >>> BC = 'neumann' >>> mesh.set_cell_gradient_BC(BC) Define zero Dirichlet on y boundaries and zero Neumann otherwise >>> BC = ['neumann', 'dirichlet', 'neumann'] >>> mesh.set_cell_gradient_BC(BC) Define zero Neumann on the bottom x-boundary and zero Dirichlet otherwise >>> BC = [['neumann', 'dirichlet'], 'dirichlet', 'dirichlet'] >>> mesh.set_cell_gradient_BC(BC) """ if isinstance(BC, str): BC = [BC] * self.dim if isinstance(BC, list): if len(BC) != self.dim: raise ValueError("BC list must be the size of your mesh") else: raise TypeError("BC must be a str or a list.") for i, bc_i in enumerate(BC): BC[i] = _validate_BC(bc_i) # ensure we create a new gradient next time we call it self._cell_gradient = None self._cell_gradient_BC = None self._cell_gradient_BC_list = BC return BC @property def stencil_cell_gradient_x(self): r"""Differencing operator along x-direction (cell centers to x-faces). This property constructs a differencing operator along the x-axis that acts on cell centered quantities; i.e. the stencil for the x-component of the cell gradient. The operator computes the differences between the values at adjacent cell centers along the x-direction, and places the result on the x-faces. The operator is a sparse matrix :math:`\mathbf{G_x}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: diff_phi_x = Gx @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **stencil_cell_gradient_x** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **stencil_cell_gradient_x** is called, the boundary conditions are enforced for the differencing operator. Returns ------- (n_faces_x, n_cells) scipy.sparse.csr_matrix The stencil for the x-component of the cell gradient Examples -------- Below, we demonstrate how to set boundary conditions for the x-component cell gradient stencil, construct the operator and apply it to a discrete scalar quantity. The mapping of the operator and its sparsity is also illustrated. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers. In this case, the scalar represents some block within a homogeneous medium. Create a uniform grid >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], "CC") Create a discrete scalar at cell centers >>> centers = mesh.cell_centers >>> phi = np.zeros(mesh.nC) >>> k = (np.abs(mesh.cell_centers[:, 0]) < 10.) & (np.abs(mesh.cell_centers[:, 1]) < 10.) >>> phi[k] = 1. Before constructing the stencil gradient operator, we must define the boundary conditions; zero Neumann for our example. Even though we are only computing the difference along x, we define boundary conditions for all boundary faces. Once the operator is created, it is applied as a matrix-vector product. >>> mesh.set_cell_gradient_BC(['neumann', 'neumann']) >>> Gx = mesh.stencil_cell_gradient_x >>> diff_phi_x = Gx @ phi Now we plot the original scalar, and the differencing taken along the x axes. >>> fig = plt.figure(figsize=(13, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi, ax=ax1) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> v = np.r_[diff_phi_x, np.zeros(mesh.nFy)] # Define vector for plotting fun >>> mesh.plot_image(v, ax=ax2, v_type="Fx") >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Difference (x-axis)", fontsize=14) >>> plt.show() The x-component cell gradient stencil is a sparse differencing matrix that maps from cell centers to x-faces. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements and a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 4)]]) >>> mesh.set_cell_gradient_BC('neumann') >>> fig = plt.figure(figsize=(12, 8)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_grid(ax=ax1) >>> ax1.set_title("Mapping of Stencil", fontsize=14, pad=15) >>> ax1.plot(mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") >>> ax1.plot(mesh.faces_x[:, 0], mesh.faces_x[:, 1], "g>", markersize=8) >>> for ii, loc in zip(range(mesh.nFx), mesh.faces_x): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (centers)', r'$\mathbf{Gx^* \phi}$ (x-faces)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(122) >>> ax2.spy(mesh.stencil_cell_gradient_x) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("X-Face Index", fontsize=12) >>> ax2.set_xlabel("Cell Index", fontsize=12) >>> plt.show() """ BC = ["neumann", "neumann"] if self.dim == 1: G1 = _ddxCellGrad(self.shape_cells[0], BC) elif self.dim == 2: G1 = sp.kron( speye(self.shape_cells[1]), _ddxCellGrad(self.shape_cells[0], BC) ) elif self.dim == 3: G1 = kron3( speye(self.shape_cells[2]), speye(self.shape_cells[1]), _ddxCellGrad(self.shape_cells[0], BC), ) return G1 @property def stencil_cell_gradient_y(self): r"""Differencing operator along y-direction (cell centers to y-faces). This property constructs a differencing operator along the x-axis that acts on cell centered quantities; i.e. the stencil for the y-component of the cell gradient. The operator computes the differences between the values at adjacent cell centers along the y-direction, and places the result on the y-faces. The operator is a sparse matrix :math:`\mathbf{G_y}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: diff_phi_y = Gy @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **stencil_cell_gradient_y** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **stencil_cell_gradient_y** is called, the boundary conditions are enforced for the differencing operator. Returns ------- (n_faces_y, n_cells) scipy.sparse.csr_matrix The stencil for the y-component of the cell gradient Examples -------- Below, we demonstrate how to set boundary conditions for the y-component cell gradient stencil, construct the operator and apply it to a discrete scalar quantity. The mapping of the operator and its sparsity is also illustrated. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers. In this case, the scalar represents some block within a homogeneous medium. Create a uniform grid >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], "CC") Create a discrete scalar at cell centers >>> centers = mesh.cell_centers >>> phi = np.zeros(mesh.nC) >>> k = (np.abs(mesh.cell_centers[:, 0]) < 10.) & (np.abs(mesh.cell_centers[:, 1]) < 10.) >>> phi[k] = 1. Before constructing the operator, we must define the boundary conditions; zero Neumann for our example. Even though we are only computing the difference along y, we define boundary conditions for all boundary faces. Once the operator is created, it is applied as a matrix-vector product. >>> mesh.set_cell_gradient_BC(['neumann', 'neumann']) >>> Gy = mesh.stencil_cell_gradient_y >>> diff_phi_y = Gy @ phi Now we plot the original scalar, and the differencing taken along the y-axis. >>> fig = plt.figure(figsize=(13, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi, ax=ax1) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> v = np.r_[np.zeros(mesh.nFx), diff_phi_y] # Define vector for plotting fun >>> mesh.plot_image(v, ax=ax2, v_type="Fy") >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Difference (y-axis)", fontsize=14) >>> plt.show() The y-component cell gradient stencil is a sparse differencing matrix that maps from cell centers to y-faces. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements and a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 4)]]) >>> mesh.set_cell_gradient_BC('neumann') >>> fig = plt.figure(figsize=(12, 8)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_grid(ax=ax1) >>> ax1.set_title("Mapping of Stencil", fontsize=14, pad=15) >>> ax1.plot(mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") >>> ax1.plot(mesh.faces_y[:, 0], mesh.faces_y[:, 1], "g^", markersize=8) >>> for ii, loc in zip(range(mesh.nFy), mesh.faces_y): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii + mesh.nFx), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (centers)', r'$\mathbf{Gy^* \phi}$ (y-faces)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(122) >>> ax2.spy(mesh.stencil_cell_gradient_y) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Y-Face Index", fontsize=12) >>> ax2.set_xlabel("Cell Index", fontsize=12) >>> plt.show() """ if self.dim < 2: return None BC = ["neumann", "neumann"] # TODO: remove this hard-coding n = self.vnC if self.dim == 2: G2 = sp.kron(_ddxCellGrad(n[1], BC), speye(n[0])) elif self.dim == 3: G2 = kron3(speye(n[2]), _ddxCellGrad(n[1], BC), speye(n[0])) return G2 @property def stencil_cell_gradient_z(self): r"""Differencing operator along z-direction (cell centers to z-faces). This property constructs a differencing operator along the z-axis that acts on cell centered quantities; i.e. the stencil for the z-component of the cell gradient. The operator computes the differences between the values at adjacent cell centers along the z-direction, and places the result on the z-faces. The operator is a sparse matrix :math:`\mathbf{G_z}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: diff_phi_z = Gz @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **stencil_cell_gradient_z** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **stencil_cell_gradient_z** is called, the boundary conditions are enforced for the differencing operator. Returns ------- (n_faces_z, n_cells) scipy.sparse.csr_matrix The stencil for the z-component of the cell gradient Examples -------- Below, we demonstrate how to set boundary conditions for the z-component cell gradient stencil, construct the operator and apply it to a discrete scalar quantity. The mapping of the operator and its sparsity is also illustrated. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers. In this case, the scalar represents some block within a homogeneous medium. Create a uniform grid >>> h = np.ones(40) >>> mesh = TensorMesh([h, h, h], "CCC") Create a discrete scalar at cell centers >>> centers = mesh.cell_centers >>> phi = np.zeros(mesh.nC) >>> k = ( ... (np.abs(mesh.cell_centers[:, 0]) < 10.) & ... (np.abs(mesh.cell_centers[:, 1]) < 10.) & ... (np.abs(mesh.cell_centers[:, 2]) < 10.) ... ) >>> phi[k] = 1. Before constructing the operator, we must define the boundary conditions; zero Neumann for our example. Even though we are only computing the difference along z, we define boundary conditions for all boundary faces. Once the operator is created, it is applied as a matrix-vector product. >>> mesh.set_cell_gradient_BC(['neumann', 'neumann', 'neumann']) >>> Gz = mesh.stencil_cell_gradient_z >>> diff_phi_z = Gz @ phi Now we plot the original scalar, and the differencing taken along the z-axis for a slice at y = 0. >>> fig = plt.figure(figsize=(13, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_slice(phi, ax=ax1, normal='Y', slice_loc=0) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> v = np.r_[np.zeros(mesh.nFx+mesh.nFy), diff_phi_z] # Define vector for plotting fun >>> mesh.plot_slice(v, ax=ax2, v_type='Fz', normal='Y', slice_loc=0) >>> ax2.set_title("Difference (z-axis)", fontsize=14) >>> plt.show() The z-component cell gradient stencil is a sparse differencing matrix that maps from cell centers to z-faces. To demonstrate this, we provide a spy plot >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(mesh.stencil_cell_gradient_z, ms=1) >>> ax1.set_title("Spy Plot", fontsize=16, pad=5) >>> ax1.set_xlabel("Cell Index", fontsize=12) >>> ax1.set_ylabel("Z-Face Index", fontsize=12) >>> plt.show() """ if self.dim < 3: return None BC = ["neumann", "neumann"] # TODO: remove this hard-coding n = self.vnC G3 = kron3(_ddxCellGrad(n[2], BC), speye(n[1]), speye(n[0])) return G3 @property def stencil_cell_gradient(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh BC = self.set_cell_gradient_BC(self._cell_gradient_BC_list) if self.dim == 1: G = _ddxCellGrad(self.shape_cells[0], BC[0]) elif self.dim == 2: G1 = sp.kron( speye(self.shape_cells[1]), _ddxCellGrad(self.shape_cells[0], BC[0]) ) G2 = sp.kron( _ddxCellGrad(self.shape_cells[1], BC[1]), speye(self.shape_cells[0]) ) G = sp.vstack((G1, G2), format="csr") elif self.dim == 3: G1 = kron3( speye(self.shape_cells[2]), speye(self.shape_cells[1]), _ddxCellGrad(self.shape_cells[0], BC[0]), ) G2 = kron3( speye(self.shape_cells[2]), _ddxCellGrad(self.shape_cells[1], BC[1]), speye(self.shape_cells[0]), ) G3 = kron3( _ddxCellGrad(self.shape_cells[2], BC[2]), speye(self.shape_cells[1]), speye(self.shape_cells[0]), ) G = sp.vstack((G1, G2, G3), format="csr") return G @property def cell_gradient(self): r"""Cell gradient operator (cell centers to faces). This property constructs the 2nd order numerical gradient operator that maps from cell centers to faces. The operator is a sparse matrix :math:`\mathbf{G_c}` that can be applied as a matrix-vector product to a discrete scalar quantity :math:`\boldsymbol{\phi}` that lives at the cell centers; i.e.:: grad_phi = Gc @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **cell_gradient** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **cell_gradient** is called, the boundary conditions are enforced for the gradient operator. Once constructed, the operator is stored as a property of the mesh. *See notes*. This operator is defined mostly as a helper property, and is not necessarily recommended to use when solving PDE's. Returns ------- (n_faces, n_cells) scipy.sparse.csr_matrix The numerical gradient operator from cell centers to faces Notes ----- In continuous space, the gradient operator is defined as: .. math:: \vec{u} = \nabla \phi = \frac{\partial \phi}{\partial x}\hat{x} + \frac{\partial \phi}{\partial y}\hat{y} + \frac{\partial \phi}{\partial z}\hat{z} Where :math:`\boldsymbol{\phi}` is the discrete representation of the continuous variable :math:`\phi` at cell centers and :math:`\mathbf{u}` is the discrete representation of :math:`\vec{u}` on the faces, **cell_gradient** constructs a discrete linear operator :math:`\mathbf{G_c}` such that: .. math:: \mathbf{u} = \mathbf{G_c} \, \boldsymbol{\phi} Second order ghost points are used to enforce boundary conditions and map appropriately to boundary faces. Along each axes direction, we are effectively computing the derivative by taking the difference between the values at adjacent cell centers and dividing by their distance. Examples -------- Below, we demonstrate how to set boundary conditions for the cell gradient operator, construct the cell gradient operator and apply it to a discrete scalar quantity. The mapping of the operator and its sparsity is also illustrated. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers which is zero on the boundaries (zero Dirichlet). Create a uniform grid >>> h = np.ones(20) >>> mesh = TensorMesh([h, h], "CC") Create a discrete scalar on nodes >>> centers = mesh.cell_centers >>> phi = np.exp(-(centers[:, 0] ** 2 + centers[:, 1] ** 2) / 4 ** 2) Once the operator is created, the gradient is performed as a matrix-vector product. Construct the gradient operator and apply to vector >>> Gc = mesh.cell_gradient >>> grad_phi = Gc @ phi Plot the results >>> fig = plt.figure(figsize=(13, 6)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi, ax=ax1) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image( ... grad_phi, ax=ax2, v_type="F", view="vec", ... stream_opts={"color": "w", "density": 1.0} ... ) >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Gradient at faces", fontsize=14) >>> plt.show() The cell gradient operator is a sparse matrix that maps from cell centers to faces. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements in the original discrete quantity :math:`\boldsymbol{\phi}` and its discrete gradient as well as a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 6)]]) >>> mesh.set_cell_gradient_BC('dirichlet') >>> fig = plt.figure(figsize=(12, 10)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_grid(ax=ax1) >>> ax1.set_title("Mapping of Gradient Operator", fontsize=14, pad=15) >>> ax1.plot(mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") >>> ax1.plot(mesh.faces_x[:, 0], mesh.faces_x[:, 1], "g^", markersize=8) >>> for ii, loc in zip(range(mesh.nFx), mesh.faces_x): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="g") >>> ax1.plot(mesh.faces_y[:, 0], mesh.faces_y[:, 1], "g>", markersize=8) >>> for ii, loc in zip(range(mesh.nFy), mesh.faces_y): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format((ii + mesh.nFx)), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (centers)', r'$\mathbf{u}$ (faces)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(122) >>> ax2.spy(mesh.cell_gradient) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Face Index", fontsize=12) >>> ax2.set_xlabel("Cell Index", fontsize=12) >>> plt.show() """ if getattr(self, "_cell_gradient", None) is None: G = self.stencil_cell_gradient S = self.face_areas # Compute areas of cell faces & volumes V = ( self.aveCC2F * self.cell_volumes ) # Average volume between adjacent cells self._cell_gradient = sdiag(S / V) * G return self._cell_gradient def cell_gradient_weak_form_robin(self, alpha=0.0, beta=1.0, gamma=0.0): r"""Create Robin conditions pieces for weak form of the cell gradient operator (cell centers to faces). This method returns the pieces required to impose Robin boundary conditions for the discrete weak form gradient operator that maps from cell centers to faces. These pieces are needed when constructing the discrete representation of the inner product :math:`\langle \vec{u} , \nabla \phi \rangle` according to the finite volume method. Where general boundary conditions are defined on :math:`\phi` according to the Robin condition: .. math:: \alpha \phi + \beta \frac{\partial \phi}{\partial n} = \gamma the user supplies values for :math:`\alpha`, :math:`\beta` and :math:`\gamma` for all boundary faces. **cell_gradient_weak_form_robin** returns the matrix :math:`\mathbf{B}` and vector :math:`\mathbf{b}` required for the discrete representation of :math:`\langle \vec{u} , \nabla \phi \rangle`. *See the notes section for a comprehensive description.* Parameters ---------- alpha : scalar or (n_boundary_faces) array_like Defines :math:`\alpha` for Robin boundary condition. Can be defined as a scalar or array_like. If array_like, the length of the array must be equal to the number of boundary faces. beta : scalar or (n_boundary_faces) array_like Defines :math:`\beta` for Robin boundary condition. Can be defined as a scalar or array_like. If array_like, must have the same length as *alpha* . gamma: scalar or (n_boundary_faces) array_like or (n_boundary_faces, n_rhs) array_like Defines :math:`\gamma` for Robin boundary condition. If array like, *gamma* can have shape (n_boundary_face,). Can also have shape (n_boundary_faces, n_rhs) if multiple systems have the same *alpha* and *beta* parameters. Returns ------- B : (n_faces, n_cells) scipy.sparse.csr_matrix A sparse matrix dependent on the values of *alpha*, *beta* and *gamma* supplied b : (n_faces) numpy.ndarray or (n_faces, n_rhs) numpy.ndarray A vector dependent on the values of *alpha*, *beta* and *gamma* supplied Notes ----- For the gradient of a scalar :math:`\phi`, the weak form is implemented by taking the inner product with a piecewise-constant test function :math:`\vec{u}` and integrating over the domain: .. math:: \langle \vec{u} , \nabla \phi \rangle \; = \int_\Omega \vec{u} \cdot (\nabla \phi) \, dv For a discrete representation of :math:`\phi` at cell centers, the gradient operator maps from cell centers to faces. To implement the boundary conditions in this case, we must use integration by parts and re-express the inner product as: .. math:: \langle \vec{u} , \nabla \phi \rangle \; = - \int_V \phi \, (\nabla \cdot \vec{u} ) \, dV + \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da where the Robin condition is applied to :math:`\phi` on the boundary. The discrete approximation to the above expression is given by: .. math:: \langle \vec{u} , \nabla \phi \rangle \; \approx - \boldsymbol{u^T \big ( D^T M_c - B \big ) \phi + u^T b} where :math:`\mathbf{D}` is the :py:attr:`~discretize.operators.DiffOperators.face_divergence` and :math:`\mathbf{M_c}` is the cell center inner product matrix (just a diagonal matrix comprised of the cell volumes). **cell_gradient_weak_form_robin** returns the matrix :math:`\mathbf{B}` and vector :math:`\mathbf{b}` based on the parameters *alpha* , *beta* and *gamma* provided. Examples -------- Here we form all of pieces required to construct the discrete representation of the inner product between :math:`\mathbf{u}` for specified Robin boundary conditions. We define :math:`\boldsymbol{\phi}` at cell centers and :math:`\mathbf{u}` on the faces. We begin by creating a small 2D tensor mesh: >>> from discretize import TensorMesh >>> import numpy as np >>> import scipy.sparse as sp >>> h = np.ones(32) >>> mesh = TensorMesh([h, h]) We then define `alpha`, `beta`, and `gamma` parameters for a zero Dirichlet condition on the boundary faces. This corresponds to setting: >>> alpha = 1.0 >>> beta = 0.0 >>> gamma = 0.0 Next, we construct all of the necessary pieces required to take the discrete inner product: >>> B, b = mesh.cell_gradient_weak_form_robin(alpha, beta, gamma) >>> Mc = sp.diags(mesh.cell_volumes) >>> Df = mesh.face_divergence In practice, these pieces are usually re-arranged when used to solve PDEs with the finite volume method. However, if you wanted to create a function which computes the discrete inner product for any :math:`\mathbf{u}` and :math:`\boldsymbol{\phi}`: >>> def inner_product(u, phi): ... return u @ (-Df.T @ Mc + B) @ phi + u @ b """ # get length between boundary cell_centers and boundary_faces Pf = self.project_face_to_boundary_face aveC2BF = Pf @ self.average_cell_to_face # distance from cell centers to ghost point on boundary faces if self.dim == 1: h = np.abs(self.boundary_faces - aveC2BF @ self.cell_centers) else: h = np.linalg.norm( self.boundary_faces - aveC2BF @ self.cell_centers, axis=1 ) # for the ghost point u_k = a*u_i + b where a = beta / h / (alpha + beta / h) A = sp.diags(a) @ aveC2BF gamma = np.asarray(gamma) if gamma.ndim > 1: b = (gamma) / (alpha + beta / h)[:, None] else: b = (gamma) / (alpha + beta / h) # value at boundary = A*cells + b M = self.boundary_face_scalar_integral A = M @ A b = M @ b return A, b @property def cell_gradient_BC(self): """Boundary conditions matrix for the cell gradient operator (Deprecated).""" warnings.warn( "cell_gradient_BC is deprecated and is not longer used. See cell_gradient", stacklevel=2, ) if getattr(self, "_cell_gradient_BC", None) is None: BC = self.set_cell_gradient_BC(self._cell_gradient_BC_list) n = self.vnC if self.dim == 1: G = _ddxCellGradBC(n[0], BC[0]) elif self.dim == 2: G1 = sp.kron(speye(n[1]), _ddxCellGradBC(n[0], BC[0])) G2 = sp.kron(_ddxCellGradBC(n[1], BC[1]), speye(n[0])) G = sp.block_diag((G1, G2), format="csr") elif self.dim == 3: G1 = kron3(speye(n[2]), speye(n[1]), _ddxCellGradBC(n[0], BC[0])) G2 = kron3(speye(n[2]), _ddxCellGradBC(n[1], BC[1]), speye(n[0])) G3 = kron3(_ddxCellGradBC(n[2], BC[2]), speye(n[1]), speye(n[0])) G = sp.block_diag((G1, G2, G3), format="csr") # Compute areas of cell faces & volumes S = self.face_areas V = ( self.aveCC2F * self.cell_volumes ) # Average volume between adjacent cells self._cell_gradient_BC = sdiag(S / V) * G return self._cell_gradient_BC @property def cell_gradient_x(self): r"""X-derivative operator (cell centers to x-faces). This property constructs an x-derivative operator that acts on cell centered quantities; i.e. the x-component of the cell gradient operator. When applied, the x-derivative is mapped to x-faces. The operator is a sparse matrix :math:`\mathbf{G_x}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: grad_phi_x = Gx @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **cell_gradient_x** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **cell_gradient_x** is called, the boundary conditions are enforced for the differencing operator. Returns ------- (n_faces_x, n_cells) scipy.sparse.csr_matrix X-derivative operator (x-component of the cell gradient) Examples -------- Below, we demonstrate how to set boundary conditions for the x-component cell gradient, construct the operator and apply it to a discrete scalar quantity. The mapping of the operator and its sparsity is also illustrated. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers. >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], "CC") >>> centers = mesh.cell_centers >>> phi = np.exp(-(centers[:, 0] ** 2) / 8** 2) Before constructing the operator, we must define the boundary conditions; zero Neumann for our example. Even though we are only computing the derivative along x, we define boundary conditions for all boundary faces. Once the operator is created, it is applied as a matrix-vector product. >>> mesh.set_cell_gradient_BC(['neumann', 'neumann']) >>> Gx = mesh.stencil_cell_gradient_x >>> grad_phi_x = Gx @ phi Now we plot the original scalar, and the x-derivative. >>> fig = plt.figure(figsize=(13, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi, ax=ax1) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> v = np.r_[grad_phi_x, np.zeros(mesh.nFy)] # Define vector for plotting fun >>> mesh.plot_image(v, ax=ax2, v_type="Fx") >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("X-derivative at x-faces", fontsize=14) >>> plt.show() The operator is a sparse x-derivative matrix that maps from cell centers to x-faces. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements and a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 4)]]) >>> mesh.set_cell_gradient_BC('neumann') >>> fig = plt.figure(figsize=(12, 8)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_grid(ax=ax1) >>> ax1.set_title("Mapping of Operator", fontsize=14, pad=15) >>> ax1.plot(mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") >>> ax1.plot(mesh.faces_x[:, 0], mesh.faces_x[:, 1], "g>", markersize=8) >>> for ii, loc in zip(range(mesh.nFx), mesh.faces_x): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (centers)', r'$\mathbf{Gx^* \phi}$ (x-faces)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(122) >>> ax2.spy(mesh.cell_gradient_x) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("X-Face Index", fontsize=12) >>> ax2.set_xlabel("Cell Index", fontsize=12) >>> plt.show() """ if getattr(self, "_cell_gradient_x", None) is None: G1 = self.stencil_cell_gradient_x # Compute areas of cell faces & volumes V = self.aveCC2F * self.cell_volumes L = self.reshape(self.face_areas / V, "F", "Fx", "V") self._cell_gradient_x = sdiag(L) * G1 return self._cell_gradient_x @property def cell_gradient_y(self): r"""Y-derivative operator (cell centers to y-faces). This property constructs a y-derivative operator that acts on cell centered quantities; i.e. the y-component of the cell gradient operator. When applied, the y-derivative is mapped to y-faces. The operator is a sparse matrix :math:`\mathbf{G_y}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: grad_phi_y = Gy @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **cell_gradient_y** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **cell_gradient_y** is called, the boundary conditions are enforced for the differencing operator. Returns ------- (n_faces_y, n_cells) scipy.sparse.csr_matrix Y-derivative operator (y-component of the cell gradient) Examples -------- Below, we demonstrate how to set boundary conditions for the y-component cell gradient, construct the operator and apply it to a discrete scalar quantity. The mapping of the operator and its sparsity is also illustrated. Our example is carried out on a 2D mesh but it can be done equivalently for a 3D mesh. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers. >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], "CC") >>> centers = mesh.cell_centers >>> phi = np.exp(-(centers[:, 1] ** 2) / 8** 2) Before constructing the operator, we must define the boundary conditions; zero Neumann for our example. Even though we are only computing the derivative along y, we define boundary conditions for all boundary faces. Once the operator is created, it is applied as a matrix-vector product. >>> mesh.set_cell_gradient_BC(['neumann', 'neumann']) >>> Gy = mesh.cell_gradient_y >>> grad_phi_y = Gy @ phi Now we plot the original scalar is y-derivative. >>> fig = plt.figure(figsize=(13, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_image(phi, ax=ax1) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> v = np.r_[np.zeros(mesh.nFx), grad_phi_y] # Define vector for plotting fun >>> mesh.plot_image(v, ax=ax2, v_type="Fy") >>> ax2.set_yticks([]) >>> ax2.set_ylabel("") >>> ax2.set_title("Y-derivative at y-faces", fontsize=14) >>> plt.show() The operator is a sparse y-derivative matrix that maps from cell centers to y-faces. To demonstrate this, we construct a small 2D mesh. We then show the ordering of the elements and a spy plot. >>> mesh = TensorMesh([[(1, 3)], [(1, 4)]]) >>> mesh.set_cell_gradient_BC('neumann') >>> fig = plt.figure(figsize=(12, 8)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_grid(ax=ax1) >>> ax1.set_title("Mapping of Operator", fontsize=14, pad=15) >>> ax1.plot(mesh.cell_centers[:, 0], mesh.cell_centers[:, 1], "ro", markersize=8) >>> for ii, loc in zip(range(mesh.nC), mesh.cell_centers): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii), color="r") >>> ax1.plot(mesh.faces_y[:, 0], mesh.faces_y[:, 1], "g^", markersize=8) >>> for ii, loc in zip(range(mesh.nFy), mesh.faces_y): ... ax1.text(loc[0] + 0.05, loc[1] + 0.02, "{0:d}".format(ii + mesh.nFx), color="g") >>> ax1.set_xticks([]) >>> ax1.set_yticks([]) >>> ax1.spines['bottom'].set_color('white') >>> ax1.spines['top'].set_color('white') >>> ax1.spines['left'].set_color('white') >>> ax1.spines['right'].set_color('white') >>> ax1.set_xlabel('X', fontsize=16, labelpad=-5) >>> ax1.set_ylabel('Y', fontsize=16, labelpad=-15) >>> ax1.legend( ... ['Mesh', r'$\mathbf{\phi}$ (centers)', r'$\mathbf{Gy^* \phi}$ (y-faces)'], ... loc='upper right', fontsize=14 ... ) >>> ax2 = fig.add_subplot(122) >>> ax2.spy(mesh.stencil_cell_gradient_y) >>> ax2.set_title("Spy Plot", fontsize=14, pad=5) >>> ax2.set_ylabel("Y-Face Index", fontsize=12) >>> ax2.set_xlabel("Cell Index", fontsize=12) >>> plt.show() """ if self.dim < 2: return None if getattr(self, "_cell_gradient_y", None) is None: G2 = self.stencil_cell_gradient_y # Compute areas of cell faces & volumes V = self.aveCC2F * self.cell_volumes L = self.reshape(self.face_areas / V, "F", "Fy", "V") self._cell_gradient_y = sdiag(L) * G2 return self._cell_gradient_y @property def cell_gradient_z(self): r"""Z-derivative operator (cell centers to z-faces). This property constructs an z-derivative operator that acts on cell centered quantities; i.e. the z-component of the cell gradient operator. When applied, the z-derivative is mapped to z-faces. The operator is a sparse matrix :math:`\mathbf{G_z}` that can be applied as a matrix-vector product to a cell centered quantity :math:`\boldsymbol{\phi}`, i.e.:: grad_phi_z = Gz @ phi By default, the operator assumes zero-Neumann boundary conditions on the scalar quantity. Before calling **cell_gradient_z** however, the user can set a mix of zero Dirichlet and zero Neumann boundary conditions using :py:attr:`~discretize.operators.DiffOperators.set_cell_gradient_BC`. When **cell_gradient_z** is called, the boundary conditions are enforced for the differencing operator. Returns ------- (n_faces_z, n_cells) scipy.sparse.csr_matrix Z-derivative operator (z-component of the cell gradient) Examples -------- Below, we demonstrate how to set boundary conditions for the z-component of the cell gradient, construct the operator and apply it to a discrete scalar quantity. The mapping of the operator and its sparsity is also illustrated. We start by importing the necessary packages and modules. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl We then construct a mesh and define a scalar function at cell centers. >>> h = np.ones(40) >>> mesh = TensorMesh([h, h, h], "CCC") >>> centers = mesh.cell_centers >>> phi = np.exp(-(centers[:, 2] ** 2) / 8** 2) Before constructing the operator, we must define the boundary conditions; zero Neumann for our example. Even though we are only computing the derivative along z, we define boundary conditions for all boundary faces. Once the operator is created, it is applied as a matrix-vector product. >>> mesh.set_cell_gradient_BC(['neumann', 'neumann', 'neumann']) >>> Gz = mesh.cell_gradient_z >>> grad_phi_z = Gz @ phi Now we plot the original scalar and the z-derivative for a slice at y = 0. >>> fig = plt.figure(figsize=(13, 5)) >>> ax1 = fig.add_subplot(121) >>> mesh.plot_slice(phi, ax=ax1, normal='Y', slice_loc=0) >>> ax1.set_title("Scalar at cell centers", fontsize=14) >>> ax2 = fig.add_subplot(122) >>> v = np.r_[np.zeros(mesh.nFx+mesh.nFy), grad_phi_z] # Define vector for plotting fun >>> mesh.plot_slice(v, ax=ax2, v_type='Fz', normal='Y', slice_loc=0) >>> ax2.set_title("Z-derivative (z-faces)", fontsize=14) >>> plt.show() The z-component cell gradient is a sparse derivative matrix that maps from cell centers to z-faces. To demonstrate this, we provide a spy plot >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(mesh.cell_gradient_z, ms=1) >>> ax1.set_title("Spy Plot", fontsize=16, pad=5) >>> ax1.set_xlabel("Cell Index", fontsize=12) >>> ax1.set_ylabel("Z-Face Index", fontsize=12) >>> plt.show() """ if self.dim < 3: return None if getattr(self, "_cell_gradient_z", None) is None: G3 = self.stencil_cell_gradient_z # Compute areas of cell faces & volumes V = self.aveCC2F * self.cell_volumes L = self.reshape(self.face_areas / V, "F", "Fz", "V") self._cell_gradient_z = sdiag(L) * G3 return self._cell_gradient_z ########################################################################### # # # Edge Curl # # # ########################################################################### @property def _edge_x_curl_stencil(self): """Stencil for the edge curl operator in the x-direction.""" n = self.vnC # The number of cell centers in each direction D32 = kron3(ddx(n[2]), speye(n[1]), speye(n[0] + 1)) D23 = kron3(speye(n[2]), ddx(n[1]), speye(n[0] + 1)) # O1 = spzeros(np.shape(D32)[0], np.shape(D31)[1]) O1 = spzeros((n[0] + 1) * n[1] * n[2], n[0] * (n[1] + 1) * (n[2] + 1)) return sp.hstack((O1, -D32, D23)) @property def _edge_y_curl_stencil(self): """Stencil for the edge curl operator in the y-direction.""" n = self.vnC # The number of cell centers in each direction D31 = kron3(ddx(n[2]), speye(n[1] + 1), speye(n[0])) D13 = kron3(speye(n[2]), speye(n[1] + 1), ddx(n[0])) # O2 = spzeros(np.shape(D31)[0], np.shape(D32)[1]) O2 = spzeros(n[0] * (n[1] + 1) * n[2], (n[0] + 1) * n[1] * (n[2] + 1)) return sp.hstack((D31, O2, -D13)) @property def _edge_z_curl_stencil(self): """Stencil for the edge curl operator in the z-direction.""" n = self.vnC # The number of cell centers in each direction D21 = kron3(speye(n[2] + 1), ddx(n[1]), speye(n[0])) D12 = kron3(speye(n[2] + 1), speye(n[1]), ddx(n[0])) # O3 = spzeros(np.shape(D21)[0], np.shape(D13)[1]) O3 = spzeros(n[0] * n[1] * (n[2] + 1), (n[0] + 1) * (n[1] + 1) * n[2]) return sp.hstack((-D21, D12, O3)) @property def _edge_curl_stencil(self): """Full stencil for the edge curl operator.""" if self.dim <= 1: raise NotImplementedError("Edge Curl only programed for 2 or 3D.") # Compute divergence operator on faces if self.dim == 2: n = self.vnC # The number of cell centers in each direction D21 = sp.kron(ddx(n[1]), speye(n[0])) D12 = sp.kron(speye(n[1]), ddx(n[0])) C = sp.hstack((-D21, D12), format="csr") return C elif self.dim == 3: # D32 = kron3(ddx(n[2]), speye(n[1]), speye(n[0]+1)) # D23 = kron3(speye(n[2]), ddx(n[1]), speye(n[0]+1)) # D31 = kron3(ddx(n[2]), speye(n[1]+1), speye(n[0])) # D13 = kron3(speye(n[2]), speye(n[1]+1), ddx(n[0])) # D21 = kron3(speye(n[2]+1), ddx(n[1]), speye(n[0])) # D12 = kron3(speye(n[2]+1), speye(n[1]), ddx(n[0])) # O1 = spzeros(np.shape(D32)[0], np.shape(D31)[1]) # O2 = spzeros(np.shape(D31)[0], np.shape(D32)[1]) # O3 = spzeros(np.shape(D21)[0], np.shape(D13)[1]) # C = sp.vstack((sp.hstack((O1, -D32, D23)), # sp.hstack((D31, O2, -D13)), # sp.hstack((-D21, D12, O3))), format="csr") C = sp.vstack( ( self._edge_x_curl_stencil, self._edge_y_curl_stencil, self._edge_z_curl_stencil, ), format="csr", ) return C @property def edge_curl(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_edge_curl", None) is None: if self.dim <= 1: raise NotImplementedError("Edge Curl only programed for 2 or 3D.") L = self.edge_lengths # Compute lengths of cell edges if self.dim == 2: S = self.cell_volumes elif self.dim == 3: S = self.face_areas self._edge_curl = sdiag(1 / S) @ self._edge_curl_stencil @ sdiag(L) return self._edge_curl @property def boundary_face_scalar_integral(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 1: return sp.csr_matrix( ([-1, 1], ([0, self.n_faces_x - 1], [0, 1])), shape=(self.n_faces_x, 2) ) P = self.project_face_to_boundary_face w_h_dot_normal = np.sum( (P @ self.face_normals) * self.boundary_face_outward_normals, axis=-1 ) A = sp.diags(self.face_areas) @ P.T @ sp.diags(w_h_dot_normal) return A @property def boundary_edge_vector_integral(self): r"""Integrate a vector function on the boundary. This matrix represents the boundary surface integral of a vector function multiplied with a finite volume test function on the mesh. In 1D and 2D, the operation assumes that the right array contains only a single component of the vector ``u``. In 3D, however, we must assume that ``u`` will contain each of the three vector components, and it must be ordered as, ``[edges_1_x, ... ,edge_N_x, edge_1_y, ..., edge_N_y, edge_1_z, ..., edge_N_z]`` , where ``N`` is the number of boundary edges. Returns ------- scipy.sparse.csr_matrix Sparse matrix of shape (n_edges, n_boundary_edges) for 1D or 2D mesh, (n_edges, 3*n_boundary_edges) for a 3D mesh. Notes ----- The integral we are representing on the boundary of the mesh is .. math:: \int_{\Omega} \vec{w} \cdot (\vec{u} \times \hat{n}) \partial \Omega In discrete form this is: .. math:: w^T * P @ u_b where `w` is defined on all edges, and `u_b` is all three components defined on boundary edges. """ Pe = self.project_edge_to_boundary_edge Pf = self.project_face_to_boundary_face dA = self.boundary_face_outward_normals * (Pf @ self.face_areas)[:, None] w = Pe @ self.edge_tangents n_boundary_edges = len(w) Av = Pf @ self.average_edge_to_face @ Pe.T if self.dim > 2: Av *= 2 av_da = Av.T @ dA if self.dim == 2: w_cross_n = cross2d(av_da, w) else: w_cross_n = np.cross(av_da, w) if self.dim == 2: return Pe.T @ sp.diags(w_cross_n, format="csr") return Pe.T @ sp.diags( w_cross_n.T, n_boundary_edges * np.arange(3), shape=(n_boundary_edges, 3 * n_boundary_edges), ) @property def boundary_node_vector_integral(self): r"""Integrate a vector function dotted with the boundary normal. This matrix represents the boundary surface integral of a vector function dotted with the boundary normal and multiplied with a scalar finite volume test function on the mesh. Returns ------- (n_nodes, dim * n_boundary_nodes) scipy.sparse.csr_matrix Sparse matrix of shape. Notes ----- The integral we are representing on the boundary of the mesh is .. math:: \int_{\Omega} (w \vec{u}) \cdot \hat{n} \partial \Omega In discrete form this is: .. math:: w^T * P @ u_b where `w` is defined on all nodes, and `u_b` is all three components defined on boundary nodes. """ if self.dim == 1: return sp.csr_matrix( ([-1, 1], ([0, self.shape_nodes[0] - 1], [0, 1])), shape=(self.shape_nodes[0], 2), ) Pn = self.project_node_to_boundary_node Pf = self.project_face_to_boundary_face n_boundary_nodes = Pn.shape[0] dA = self.boundary_face_outward_normals * (Pf @ self.face_areas)[:, None] Av = Pf @ self.average_node_to_face @ Pn.T u_dot_ds = Av.T @ dA diags = u_dot_ds.T offsets = n_boundary_nodes * np.arange(self.dim) return Pn.T @ sp.diags( diags, offsets, shape=(n_boundary_nodes, self.dim * n_boundary_nodes) ) def get_BC_projections(self, BC, discretization="CC"): """Create the weak form boundary condition projection matrices. Examples -------- .. code:: python # Neumann in all directions BC = 'neumann' # 3D, Dirichlet in y Neumann else BC = ['neumann', 'dirichlet', 'neumann'] # 3D, Neumann in x on bottom of domain, Dirichlet else BC = [['neumann', 'dirichlet'], 'dirichlet', 'dirichlet'] """ if discretization != "CC": raise NotImplementedError( "Boundary conditions only implemented" "for CC discretization." ) if isinstance(BC, str): BC = [BC for _ in self.vnC] # Repeat the str self.dim times elif isinstance(BC, list): if len(BC) != self.dim: raise ValueError("BC list must be the size of your mesh") else: raise TypeError("BC must be a str or a list.") for i, bc_i in enumerate(BC): BC[i] = _validate_BC(bc_i) def projDirichlet(n, bc): bc = _validate_BC(bc) ij = ([0, n], [0, 1]) vals = [0, 0] if bc[0] == "dirichlet": vals[0] = -1 if bc[1] == "dirichlet": vals[1] = 1 return sp.csr_matrix((vals, ij), shape=(n + 1, 2)) def projNeumannIn(n, bc): bc = _validate_BC(bc) P = sp.identity(n + 1).tocsr() if bc[0] == "neumann": P = P[1:, :] if bc[1] == "neumann": P = P[:-1, :] return P def projNeumannOut(n, bc): bc = _validate_BC(bc) ij = ([0, 1], [0, n]) vals = [0, 0] if bc[0] == "neumann": vals[0] = 1 if bc[1] == "neumann": vals[1] = 1 return sp.csr_matrix((vals, ij), shape=(2, n + 1)) n = self.vnC indF = self.face_boundary_indices if self.dim == 1: Pbc = projDirichlet(n[0], BC[0]) indF = indF[0] | indF[1] Pbc = Pbc * sdiag(self.face_areas[indF]) Pin = projNeumannIn(n[0], BC[0]) Pout = projNeumannOut(n[0], BC[0]) elif self.dim == 2: Pbc1 = sp.kron(speye(n[1]), projDirichlet(n[0], BC[0])) Pbc2 = sp.kron(projDirichlet(n[1], BC[1]), speye(n[0])) Pbc = sp.block_diag((Pbc1, Pbc2), format="csr") indF = np.r_[(indF[0] | indF[1]), (indF[2] | indF[3])] Pbc = Pbc * sdiag(self.face_areas[indF]) P1 = sp.kron(speye(n[1]), projNeumannIn(n[0], BC[0])) P2 = sp.kron(projNeumannIn(n[1], BC[1]), speye(n[0])) Pin = sp.block_diag((P1, P2), format="csr") P1 = sp.kron(speye(n[1]), projNeumannOut(n[0], BC[0])) P2 = sp.kron(projNeumannOut(n[1], BC[1]), speye(n[0])) Pout = sp.block_diag((P1, P2), format="csr") elif self.dim == 3: Pbc1 = kron3(speye(n[2]), speye(n[1]), projDirichlet(n[0], BC[0])) Pbc2 = kron3(speye(n[2]), projDirichlet(n[1], BC[1]), speye(n[0])) Pbc3 = kron3(projDirichlet(n[2], BC[2]), speye(n[1]), speye(n[0])) Pbc = sp.block_diag((Pbc1, Pbc2, Pbc3), format="csr") indF = np.r_[(indF[0] | indF[1]), (indF[2] | indF[3]), (indF[4] | indF[5])] Pbc = Pbc * sdiag(self.face_areas[indF]) P1 = kron3(speye(n[2]), speye(n[1]), projNeumannIn(n[0], BC[0])) P2 = kron3(speye(n[2]), projNeumannIn(n[1], BC[1]), speye(n[0])) P3 = kron3(projNeumannIn(n[2], BC[2]), speye(n[1]), speye(n[0])) Pin = sp.block_diag((P1, P2, P3), format="csr") P1 = kron3(speye(n[2]), speye(n[1]), projNeumannOut(n[0], BC[0])) P2 = kron3(speye(n[2]), projNeumannOut(n[1], BC[1]), speye(n[0])) P3 = kron3(projNeumannOut(n[2], BC[2]), speye(n[1]), speye(n[0])) Pout = sp.block_diag((P1, P2, P3), format="csr") return Pbc, Pin, Pout def get_BC_projections_simple(self, discretization="CC"): """Create weak form boundary condition projection matrices for mixed boundary condition.""" if discretization != "CC": raise NotImplementedError( "Boundary conditions only implemented" "for CC discretization." ) def projBC(n): ij = ([0, n], [0, 1]) vals = [0, 0] vals[0] = 1 vals[1] = 1 return sp.csr_matrix((vals, ij), shape=(n + 1, 2)) def projDirichlet(n, bc): bc = _validate_BC(bc) ij = ([0, n], [0, 1]) vals = [0, 0] if bc[0] == "dirichlet": vals[0] = -1 if bc[1] == "dirichlet": vals[1] = 1 return sp.csr_matrix((vals, ij), shape=(n + 1, 2)) BC = [ ["dirichlet", "dirichlet"], ["dirichlet", "dirichlet"], ["dirichlet", "dirichlet"], ] n = self.vnC indF = self.face_boundary_indices if self.dim == 1: Pbc = projDirichlet(n[0], BC[0]) B = projBC(n[0]) indF = indF[0] | indF[1] Pbc = Pbc * sdiag(self.face_areas[indF]) elif self.dim == 2: Pbc1 = sp.kron(speye(n[1]), projDirichlet(n[0], BC[0])) Pbc2 = sp.kron(projDirichlet(n[1], BC[1]), speye(n[0])) Pbc = sp.block_diag((Pbc1, Pbc2), format="csr") B1 = sp.kron(speye(n[1]), projBC(n[0])) B2 = sp.kron(projBC(n[1]), speye(n[0])) B = sp.block_diag((B1, B2), format="csr") indF = np.r_[(indF[0] | indF[1]), (indF[2] | indF[3])] Pbc = Pbc * sdiag(self.face_areas[indF]) elif self.dim == 3: Pbc1 = kron3(speye(n[2]), speye(n[1]), projDirichlet(n[0], BC[0])) Pbc2 = kron3(speye(n[2]), projDirichlet(n[1], BC[1]), speye(n[0])) Pbc3 = kron3(projDirichlet(n[2], BC[2]), speye(n[1]), speye(n[0])) Pbc = sp.block_diag((Pbc1, Pbc2, Pbc3), format="csr") B1 = kron3(speye(n[2]), speye(n[1]), projBC(n[0])) B2 = kron3(speye(n[2]), projBC(n[1]), speye(n[0])) B3 = kron3(projBC(n[2]), speye(n[1]), speye(n[0])) B = sp.block_diag((B1, B2, B3), format="csr") indF = np.r_[(indF[0] | indF[1]), (indF[2] | indF[3]), (indF[4] | indF[5])] Pbc = Pbc * sdiag(self.face_areas[indF]) return Pbc, B.T ########################################################################### # # # Averaging # # # ########################################################################### @property def average_face_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_face_to_cell", None) is None: if self.dim == 1: self._average_face_to_cell = self.aveFx2CC elif self.dim == 2: self._average_face_to_cell = (0.5) * sp.hstack( (self.aveFx2CC, self.aveFy2CC), format="csr" ) elif self.dim == 3: self._average_face_to_cell = (1.0 / 3.0) * sp.hstack( (self.aveFx2CC, self.aveFy2CC, self.aveFz2CC), format="csr" ) return self._average_face_to_cell @property def average_face_to_cell_vector(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_face_to_cell_vector", None) is None: if self.dim == 1: self._average_face_to_cell_vector = self.aveFx2CC elif self.dim == 2: self._average_face_to_cell_vector = sp.block_diag( (self.aveFx2CC, self.aveFy2CC), format="csr" ) elif self.dim == 3: self._average_face_to_cell_vector = sp.block_diag( (self.aveFx2CC, self.aveFy2CC, self.aveFz2CC), format="csr" ) return self._average_face_to_cell_vector @property def average_face_x_to_cell(self): r"""Averaging operator from x-faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from x-faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on x-faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_faces_x) scipy.sparse.csr_matrix The scalar averaging operator from x-faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_x}` be a discrete scalar quantity that lives on x-faces. **average_face_x_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{xc}}` that projects :math:`\boldsymbol{\phi_x}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{xc}} \, \boldsymbol{\phi_x} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its x-faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Axc @ phi_x Examples -------- Here we compute the values of a scalar function on the x-faces. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Create a scalar variable on x-faces: >>> phi_x = np.zeros(mesh.nFx) >>> xy = mesh.faces_x >>> phi_x[(xy[:, 1] > 0)] = 25.0 >>> phi_x[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. >>> Axc = mesh.average_face_x_to_cell >>> phi_c = Axc @ phi_x And plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> v = np.r_[phi_x, np.zeros(mesh.nFy)] # create vector for plotting function >>> mesh.plot_image(v, ax=ax1, v_type="Fx") >>> ax1.set_title("Variable at x-faces", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Axc, ms=1) >>> ax1.set_title("X-Face Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ if getattr(self, "_average_face_x_to_cell", None) is None: n = self.vnC if self.dim == 1: self._average_face_x_to_cell = av(n[0]) elif self.dim == 2: self._average_face_x_to_cell = sp.kron(speye(n[1]), av(n[0])) elif self.dim == 3: self._average_face_x_to_cell = kron3(speye(n[2]), speye(n[1]), av(n[0])) return self._average_face_x_to_cell @property def average_face_y_to_cell(self): r"""Averaging operator from y-faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from y-faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on x-faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_faces_y) scipy.sparse.csr_matrix The scalar averaging operator from y-faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_y}` be a discrete scalar quantity that lives on y-faces. **average_face_y_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{yc}}` that projects :math:`\boldsymbol{\phi_y}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{yc}} \, \boldsymbol{\phi_y} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its y-faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Ayc @ phi_y Examples -------- Here we compute the values of a scalar function on the y-faces. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Create a scalar variable on y-faces, >>> phi_y = np.zeros(mesh.nFy) >>> xy = mesh.faces_y >>> phi_y[(xy[:, 1] > 0)] = 25.0 >>> phi_y[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. >>> Ayc = mesh.average_face_y_to_cell >>> phi_c = Ayc @ phi_y And finally, plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> v = np.r_[np.zeros(mesh.nFx), phi_y] # create vector for plotting function >>> mesh.plot_image(v, ax=ax1, v_type="Fy") >>> ax1.set_title("Variable at y-faces", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Ayc, ms=1) >>> ax1.set_title("Y-Face Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ if self.dim < 2: return None if getattr(self, "_average_face_y_to_cell", None) is None: n = self.vnC if self.dim == 2: self._average_face_y_to_cell = sp.kron(av(n[1]), speye(n[0])) elif self.dim == 3: self._average_face_y_to_cell = kron3(speye(n[2]), av(n[1]), speye(n[0])) return self._average_face_y_to_cell @property def average_face_z_to_cell(self): r"""Averaging operator from z-faces to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from z-faces to cell centers. This averaging operator is used when a discrete scalar quantity defined on z-faces must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_faces_z) scipy.sparse.csr_matrix The scalar averaging operator from z-faces to cell centers Notes ----- Let :math:`\boldsymbol{\phi_z}` be a discrete scalar quantity that lives on z-faces. **average_face_z_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{zc}}` that projects :math:`\boldsymbol{\phi_z}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{zc}} \, \boldsymbol{\phi_z} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its z-faces. The operation is implemented as a matrix vector product, i.e.:: phi_c = Azc @ phi_z Examples -------- Here we compute the values of a scalar function on the z-faces. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h, h], x0="CCC") Create a scalar variable on z-faces >>> phi_z = np.zeros(mesh.nFz) >>> xyz = mesh.faces_z >>> phi_z[(xyz[:, 2] > 0)] = 25.0 >>> phi_z[(xyz[:, 2] < -10.0) & (xyz[:, 0] > -10.0) & (xyz[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. We plot the original scalar and its average at cell centers for a slice at y=0. >>> Azc = mesh.average_face_z_to_cell >>> phi_c = Azc @ phi_z And plot the results: >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> v = np.r_[np.zeros(mesh.nFx+mesh.nFy), phi_z] # create vector for plotting >>> mesh.plot_slice(v, ax=ax1, normal='Y', slice_loc=0, v_type="Fz") >>> ax1.set_title("Variable at z-faces", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, normal='Y', slice_loc=0, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Azc, ms=1) >>> ax1.set_title("Z-Face Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ if self.dim < 3: return None if getattr(self, "_average_face_z_to_cell", None) is None: n = self.vnC if self.dim == 3: self._average_face_z_to_cell = kron3(av(n[2]), speye(n[1]), speye(n[0])) return self._average_face_z_to_cell @property def average_cell_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_cell_to_face", None) is None: if self.dim == 1: self._average_cell_to_face = av_extrap(self.shape_cells[0]) elif self.dim == 2: self._average_cell_to_face = sp.vstack( ( sp.kron( speye(self.shape_cells[1]), av_extrap(self.shape_cells[0]) ), sp.kron( av_extrap(self.shape_cells[1]), speye(self.shape_cells[0]) ), ), format="csr", ) elif self.dim == 3: self._average_cell_to_face = sp.vstack( ( kron3( speye(self.shape_cells[2]), speye(self.shape_cells[1]), av_extrap(self.shape_cells[0]), ), kron3( speye(self.shape_cells[2]), av_extrap(self.shape_cells[1]), speye(self.shape_cells[0]), ), kron3( av_extrap(self.shape_cells[2]), speye(self.shape_cells[1]), speye(self.shape_cells[0]), ), ), format="csr", ) return self._average_cell_to_face @property def average_cell_vector_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_cell_vector_to_face", None) is None: if self.dim == 1: self._average_cell_vector_to_face = self.aveCC2F elif self.dim == 2: aveCCV2Fx = sp.kron( speye(self.shape_cells[1]), av_extrap(self.shape_cells[0]) ) aveCC2VFy = sp.kron( av_extrap(self.shape_cells[1]), speye(self.shape_cells[0]) ) self._average_cell_vector_to_face = sp.block_diag( (aveCCV2Fx, aveCC2VFy), format="csr" ) elif self.dim == 3: aveCCV2Fx = kron3( speye(self.shape_cells[2]), speye(self.shape_cells[1]), av_extrap(self.shape_cells[0]), ) aveCC2VFy = kron3( speye(self.shape_cells[2]), av_extrap(self.shape_cells[1]), speye(self.shape_cells[0]), ) aveCC2BFz = kron3( av_extrap(self.shape_cells[2]), speye(self.shape_cells[1]), speye(self.shape_cells[0]), ) self._average_cell_vector_to_face = sp.block_diag( (aveCCV2Fx, aveCC2VFy, aveCC2BFz), format="csr" ) return self._average_cell_vector_to_face @property def average_cell_to_edge(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_cell_to_edge", None) is None: n = self.shape_cells if self.dim == 1: avg = sp.eye(n[0]) elif self.dim == 2: avg = sp.vstack( ( sp.kron(av_extrap(n[1]), speye(n[0])), sp.kron(speye(n[1]), av_extrap(n[0])), ), format="csr", ) elif self.dim == 3: avg = sp.vstack( ( kron3(av_extrap(n[2]), av_extrap(n[1]), speye(n[0])), kron3(av_extrap(n[2]), speye(n[1]), av_extrap(n[0])), kron3(speye(n[2]), av_extrap(n[1]), av_extrap(n[0])), ), format="csr", ) self._average_cell_to_edge = avg return self._average_cell_to_edge @property def average_edge_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_edge_to_cell", None) is None: if self.dim == 1: self._avE2CC = self.aveEx2CC elif self.dim == 2: self._avE2CC = 0.5 * sp.hstack( (self.aveEx2CC, self.aveEy2CC), format="csr" ) elif self.dim == 3: self._avE2CC = (1.0 / 3) * sp.hstack( (self.aveEx2CC, self.aveEy2CC, self.aveEz2CC), format="csr" ) return self._avE2CC @property def average_edge_to_cell_vector(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_edge_to_cell_vector", None) is None: if self.dim == 1: self._average_edge_to_cell_vector = self.aveEx2CC elif self.dim == 2: self._average_edge_to_cell_vector = sp.block_diag( (self.aveEx2CC, self.aveEy2CC), format="csr" ) elif self.dim == 3: self._average_edge_to_cell_vector = sp.block_diag( (self.aveEx2CC, self.aveEy2CC, self.aveEz2CC), format="csr" ) return self._average_edge_to_cell_vector @property def average_edge_x_to_cell(self): r"""Averaging operator from x-edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from x-edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on x-edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges_x) scipy.sparse.csr_matrix The scalar averaging operator from x-edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_x}` be a discrete scalar quantity that lives on x-edges. **average_edge_x_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{xc}}` that projects :math:`\boldsymbol{\phi_x}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{xc}} \, \boldsymbol{\phi_x} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its x-edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Axc @ phi_x Examples -------- Here we compute the values of a scalar function on the x-edges. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a scalar variable on x-edges, >>> phi_x = np.zeros(mesh.nEx) >>> xy = mesh.edges_x >>> phi_x[(xy[:, 1] > 0)] = 25.0 >>> phi_x[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. >>> Axc = mesh.average_edge_x_to_cell >>> phi_c = Axc @ phi_x And plot the results, >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> v = np.r_[phi_x, np.zeros(mesh.nEy)] # create vector for plotting function >>> mesh.plot_image(v, ax=ax1, v_type="Ex") >>> ax1.set_title("Variable at x-edges", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Axc, ms=1) >>> ax1.set_title("X-Edge Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ if getattr(self, "_average_edge_x_to_cell", None) is None: # The number of cell centers in each direction n = self.vnC if self.dim == 1: self._average_edge_x_to_cell = speye(n[0]) elif self.dim == 2: self._average_edge_x_to_cell = sp.kron(av(n[1]), speye(n[0])) elif self.dim == 3: self._average_edge_x_to_cell = kron3(av(n[2]), av(n[1]), speye(n[0])) return self._average_edge_x_to_cell @property def average_edge_y_to_cell(self): r"""Averaging operator from y-edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from y-edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on y-edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges_y) scipy.sparse.csr_matrix The scalar averaging operator from y-edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_y}` be a discrete scalar quantity that lives on y-edges. **average_edge_y_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{yc}}` that projects :math:`\boldsymbol{\phi_y}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{yc}} \, \boldsymbol{\phi_y} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its y-edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Ayc @ phi_y Examples -------- Here we compute the values of a scalar function on the y-edges. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h], x0="CC") Then we create a scalar variable on y-edges, >>> phi_y = np.zeros(mesh.nEy) >>> xy = mesh.edges_y >>> phi_y[(xy[:, 1] > 0)] = 25.0 >>> phi_y[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. >>> Ayc = mesh.average_edge_y_to_cell >>> phi_c = Ayc @ phi_y And plot the results, >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> v = np.r_[np.zeros(mesh.nEx), phi_y] # create vector for plotting function >>> mesh.plot_image(v, ax=ax1, v_type="Ey") >>> ax1.set_title("Variable at y-edges", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Ayc, ms=1) >>> ax1.set_title("Y-Edge Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ if self.dim < 2: return None if getattr(self, "_average_edge_y_to_cell", None) is None: # The number of cell centers in each direction n = self.vnC if self.dim == 2: self._average_edge_y_to_cell = sp.kron(speye(n[1]), av(n[0])) elif self.dim == 3: self._average_edge_y_to_cell = kron3(av(n[2]), speye(n[1]), av(n[0])) return self._average_edge_y_to_cell @property def average_edge_z_to_cell(self): r"""Averaging operator from z-edges to cell centers (scalar quantities). This property constructs a 2nd order averaging operator that maps scalar quantities from z-edges to cell centers. This averaging operator is used when a discrete scalar quantity defined on z-edges must be projected to cell centers. Once constructed, the operator is stored permanently as a property of the mesh. *See notes*. Returns ------- (n_cells, n_edges_z) scipy.sparse.csr_matrix The scalar averaging operator from z-edges to cell centers Notes ----- Let :math:`\boldsymbol{\phi_z}` be a discrete scalar quantity that lives on z-edges. **average_edge_z_to_cell** constructs a discrete linear operator :math:`\mathbf{A_{zc}}` that projects :math:`\boldsymbol{\phi_z}` to cell centers, i.e.: .. math:: \boldsymbol{\phi_c} = \mathbf{A_{zc}} \, \boldsymbol{\phi_z} where :math:`\boldsymbol{\phi_c}` approximates the value of the scalar quantity at cell centers. For each cell, we are simply averaging the values defined on its z-edges. The operation is implemented as a matrix vector product, i.e.:: phi_c = Azc @ phi_z Examples -------- Here we compute the values of a scalar function on the z-edges. We then create an averaging operator to approximate the function at cell centers. We choose to define a scalar function that is strongly discontinuous in some places to demonstrate how the averaging operator will smooth out discontinuities. We start by importing the necessary packages and defining a mesh. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = np.ones(40) >>> mesh = TensorMesh([h, h, h], x0="CCC") The we create a scalar variable on z-edges, >>> phi_z = np.zeros(mesh.nEz) >>> xyz = mesh.edges_z >>> phi_z[(xyz[:, 2] > 0)] = 25.0 >>> phi_z[(xyz[:, 2] < -10.0) & (xyz[:, 0] > -10.0) & (xyz[:, 0] < 10.0)] = 50.0 Next, we construct the averaging operator and apply it to the discrete scalar quantity to approximate the value at cell centers. We plot the original scalar and its average at cell centers for a slice at y=0. >>> Azc = mesh.average_edge_z_to_cell >>> phi_c = Azc @ phi_z Plot the results, >>> fig = plt.figure(figsize=(11, 5)) >>> ax1 = fig.add_subplot(121) >>> v = np.r_[np.zeros(mesh.nEx+mesh.nEy), phi_z] # create vector for plotting >>> mesh.plot_slice(v, ax=ax1, normal='Y', slice_loc=0, v_type="Ez") >>> ax1.set_title("Variable at z-edges", fontsize=16) >>> ax2 = fig.add_subplot(122) >>> mesh.plot_image(phi_c, ax=ax2, normal='Y', slice_loc=0, v_type="CC") >>> ax2.set_title("Averaged to cell centers", fontsize=16) >>> plt.show() Below, we show a spy plot illustrating the sparsity and mapping of the operator >>> fig = plt.figure(figsize=(9, 9)) >>> ax1 = fig.add_subplot(111) >>> ax1.spy(Azc, ms=1) >>> ax1.set_title("Z-Edge Index", fontsize=12, pad=5) >>> ax1.set_ylabel("Cell Index", fontsize=12) >>> plt.show() """ if self.dim < 3: return None if getattr(self, "_average_edge_z_to_cell", None) is None: # The number of cell centers in each direction n = self.vnC if self.dim == 3: self._average_edge_z_to_cell = kron3(speye(n[2]), av(n[1]), av(n[0])) return self._average_edge_z_to_cell @property def average_edge_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 1: return self.average_cell_to_face elif self.dim == 2: return sp.diags( [1, 1], [-self.n_faces_x, self.n_faces_y], shape=(self.n_faces, self.n_edges), ) n1, n2, n3 = self.shape_cells ex_to_fy = kron3(av(n3), speye(n2 + 1), speye(n1)) ex_to_fz = kron3(speye(n3 + 1), av(n2), speye(n1)) ey_to_fx = kron3(av(n3), speye(n2), speye(n1 + 1)) ey_to_fz = kron3(speye(n3 + 1), speye(n2), av(n1)) ez_to_fx = kron3(speye(n3), av(n2), speye(n1 + 1)) ez_to_fy = kron3(speye(n3), speye(n2 + 1), av(n1)) e_to_f = 0.5 * sp.bmat( [ [None, ey_to_fx, ez_to_fx], [ex_to_fy, None, ez_to_fy], [ex_to_fz, ey_to_fz, None], ], format="csr", ) return e_to_f @property def average_node_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_cell", None) is None: # The number of cell centers in each direction if self.dim == 1: self._average_node_to_cell = av(self.shape_cells[0]) elif self.dim == 2: self._average_node_to_cell = sp.kron( av(self.shape_cells[1]), av(self.shape_cells[0]) ).tocsr() elif self.dim == 3: self._average_node_to_cell = kron3( av(self.shape_cells[2]), av(self.shape_cells[1]), av(self.shape_cells[0]), ).tocsr() return self._average_node_to_cell @property def _average_node_to_edge_x(self): """Averaging operator on cell nodes to x-edges.""" if self.dim == 1: aveN2Ex = av(self.shape_cells[0]) elif self.dim == 2: aveN2Ex = sp.kron(speye(self.shape_nodes[1]), av(self.shape_cells[0])) elif self.dim == 3: aveN2Ex = kron3( speye(self.shape_nodes[2]), speye(self.shape_nodes[1]), av(self.shape_cells[0]), ) return aveN2Ex @property def _average_node_to_edge_y(self): """Averaging operator on cell nodes to y-edges.""" if self.dim == 1: return None elif self.dim == 2: aveN2Ey = sp.kron(av(self.shape_cells[1]), speye(self.shape_nodes[0])) elif self.dim == 3: aveN2Ey = kron3( speye(self.shape_nodes[2]), av(self.shape_cells[1]), speye(self.shape_nodes[0]), ) return aveN2Ey @property def _average_node_to_edge_z(self): """Averaging operator on cell nodes to z-edges.""" if self.dim == 1 or self.dim == 2: return None elif self.dim == 3: aveN2Ez = kron3( av(self.shape_cells[2]), speye(self.shape_nodes[1]), speye(self.shape_nodes[0]), ) return aveN2Ez @property def average_node_to_edge(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_edge", None) is None: # The number of cell centers in each direction if self.dim == 1: self._average_node_to_edge = self._average_node_to_edge_x elif self.dim == 2: self._average_node_to_edge = sp.vstack( (self._average_node_to_edge_x, self._average_node_to_edge_y), format="csr", ) elif self.dim == 3: self._average_node_to_edge = sp.vstack( ( self._average_node_to_edge_x, self._average_node_to_edge_y, self._average_node_to_edge_z, ), format="csr", ) return self._average_node_to_edge @property def _average_node_to_face_x(self): if self.dim == 1: aveN2Fx = speye(self.shape_nodes[0]) elif self.dim == 2: aveN2Fx = sp.kron(av(self.shape_cells[1]), speye(self.shape_nodes[0])) elif self.dim == 3: aveN2Fx = kron3( av(self.shape_cells[2]), av(self.shape_cells[1]), speye(self.shape_nodes[0]), ) return aveN2Fx @property def _average_node_to_face_y(self): if self.dim == 1: return None elif self.dim == 2: aveN2Fy = sp.kron(speye(self.shape_nodes[1]), av(self.shape_cells[0])) elif self.dim == 3: aveN2Fy = kron3( av(self.shape_cells[2]), speye(self.shape_nodes[1]), av(self.shape_cells[0]), ) return aveN2Fy @property def _average_node_to_face_z(self): if self.dim == 1 or self.dim == 2: return None else: aveN2Fz = kron3( speye(self.shape_nodes[2]), av(self.shape_cells[1]), av(self.shape_cells[0]), ) return aveN2Fz @property def average_node_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_face", None) is None: # The number of cell centers in each direction if self.dim == 1: self._average_node_to_face = self._average_node_to_face_x elif self.dim == 2: self._average_node_to_face = sp.vstack( (self._average_node_to_face_x, self._average_node_to_face_y), format="csr", ) elif self.dim == 3: self._average_node_to_face = sp.vstack( ( self._average_node_to_face_x, self._average_node_to_face_y, self._average_node_to_face_z, ), format="csr", ) return self._average_node_to_face @property def project_face_to_boundary_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh # Simple matrix which projects the values of the faces onto the boundary faces # Can also be used to "select" the boundary faces # Create a matrix that projects all faces onto boundary faces # The below should work for a regular structured mesh is_b = make_boundary_bool(self.shape_faces_x, bdir="x") if self.dim > 1: is_b = np.r_[is_b, make_boundary_bool(self.shape_faces_y, bdir="y")] if self.dim == 3: is_b = np.r_[is_b, make_boundary_bool(self.shape_faces_z, bdir="z")] return sp.eye(self.n_faces, format="csr")[is_b] @property def project_edge_to_boundary_edge(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh # Simple matrix which projects the values of the faces onto the boundary faces # Can also be used to "select" the boundary faces # Create a matrix that projects all edges onto boundary edges # The below should work for a regular structured mesh if self.dim == 1: return None # No edges are on the boundary in 1D is_b = np.r_[ make_boundary_bool(self.shape_edges_x, bdir="yz"), make_boundary_bool(self.shape_edges_y, bdir="xz"), ] if self.dim == 3: is_b = np.r_[is_b, make_boundary_bool(self.shape_edges_z, bdir="xy")] return sp.eye(self.n_edges, format="csr")[is_b] @property def project_node_to_boundary_node(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh # Simple matrix which projects the values of the nodes onto the boundary nodes # Can also be used to "select" the boundary nodes # Create a matrix that projects all nodes onto boundary nodes # The below should work for a regular structured mesh is_b = make_boundary_bool(self.shape_nodes) return sp.eye(self.n_nodes, format="csr")[is_b] # DEPRECATED cellGrad = deprecate_property( "cell_gradient", "cellGrad", removal_version="1.0.0", error=True ) cellGradBC = deprecate_property( "cell_gradient_BC", "cellGradBC", removal_version="1.0.0", error=True ) cellGradx = deprecate_property( "cell_gradient_x", "cellGradx", removal_version="1.0.0", error=True ) cellGrady = deprecate_property( "cell_gradient_y", "cellGrady", removal_version="1.0.0", error=True ) cellGradz = deprecate_property( "cell_gradient_z", "cellGradz", removal_version="1.0.0", error=True ) faceDivx = deprecate_property( "face_x_divergence", "faceDivx", removal_version="1.0.0", error=True ) faceDivy = deprecate_property( "face_y_divergence", "faceDivy", removal_version="1.0.0", error=True ) faceDivz = deprecate_property( "face_z_divergence", "faceDivz", removal_version="1.0.0", error=True ) _cellGradStencil = deprecate_property( "stencil_cell_gradient", "_cellGradStencil", removal_version="1.0.0", error=True, ) _cellGradxStencil = deprecate_property( "stencil_cell_gradient_x", "_cellGradxStencil", removal_version="1.0.0", error=True, ) _cellGradyStencil = deprecate_property( "stencil_cell_gradient_y", "_cellGradyStencil", removal_version="1.0.0", error=True, ) _cellGradzStencil = deprecate_property( "stencil_cell_gradient_z", "_cellGradzStencil", removal_version="1.0.0", error=True, ) setCellGradBC = deprecate_method( "set_cell_gradient_BC", "setCellGradBC", removal_version="1.0.0", error=True, ) getBCProjWF = deprecate_method( "get_BC_projections", "getBCProjWF", removal_version="1.0.0", error=True ) getBCProjWF_simple = deprecate_method( "get_BC_projections_simple", "getBCProjWF_simple", removal_version="1.0.0", error=True, ) ================================================ FILE: discretize/operators/inner_products.py ================================================ """Construct inner product operators for tensor like meshes.""" from scipy import sparse as sp from discretize.base import BaseMesh from discretize.utils import ( sub2ind, sdiag, inverse_property_tensor, TensorType, make_property_tensor, ndgrid, inverse_2x2_block_diagonal, get_subarray, inverse_3x3_block_diagonal, spzeros, sdinv, mkvc, is_scalar, ) import numpy as np class InnerProducts(BaseMesh): """Class for constructing inner product matrices. ``InnerProducts`` is a mixin class for constructing inner product matrices, their inverses and their derivatives with respect to model parameters. The ``InnerProducts`` class is inherited by all ``discretize`` mesh classes. In practice, we don't create instances of the ``InnerProducts`` class in order to construct inner product matrices, their inverses or their derivatives. These quantities are instead constructed from instances of ``discretize`` meshes using the appropriate method. """ def get_face_inner_product( # NOQA D102 self, model=None, invert_model=False, invert_matrix=False, do_fast=True, **kwargs, ): # Inherited documentation from discretize.base.BaseMesh if "invProp" in kwargs: raise TypeError( "The invProp keyword argument has been removed, please use invert_model. " "This will be removed in discretize 1.0.0", ) if "invMat" in kwargs: raise TypeError( "The invMat keyword argument has been removed, please use invert_matrix. " "This will be removed in discretize 1.0.0", ) if "doFast" in kwargs: raise TypeError( "The doFast keyword argument has been removed, please use do_fast. " "This will be removed in discretize 1.0.0", ) do_fast = kwargs["doFast"] return self._getInnerProduct( "F", model=model, invert_model=invert_model, invert_matrix=invert_matrix, do_fast=do_fast, ) def get_edge_inner_product( # NOQA D102 self, model=None, invert_model=False, invert_matrix=False, do_fast=True, **kwargs, ): # Inherited documentation from discretize.base.BaseMesh if "invProp" in kwargs: raise TypeError( "The invProp keyword argument has been removed, please use invert_model. " "This will be removed in discretize 1.0.0", ) if "invMat" in kwargs: raise TypeError( "The invMat keyword argument has been removed, please use invert_matrix. " "This will be removed in discretize 1.0.0", ) if "doFast" in kwargs: raise TypeError( "The doFast keyword argument has been removed, please use do_fast. " "This will be removed in discretize 1.0.0", ) return self._getInnerProduct( "E", model=model, invert_model=invert_model, invert_matrix=invert_matrix, do_fast=do_fast, ) def get_edge_inner_product_surface( # NOQA D102 self, model=None, invert_model=False, invert_matrix=False, **kwargs ): # Inherited documentation from discretize.base.BaseMesh if model is None: model = np.ones(self.nF) if invert_model: model = 1.0 / model if is_scalar(model): model = model * np.ones(self.nF) # COULD ADD THIS CASE IF DESIRED # elif len(model) == self.dim: # model = np.r_[ # [model[ii]*self.vnF[ii] for ii in range(0, self.dim)] # ] # Isotropic case only if model.size != self.nF: raise ValueError( "Unexpected shape of tensor: {}".format(model.shape), "Must be scalar or have length equal to total number of faces.", ) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = self.vnE if self.is_symmetric: n_elements = 1 else: n_elements = sum([1 if x != 0 else 0 for x in shape]) - 1 else: n_elements = self.dim - 1 Aprop = self.face_areas * mkvc(model) Av = self.average_edge_to_face M = n_elements * sdiag(Av.T * Aprop) if invert_matrix: return sdinv(M) else: return M def _getInnerProduct( self, projection_type, model=None, invert_model=False, invert_matrix=False, do_fast=True, **kwargs, ): """Get the inner product matrix. Parameters ---------- str : projection_type 'F' for faces 'E' for edges numpy.ndarray : model material property (tensor properties are possible) at each cell center (nC, (1, 3, or 6)) bool : invert_model inverts the material property bool : invert_matrix inverts the matrix bool : do_fast do a faster implementation if available. Returns ------- scipy.sparse.csr_matrix M, the inner product matrix (nE, nE) """ if "invProp" in kwargs: raise TypeError( "The invProp keyword argument has been removed, please use invert_model. " "This will be removed in discretize 1.0.0", ) if "invMat" in kwargs: raise TypeError( "The invMat keyword argument has been removed, please use invert_matrix. " "This will be removed in discretize 1.0.0", ) if "doFast" in kwargs: raise TypeError( "The doFast keyword argument has been removed, please use do_fast. " "This will be removed in discretize 1.0.0", ) if projection_type not in ["F", "E"]: raise TypeError("projection_type must be 'F' for faces or 'E' for edges") fast = None if hasattr(self, "_fastInnerProduct") and do_fast: fast = self._fastInnerProduct( projection_type, model=model, invert_model=invert_model, invert_matrix=invert_matrix, ) if fast is not None: return fast if invert_model: model = inverse_property_tensor(self, model) tensorType = TensorType(self, model) Mu = make_property_tensor(self, model) Ps = self._getInnerProductProjectionMatrices(projection_type, tensorType) A = np.sum([P.T * Mu * P for P in Ps]) if invert_matrix and tensorType < 3: A = sdinv(A) elif invert_matrix and tensorType == 3: raise Exception("Solver needed to invert A.") return A def _getInnerProductProjectionMatrices(self, projection_type, tensorType): """Get the inner product projection matrices. Parameters ---------- projection_type : str 'F' for faces 'E' for edges tensorType : TensorType type of the tensor: TensorType(mesh, sigma) Returns ------- scipy.sparse.csr_matrix """ if not isinstance(tensorType, TensorType): raise TypeError("tensorType must be an instance of TensorType.") if projection_type not in ["F", "E"]: raise TypeError("projection_type must be 'F' for faces or 'E' for edges") d = self.dim # We will multiply by sqrt on each side to keep symmetry V = sp.kron(sp.identity(d), sdiag(np.sqrt((2 ** (-d)) * self.cell_volumes))) nodes = ["000", "100", "010", "110", "001", "101", "011", "111"][: 2**d] if projection_type == "F": locs = { "000": [("fXm",), ("fXm", "fYm"), ("fXm", "fYm", "fZm")], "100": [("fXp",), ("fXp", "fYm"), ("fXp", "fYm", "fZm")], "010": [None, ("fXm", "fYp"), ("fXm", "fYp", "fZm")], "110": [None, ("fXp", "fYp"), ("fXp", "fYp", "fZm")], "001": [None, None, ("fXm", "fYm", "fZp")], "101": [None, None, ("fXp", "fYm", "fZp")], "011": [None, None, ("fXm", "fYp", "fZp")], "111": [None, None, ("fXp", "fYp", "fZp")], } proj = getattr(self, "_getFaceP" + ("x" * d))() elif projection_type == "E": locs = { "000": [("eX0",), ("eX0", "eY0"), ("eX0", "eY0", "eZ0")], "100": [("eX0",), ("eX0", "eY1"), ("eX0", "eY1", "eZ1")], "010": [None, ("eX1", "eY0"), ("eX1", "eY0", "eZ2")], "110": [None, ("eX1", "eY1"), ("eX1", "eY1", "eZ3")], "001": [None, None, ("eX2", "eY2", "eZ0")], "101": [None, None, ("eX2", "eY3", "eZ1")], "011": [None, None, ("eX3", "eY2", "eZ2")], "111": [None, None, ("eX3", "eY3", "eZ3")], } proj = getattr(self, "_getEdgeP" + ("x" * d))() return [V * proj(*locs[node][d - 1]) for node in nodes] def get_face_inner_product_deriv( # NOQA D102 self, model, do_fast=True, invert_model=False, invert_matrix=False, **kwargs ): # Inherited documentation from discretize.base.BaseMesh if "invProp" in kwargs: raise TypeError( "The invProp keyword argument has been removed, please use invert_model. " "This will be removed in discretize 1.0.0", ) if "invMat" in kwargs: raise TypeError( "The invMat keyword argument has been removed, please use invert_matrix. " "This will be removed in discretize 1.0.0", ) if "doFast" in kwargs: raise TypeError( "The doFast keyword argument has been removed, please use do_fast. " "This will be removed in discretize 1.0.0", ) return self._getInnerProductDeriv( model, "F", do_fast=do_fast, invert_model=invert_model, invert_matrix=invert_matrix, ) def get_edge_inner_product_deriv( # NOQA D102 self, model, do_fast=True, invert_model=False, invert_matrix=False, **kwargs ): # Inherited documentation from discretize.base.BaseMesh if "invProp" in kwargs: raise TypeError( "The invProp keyword argument has been removed, please use invert_model. " "This will be removed in discretize 1.0.0", ) if "invMat" in kwargs: raise TypeError( "The invMat keyword argument has been removed, please use invert_matrix. " "This will be removed in discretize 1.0.0", ) if "doFast" in kwargs: raise TypeError( "The doFast keyword argument has been removed, please use do_fast. " "This will be removed in discretize 1.0.0", ) return self._getInnerProductDeriv( model, "E", do_fast=do_fast, invert_model=invert_model, invert_matrix=invert_matrix, ) def get_edge_inner_product_surface_deriv( # NOQA D102 self, model, invert_model=False, invert_matrix=False, **kwargs ): # Inherited documentation from discretize.base.BaseMesh if model is None: tensorType = -1 elif is_scalar(model): tensorType = 0 elif model.size == self.nF: tensorType = 1 else: raise ValueError( "Unexpected shape of tensor: {}".format(model.shape), "Must be scalar or have length equal to total number of faces.", ) dMdprop = None if invert_matrix or invert_model: MI = self.get_edge_inner_product_surface( model, invert_model=invert_model, invert_matrix=invert_matrix, ) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = self.vnE if self.is_symmetric: n_elements = 1 else: n_elements = sum([1 if x != 0 else 0 for x in shape]) - 1 else: n_elements = self.dim - 1 A = sdiag(self.face_areas) Av = self.average_edge_to_face if tensorType == 0: # isotropic, constant ones = sp.csr_matrix( (np.ones(self.nF), (range(self.nF), np.zeros(self.nF))), shape=(self.nF, 1), ) if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * A * ones elif invert_matrix and invert_model: dMdprop = n_elements * ( sdiag(MI.diagonal() ** 2) * Av.T * A * ones * sdiag(1.0 / model**2) ) elif invert_model: dMdprop = n_elements * Av.T * A * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * A) elif tensorType == 1: # isotropic, variable in space if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * A elif invert_matrix and invert_model: dMdprop = n_elements * ( sdiag(MI.diagonal() ** 2) * Av.T * A * sdiag(1.0 / model**2) ) elif invert_model: dMdprop = n_elements * Av.T * A * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * A) if dMdprop is not None: def innerProductDeriv(v): return sdiag(v) * dMdprop return innerProductDeriv else: return None def _getInnerProductDeriv( self, model, projection_type, do_fast=True, invert_model=False, invert_matrix=False, ): """Get the inner product projection derivative function. Parameters ---------- model : numpy.ndarray material property (tensor properties are possible) at each cell center (nC, (1, 3, or 6)) projection_type : str 'F' for faces 'E' for edges do_fast : bool do a faster implementation if available. invert_model : bool inverts the material property invert_matrix : bool inverts the matrix Returns ------- callable dMdm, the derivative of the inner product matrix (nE, nC*nA) """ fast = None if hasattr(self, "_fastInnerProductDeriv") and do_fast: fast = self._fastInnerProductDeriv( projection_type, model, invert_model=invert_model, invert_matrix=invert_matrix, ) if fast is not None: return fast if invert_model or invert_matrix: raise NotImplementedError( "inverting the property or the matrix is not yet implemented for this mesh/tensorType. You should write it!" ) tensorType = TensorType(self, model) P = self._getInnerProductProjectionMatrices( projection_type, tensorType=tensorType ) def innerProductDeriv(v): return self._getInnerProductDerivFunction(tensorType, P, projection_type, v) return innerProductDeriv def _getInnerProductDerivFunction(self, tensorType, P, projection_type, v): """Get the inner product projection derivative function depending on the tensor type. Parameters ---------- model : numpy.ndarray material property (tensor properties are possible) at each cell center (nC, (1, 3, or 6)) v : numpy.ndarray vector to multiply (required in the general implementation) P : list list of projection matrices projection_type : str 'F' for faces 'E' for edges Returns ------- scipy.sparse.csr_matrix dMdm, the derivative of the inner product matrix (n, nC*nA) """ if projection_type not in ["F", "E"]: raise TypeError("projection_type must be 'F' for faces or 'E' for edges") n = getattr(self, "n" + projection_type) if tensorType == -1: return None if v is None: raise Exception("v must be supplied for this implementation.") d = self.dim Z = spzeros(self.nC, self.nC) if tensorType == 0: dMdm = spzeros(n, 1) for p in P: dMdm = dMdm + sp.csr_matrix( (p.T * (p * v), (range(n), np.zeros(n))), shape=(n, 1) ) if d == 1: if tensorType == 1: dMdm = spzeros(n, self.nC) for p in P: dMdm = dMdm + p.T * sdiag(p * v) elif d == 2: if tensorType == 1: dMdm = spzeros(n, self.nC) for p in P: Y = p * v y1 = Y[: self.nC] y2 = Y[self.nC :] dMdm = dMdm + p.T * sp.vstack((sdiag(y1), sdiag(y2))) elif tensorType == 2: dMdms = [spzeros(n, self.nC) for _ in range(2)] for p in P: Y = p * v y1 = Y[: self.nC] y2 = Y[self.nC :] dMdms[0] = dMdms[0] + p.T * sp.vstack((sdiag(y1), Z)) dMdms[1] = dMdms[1] + p.T * sp.vstack((Z, sdiag(y2))) dMdm = sp.hstack(dMdms) elif tensorType == 3: dMdms = [spzeros(n, self.nC) for _ in range(3)] for p in P: Y = p * v y1 = Y[: self.nC] y2 = Y[self.nC :] dMdms[0] = dMdms[0] + p.T * sp.vstack((sdiag(y1), Z)) dMdms[1] = dMdms[1] + p.T * sp.vstack((Z, sdiag(y2))) dMdms[2] = dMdms[2] + p.T * sp.vstack((sdiag(y2), sdiag(y1))) dMdm = sp.hstack(dMdms) elif d == 3: if tensorType == 1: dMdm = spzeros(n, self.nC) for p in P: Y = p * v y1 = Y[: self.nC] y2 = Y[self.nC : self.nC * 2] y3 = Y[self.nC * 2 :] dMdm = dMdm + p.T * sp.vstack((sdiag(y1), sdiag(y2), sdiag(y3))) elif tensorType == 2: dMdms = [spzeros(n, self.nC) for _ in range(3)] for p in P: Y = p * v y1 = Y[: self.nC] y2 = Y[self.nC : self.nC * 2] y3 = Y[self.nC * 2 :] dMdms[0] = dMdms[0] + p.T * sp.vstack((sdiag(y1), Z, Z)) dMdms[1] = dMdms[1] + p.T * sp.vstack((Z, sdiag(y2), Z)) dMdms[2] = dMdms[2] + p.T * sp.vstack((Z, Z, sdiag(y3))) dMdm = sp.hstack(dMdms) elif tensorType == 3: dMdms = [spzeros(n, self.nC) for _ in range(6)] for p in P: Y = p * v y1 = Y[: self.nC] y2 = Y[self.nC : self.nC * 2] y3 = Y[self.nC * 2 :] dMdms[0] = dMdms[0] + p.T * sp.vstack((sdiag(y1), Z, Z)) dMdms[1] = dMdms[1] + p.T * sp.vstack((Z, sdiag(y2), Z)) dMdms[2] = dMdms[2] + p.T * sp.vstack((Z, Z, sdiag(y3))) dMdms[3] = dMdms[3] + p.T * sp.vstack((sdiag(y2), sdiag(y1), Z)) dMdms[4] = dMdms[4] + p.T * sp.vstack((sdiag(y3), Z, sdiag(y1))) dMdms[5] = dMdms[5] + p.T * sp.vstack((Z, sdiag(y3), sdiag(y2))) dMdm = sp.hstack(dMdms) return dMdm # ------------------------ Geometries ------------------------------ # # # node(i,j,k+1) ------ edge2(i,j,k+1) ----- node(i,j+1,k+1) # / / # / / | # edge3(i,j,k) face1(i,j,k) edge3(i,j+1,k) # / / | # / / | # node(i,j,k) ------ edge2(i,j,k) ----- node(i,j+1,k) # | | | # | | node(i+1,j+1,k+1) # | | / # edge1(i,j,k) face3(i,j,k) edge1(i,j+1,k) # | | / # | | / # | |/ # node(i+1,j,k) ------ edge2(i+1,j,k) ----- node(i+1,j+1,k) def _getFacePx(M): """Return a function for creating face projection matrices in 1D.""" ii = np.arange(M.shape_cells[0]) def Px(xFace): # xFace is 'fXp' or 'fXm' posFx = 0 if xFace == "fXm" else 1 IND = ii + posFx PX = sp.csr_matrix((np.ones(M.nC), (range(M.nC), IND)), shape=(M.nC, M.nF)) return PX return Px def _getFacePxx(M): """Return a function for creating face projection matrices in 2D.""" # returns a function for creating projection matrices # # Mats takes you from faces a subset of all faces on only the # faces that you ask for. # # These are centered around a single nodes. # # For example, if this was your entire mesh: # # f3(Yp) # 2_______________3 # | | # | | # | | # f0(Xm) | x | f1(Xp) # | | # | | # |_______________| # 0 1 # f2(Ym) # # Pxx('fXm','fYm') = | 1, 0, 0, 0 | # | 0, 0, 1, 0 | # # Pxx('fXp','fYm') = | 0, 1, 0, 0 | # | 0, 0, 1, 0 | i, j = np.arange(M.shape_cells[0]), np.arange(M.shape_cells[1]) iijj = ndgrid(i, j) ii, jj = iijj[:, 0], iijj[:, 1] if M._meshType == "Curv": fN1 = M.reshape(M.face_normals, "F", "Fx", "M") fN2 = M.reshape(M.face_normals, "F", "Fy", "M") def Pxx(xFace, yFace): # xFace is 'fXp' or 'fXm' # yFace is 'fYp' or 'fYm' # no | node | f1 | f2 # 00 | i ,j | i , j | i, j # 10 | i+1,j | i+1, j | i, j # 01 | i ,j+1 | i , j | i, j+1 # 11 | i+1,j+1 | i+1, j | i, j+1 posFx = 0 if xFace == "fXm" else 1 posFy = 0 if yFace == "fYm" else 1 ind1 = sub2ind(M.vnFx, np.c_[ii + posFx, jj]) ind2 = sub2ind(M.vnFy, np.c_[ii, jj + posFy]) + M.nFx IND = np.r_[ind1, ind2].flatten() PXX = sp.csr_matrix( (np.ones(2 * M.nC), (range(2 * M.nC), IND)), shape=(2 * M.nC, M.nF) ) if M._meshType == "Curv": I2x2 = inverse_2x2_block_diagonal( get_subarray(fN1[0], [i + posFx, j]), get_subarray(fN1[1], [i + posFx, j]), get_subarray(fN2[0], [i, j + posFy]), get_subarray(fN2[1], [i, j + posFy]), ) PXX = I2x2 * PXX return PXX return Pxx def _getFacePxxx(M): """Return a function for creating face projection matrices in 3D. Mats takes you from faces a subset of all faces on only the faces that you ask for. These are centered around a single nodes. """ i, j, k = ( np.arange(M.shape_cells[0]), np.arange(M.shape_cells[1]), np.arange(M.shape_cells[2]), ) iijjkk = ndgrid(i, j, k) ii, jj, kk = iijjkk[:, 0], iijjkk[:, 1], iijjkk[:, 2] if M._meshType == "Curv": fN1 = M.reshape(M.face_normals, "F", "Fx", "M") fN2 = M.reshape(M.face_normals, "F", "Fy", "M") fN3 = M.reshape(M.face_normals, "F", "Fz", "M") def Pxxx(xFace, yFace, zFace): # xFace is 'fXp' or 'fXm' # yFace is 'fYp' or 'fYm' # zFace is 'fZp' or 'fZm' # no | node | f1 | f2 | f3 # 000 | i ,j ,k | i , j, k | i, j , k | i, j, k # 100 | i+1,j ,k | i+1, j, k | i, j , k | i, j, k # 010 | i ,j+1,k | i , j, k | i, j+1, k | i, j, k # 110 | i+1,j+1,k | i+1, j, k | i, j+1, k | i, j, k # 001 | i ,j ,k+1 | i , j, k | i, j , k | i, j, k+1 # 101 | i+1,j ,k+1 | i+1, j, k | i, j , k | i, j, k+1 # 011 | i ,j+1,k+1 | i , j, k | i, j+1, k | i, j, k+1 # 111 | i+1,j+1,k+1 | i+1, j, k | i, j+1, k | i, j, k+1 posX = 0 if xFace == "fXm" else 1 posY = 0 if yFace == "fYm" else 1 posZ = 0 if zFace == "fZm" else 1 ind1 = sub2ind(M.vnFx, np.c_[ii + posX, jj, kk]) ind2 = sub2ind(M.vnFy, np.c_[ii, jj + posY, kk]) + M.nFx ind3 = sub2ind(M.vnFz, np.c_[ii, jj, kk + posZ]) + M.nFx + M.nFy IND = np.r_[ind1, ind2, ind3].flatten() PXXX = sp.coo_matrix( (np.ones(3 * M.nC), (range(3 * M.nC), IND)), shape=(3 * M.nC, M.nF) ).tocsr() if M._meshType == "Curv": I3x3 = inverse_3x3_block_diagonal( get_subarray(fN1[0], [i + posX, j, k]), get_subarray(fN1[1], [i + posX, j, k]), get_subarray(fN1[2], [i + posX, j, k]), get_subarray(fN2[0], [i, j + posY, k]), get_subarray(fN2[1], [i, j + posY, k]), get_subarray(fN2[2], [i, j + posY, k]), get_subarray(fN3[0], [i, j, k + posZ]), get_subarray(fN3[1], [i, j, k + posZ]), get_subarray(fN3[2], [i, j, k + posZ]), ) PXXX = I3x3 * PXXX return PXXX return Pxxx def _getEdgePx(M): """Return a function for creating edge projection matrices in 1D.""" def Px(xEdge): if xEdge != "eX0": raise TypeError("xEdge = {0!s}, not eX0".format(xEdge)) return sp.identity(M.nC) return Px def _getEdgePxx(M): """Return a function for creating edge projection matrices in 2D.""" i, j = np.arange(M.shape_cells[0]), np.arange(M.shape_cells[1]) iijj = ndgrid(i, j) ii, jj = iijj[:, 0], iijj[:, 1] if M._meshType == "Curv": eT1 = M.reshape(M.edge_tangents, "E", "Ex", "M") eT2 = M.reshape(M.edge_tangents, "E", "Ey", "M") def Pxx(xEdge, yEdge): # no | node | e1 | e2 # 00 | i ,j | i ,j | i ,j # 10 | i+1,j | i ,j | i+1,j # 01 | i ,j+1 | i ,j+1 | i ,j # 11 | i+1,j+1 | i ,j+1 | i+1,j posX = 0 if xEdge == "eX0" else 1 posY = 0 if yEdge == "eY0" else 1 ind1 = sub2ind(M.vnEx, np.c_[ii, jj + posX]) ind2 = sub2ind(M.vnEy, np.c_[ii + posY, jj]) + M.nEx IND = np.r_[ind1, ind2].flatten() PXX = sp.coo_matrix( (np.ones(2 * M.nC), (range(2 * M.nC), IND)), shape=(2 * M.nC, M.nE) ).tocsr() if M._meshType == "Curv": I2x2 = inverse_2x2_block_diagonal( get_subarray(eT1[0], [i, j + posX]), get_subarray(eT1[1], [i, j + posX]), get_subarray(eT2[0], [i + posY, j]), get_subarray(eT2[1], [i + posY, j]), ) PXX = I2x2 * PXX return PXX return Pxx def _getEdgePxxx(M): """Return a function for creating edge projection matrices in 3D.""" i, j, k = ( np.arange(M.shape_cells[0]), np.arange(M.shape_cells[1]), np.arange(M.shape_cells[2]), ) iijjkk = ndgrid(i, j, k) ii, jj, kk = iijjkk[:, 0], iijjkk[:, 1], iijjkk[:, 2] if M._meshType == "Curv": eT1 = M.reshape(M.edge_tangents, "E", "Ex", "M") eT2 = M.reshape(M.edge_tangents, "E", "Ey", "M") eT3 = M.reshape(M.edge_tangents, "E", "Ez", "M") def Pxxx(xEdge, yEdge, zEdge): # no | node | e1 | e2 | e3 # 000 | i ,j ,k | i ,j ,k | i ,j ,k | i ,j ,k # 100 | i+1,j ,k | i ,j ,k | i+1,j ,k | i+1,j ,k # 010 | i ,j+1,k | i ,j+1,k | i ,j ,k | i ,j+1,k # 110 | i+1,j+1,k | i ,j+1,k | i+1,j ,k | i+1,j+1,k # 001 | i ,j ,k+1 | i ,j ,k+1 | i ,j ,k+1 | i ,j ,k # 101 | i+1,j ,k+1 | i ,j ,k+1 | i+1,j ,k+1 | i+1,j ,k # 011 | i ,j+1,k+1 | i ,j+1,k+1 | i ,j ,k+1 | i ,j+1,k # 111 | i+1,j+1,k+1 | i ,j+1,k+1 | i+1,j ,k+1 | i+1,j+1,k posX = ( [0, 0] if xEdge == "eX0" else [1, 0] if xEdge == "eX1" else [0, 1] if xEdge == "eX2" else [1, 1] ) posY = ( [0, 0] if yEdge == "eY0" else [1, 0] if yEdge == "eY1" else [0, 1] if yEdge == "eY2" else [1, 1] ) posZ = ( [0, 0] if zEdge == "eZ0" else [1, 0] if zEdge == "eZ1" else [0, 1] if zEdge == "eZ2" else [1, 1] ) ind1 = sub2ind(M.vnEx, np.c_[ii, jj + posX[0], kk + posX[1]]) ind2 = sub2ind(M.vnEy, np.c_[ii + posY[0], jj, kk + posY[1]]) + M.nEx ind3 = ( sub2ind(M.vnEz, np.c_[ii + posZ[0], jj + posZ[1], kk]) + M.nEx + M.nEy ) IND = np.r_[ind1, ind2, ind3].flatten() PXXX = sp.coo_matrix( (np.ones(3 * M.nC), (range(3 * M.nC), IND)), shape=(3 * M.nC, M.nE) ).tocsr() if M._meshType == "Curv": I3x3 = inverse_3x3_block_diagonal( get_subarray(eT1[0], [i, j + posX[0], k + posX[1]]), get_subarray(eT1[1], [i, j + posX[0], k + posX[1]]), get_subarray(eT1[2], [i, j + posX[0], k + posX[1]]), get_subarray(eT2[0], [i + posY[0], j, k + posY[1]]), get_subarray(eT2[1], [i + posY[0], j, k + posY[1]]), get_subarray(eT2[2], [i + posY[0], j, k + posY[1]]), get_subarray(eT3[0], [i + posZ[0], j + posZ[1], k]), get_subarray(eT3[1], [i + posZ[0], j + posZ[1], k]), get_subarray(eT3[2], [i + posZ[0], j + posZ[1], k]), ) PXXX = I3x3 * PXXX return PXXX return Pxxx ================================================ FILE: discretize/operators/meson.build ================================================ python_sources = [ '__init__.py', 'differential_operators.py', 'inner_products.py', ] py.install_sources( python_sources, subdir: 'discretize/operators' ) ================================================ FILE: discretize/tensor_cell.py ================================================ """Cell class for TensorMesh.""" import itertools import numpy as np class TensorCell: """ Representation of a cell in a TensorMesh. Parameters ---------- h : (dim) numpy.ndarray Array with the cell widths along each direction. For a 2D mesh, it must have two elements (``hx``, ``hy``). For a 3D mesh it must have three elements (``hx``, ``hy``, ``hz``). origin : (dim) numpy.ndarray Array with the coordinates of the origin of the cell, i.e. the bottom-left-frontmost corner. index_unraveled : (dim) tuple Array with the unraveled indices of the cell in its parent mesh. mesh_shape : (dim) tuple Shape of the parent mesh. Examples -------- Define a simple :class:`discretize.TensorMesh`. >>> from discretize import TensorMesh >>> mesh = TensorMesh([5, 8, 10]) We can obtain a particular cell in the mesh by its index: >>> cell = mesh[3] >>> cell TensorCell(h=[0.2 0.125 0.1 ], origin=[0.6 0. 0. ], index=3, mesh_shape=(5, 8, 10)) And then obtain information about it, like its :attr:`discretize.tensor_cell.TensorCell.origin`: >>> cell.origin array([0.6, 0. , 0. ]) Or its :attr:`discretize.tensor_cell.TensorCell.bounds`: >>> cell.bounds array([0.6 , 0.8 , 0. , 0.125, 0. , 0.1 ]) We can also get its neighboring cells: >>> neighbours = cell.get_neighbors(mesh) >>> for neighbor in neighbours: ... print(neighbor.center) [0.5 0.0625 0.05 ] [0.9 0.0625 0.05 ] [0.7 0.1875 0.05 ] [0.7 0.0625 0.15 ] Alternatively, we can iterate over all cells in the mesh with a simple *for loop* or list comprehension: >>> cells = [cell for cell in mesh] >>> len(cells) 400 """ def __init__(self, h, origin, index_unraveled, mesh_shape): self._h = h self._origin = origin self._index_unraveled = index_unraveled self._mesh_shape = mesh_shape def __repr__(self): """Represent a TensorCell.""" attributes = ", ".join( [ f"{attr}={getattr(self, attr)}" for attr in ("h", "origin", "index", "mesh_shape") ] ) return f"TensorCell({attributes})" def __eq__(self, other): """Check if this cell is the same as other one.""" if not isinstance(other, TensorCell): raise TypeError( f"Cannot compare an object of type '{other.__class__.__name__}' " "with a TensorCell" ) are_equal = ( np.all(self.h == other.h) and np.all(self.origin == other.origin) and self.index == other.index and self.mesh_shape == other.mesh_shape ) return are_equal @property def h(self): """Cell widths.""" return self._h @property def origin(self): """Coordinates of the origin of the cell.""" return self._origin @property def index(self): """Index of the cell in a TensorMesh.""" return np.ravel_multi_index( self.index_unraveled, dims=self.mesh_shape, order="F" ) @property def index_unraveled(self): """Unraveled index of the cell in a TensorMesh.""" return self._index_unraveled @property def mesh_shape(self): """Shape of the parent mesh.""" return self._mesh_shape @property def dim(self): """Dimensions of the cell (1, 2 or 3).""" return len(self.h) @property def center(self): """ Coordinates of the cell center. Returns ------- center : (dim) array Array with the coordinates of the cell center. """ center = np.array(self.origin) + np.array(self.h) / 2 return center @property def bounds(self): """ Bounds of the cell. Coordinates that define the bounds of the cell. Bounds are returned in the following order: ``x1``, ``x2``, ``y1``, ``y2``, ``z1``, ``z2``. Returns ------- bounds : (2 * dim) array Array with the cell bounds. """ bounds = np.array( [ origin_i + factor * h_i for origin_i, h_i in zip(self.origin, self.h) for factor in (0, 1) ] ) return bounds @property def neighbors(self): """ Indices for this cell's neighbors within its parent mesh. Returns ------- list of list of int """ neighbor_indices = [] for dim in range(self.dim): for delta in (-1, 1): index = list(self.index_unraveled) index[dim] += delta if 0 <= index[dim] < self._mesh_shape[dim]: neighbor_indices.append( np.ravel_multi_index(index, dims=self.mesh_shape, order="F") ) return neighbor_indices @property def nodes(self): """ Indices for this cell's nodes within its parent mesh. Returns ------- list of int """ # Define shape of nodes in parent mesh nodes_shape = [s + 1 for s in self.mesh_shape] # Get indices of nodes per dimension nodes_index_per_dim = [[index, index + 1] for index in self.index_unraveled] # Combine the nodes_index_per_dim using itertools.product. # Because we want to follow a FORTRAN order, we need to reverse the # order of the nodes_index_per_dim and the indices. nodes_indices = [i[::-1] for i in itertools.product(*nodes_index_per_dim[::-1])] # Ravel indices nodes_indices = [ np.ravel_multi_index(index, dims=nodes_shape, order="F") for index in nodes_indices ] return nodes_indices @property def edges(self): """ Indices for this cell's edges within its parent mesh. Returns ------- list of int """ if self.dim == 1: edges_indices = [self.index] elif self.dim == 2: # Get shape of edges grids (for edges_x and edges_y) edges_x_shape = [self.mesh_shape[0], self.mesh_shape[1] + 1] edges_y_shape = [self.mesh_shape[0] + 1, self.mesh_shape[1]] # Calculate total amount of edges_x n_edges_x = edges_x_shape[0] * edges_x_shape[1] # Get indices of edges_x edges_x_indices = [ [self.index_unraveled[0], self.index_unraveled[1] + delta] for delta in (0, 1) ] edges_x_indices = [ np.ravel_multi_index(index, dims=edges_x_shape, order="F") for index in edges_x_indices ] # Get indices of edges_y edges_y_indices = [ [self.index_unraveled[0] + delta, self.index_unraveled[1]] for delta in (0, 1) ] edges_y_indices = [ n_edges_x + np.ravel_multi_index(index, dims=edges_y_shape, order="F") for index in edges_y_indices ] edges_indices = edges_x_indices + edges_y_indices elif self.dim == 3: edges_x_shape = [ n if i == 0 else n + 1 for i, n in enumerate(self.mesh_shape) ] edges_y_shape = [ n if i == 1 else n + 1 for i, n in enumerate(self.mesh_shape) ] edges_z_shape = [ n if i == 2 else n + 1 for i, n in enumerate(self.mesh_shape) ] # Calculate total amount of edges_x and edges_y n_edges_x = edges_x_shape[0] * edges_x_shape[1] * edges_x_shape[2] n_edges_y = edges_y_shape[0] * edges_y_shape[1] * edges_y_shape[2] # Get indices of edges_x edges_x_indices = [ [ self.index_unraveled[0], self.index_unraveled[1] + delta_y, self.index_unraveled[2] + delta_z, ] for delta_z in (0, 1) for delta_y in (0, 1) ] edges_x_indices = [ np.ravel_multi_index(index, dims=edges_x_shape, order="F") for index in edges_x_indices ] # Get indices of edges_y edges_y_indices = [ [ self.index_unraveled[0] + delta_x, self.index_unraveled[1], self.index_unraveled[2] + delta_z, ] for delta_z in (0, 1) for delta_x in (0, 1) ] edges_y_indices = [ n_edges_x + np.ravel_multi_index(index, dims=edges_y_shape, order="F") for index in edges_y_indices ] # Get indices of edges_z edges_z_indices = [ [ self.index_unraveled[0] + delta_x, self.index_unraveled[1] + delta_y, self.index_unraveled[2], ] for delta_y in (0, 1) for delta_x in (0, 1) ] edges_z_indices = [ n_edges_x + n_edges_y + np.ravel_multi_index(index, dims=edges_z_shape, order="F") for index in edges_z_indices ] edges_indices = edges_x_indices + edges_y_indices + edges_z_indices return edges_indices @property def faces(self): """ Indices for cell's faces within its parent mesh. Returns ------- list of int """ if self.dim == 1: faces_indices = [self.index, self.index + 1] elif self.dim == 2: # Get shape of faces grids # (faces_x are normal to x and faces_y are normal to y) faces_x_shape = [self.mesh_shape[0] + 1, self.mesh_shape[1]] faces_y_shape = [self.mesh_shape[0], self.mesh_shape[1] + 1] # Calculate total amount of faces_x n_faces_x = faces_x_shape[0] * faces_x_shape[1] # Get indices of faces_x faces_x_indices = [ [self.index_unraveled[0] + delta, self.index_unraveled[1]] for delta in (0, 1) ] faces_x_indices = [ np.ravel_multi_index(index, dims=faces_x_shape, order="F") for index in faces_x_indices ] # Get indices of faces_y faces_y_indices = [ [self.index_unraveled[0], self.index_unraveled[1] + delta] for delta in (0, 1) ] faces_y_indices = [ n_faces_x + np.ravel_multi_index(index, dims=faces_y_shape, order="F") for index in faces_y_indices ] faces_indices = faces_x_indices + faces_y_indices elif self.dim == 3: # Get shape of faces grids faces_x_shape = [ n + 1 if i == 0 else n for i, n in enumerate(self.mesh_shape) ] faces_y_shape = [ n + 1 if i == 1 else n for i, n in enumerate(self.mesh_shape) ] faces_z_shape = [ n + 1 if i == 2 else n for i, n in enumerate(self.mesh_shape) ] # Calculate total amount of faces_x and faces_y n_faces_x = faces_x_shape[0] * faces_x_shape[1] * faces_x_shape[2] n_faces_y = faces_y_shape[0] * faces_y_shape[1] * faces_y_shape[2] # Get indices of faces_x faces_x_indices = [ [ self.index_unraveled[0] + delta, self.index_unraveled[1], self.index_unraveled[2], ] for delta in (0, 1) ] faces_x_indices = [ np.ravel_multi_index(index, dims=faces_x_shape, order="F") for index in faces_x_indices ] # Get indices of faces_y faces_y_indices = [ [ self.index_unraveled[0], self.index_unraveled[1] + delta, self.index_unraveled[2], ] for delta in (0, 1) ] faces_y_indices = [ n_faces_x + np.ravel_multi_index(index, dims=faces_y_shape, order="F") for index in faces_y_indices ] # Get indices of faces_z faces_z_indices = [ [ self.index_unraveled[0], self.index_unraveled[1], self.index_unraveled[2] + delta, ] for delta in (0, 1) ] faces_z_indices = [ n_faces_x + n_faces_y + np.ravel_multi_index(index, dims=faces_z_shape, order="F") for index in faces_z_indices ] faces_indices = faces_x_indices + faces_y_indices + faces_z_indices return faces_indices def get_neighbors(self, mesh): """ Return the neighboring cells in the mesh. Parameters ---------- mesh : TensorMesh TensorMesh where the current cell lives. Returns ------- list of TensorCell """ return [mesh[index] for index in self.neighbors] ================================================ FILE: discretize/tensor_mesh.py ================================================ """Module housing the TensorMesh implementation.""" import itertools import numpy as np from discretize.base import BaseRectangularMesh, BaseTensorMesh from discretize.operators import DiffOperators, InnerProducts from discretize.mixins import InterfaceMixins, TensorMeshIO from discretize.utils import mkvc, as_array_n_by_dim from discretize.utils.code_utils import deprecate_property from .tensor_cell import TensorCell class TensorMesh( DiffOperators, InnerProducts, BaseTensorMesh, BaseRectangularMesh, TensorMeshIO, InterfaceMixins, ): """ Tensor mesh class. Tensor meshes are numerical grids whose cell centers, nodes, faces, edges, widths, volumes, etc... can be directly expressed as tensor products. The axes defining coordinates of the mesh are orthogonal. And cell properties along one axis do not vary with respect to the position along any other axis. Parameters ---------- h : (dim) iterable of int, numpy.ndarray, or tuple Defines the cell widths along each axis. The length of the iterable object is equal to the dimension of the mesh (1, 2 or 3). For a 3D mesh, the list would have the form *[hx, hy, hz]* . Along each axis, the user has 3 choices for defining the cells widths: - :class:`int` -> A unit interval is equally discretized into `N` cells. - :class:`numpy.ndarray` -> The widths are explicity given for each cell - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])* where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional) is a padding factor denoting exponential increase/decrease in the cell width for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]* origin : (dim) iterable, default: 0 Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost corner. By default, the mesh is anchored such that its origin is at *[0, 0, 0]* . For each dimension (x, y or z), The user may set the origin 2 ways: - a ``scalar`` which explicitly defines origin along that dimension. - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along each axis is the first node location ('0'), in the center ('C') or the last node location ('N') (see Examples). See Also -------- utils.unpack_widths : The function used to expand a tuple to generate widths. Examples -------- An example of a 2D tensor mesh is shown below. Here we use a list of tuple to define the discretization along the x-axis and a numpy array to define the discretization along the y-axis. We also use a string argument to center the x-axis about x = 0 and set the top of the mesh to y = 0. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> ncx = 10 # number of core mesh cells in x >>> dx = 5 # base cell width x >>> npad_x = 3 # number of padding cells in x >>> exp_x = 1.25 # expansion rate of padding cells in x >>> ncy = 24 # total number of mesh cells in y >>> dy = 5 # base cell width y >>> hx = [(dx, npad_x, -exp_x), (dx, ncx), (dx, npad_x, exp_x)] >>> hy = dy * np.ones(ncy) >>> mesh = TensorMesh([hx, hy], origin='CN') >>> fig = plt.figure(figsize=(5,5)) >>> ax = fig.add_subplot(111) >>> mesh.plot_grid(ax=ax) >>> plt.show() """ _meshType = "TENSOR" _aliases = { **DiffOperators._aliases, **BaseRectangularMesh._aliases, **BaseTensorMesh._aliases, } def __repr__(self): """Plain text representation.""" fmt = "\n {}: {:,} cells\n\n".format(type(self).__name__, self.nC) fmt += 22 * " " + "MESH EXTENT" + 13 * " " + "CELL WIDTH FACTOR\n" fmt += " dir nC min max min max " fmt += " max\n --- --- " + 27 * "-" + " " + 18 * "-" + " ------\n" # Get attributes and put into table. attrs = self._repr_attributes() for i in range(self.dim): name = attrs["names"][i] iattr = attrs[name] fmt += " {}".format(name) fmt += " {:6}".format(iattr["nC"]) for p in ["min", "max"]: fmt += " {:13,.2f}".format(iattr[p]) for p in ["h_min", "h_max"]: fmt += " {:9,.2f}".format(iattr[p]) fmt += "{:8,.2f}".format(iattr["max_fact"]) fmt += "\n" # End row fmt += "\n" return fmt def _repr_html_(self): """HTML representation.""" style = " style='padding: 5px 20px 5px 20px;'" fmt = "\n" fmt += " \n" fmt += " {}\n".format(type(self).__name__) fmt += " {:,} cells\n".format(self.nC) fmt += " \n" fmt += " \n" fmt += " \n" fmt += " \n" fmt += " MESH EXTENT\n" fmt += " CELL WIDTH\n" fmt += " FACTOR\n" fmt += " \n" fmt += " \n" fmt += " dir\n" fmt += " nC\n" fmt += " min\n" fmt += " max\n" fmt += " min\n" fmt += " max\n" fmt += " max\n" fmt += " \n" # Get attributes and put into table. attrs = self._repr_attributes() for i in range(self.dim): name = attrs["names"][i] iattr = attrs[name] fmt += " \n" # Start row fmt += " {}\n".format(name) fmt += " {}\n".format(iattr["nC"]) for p in ["min", "max", "h_min", "h_max", "max_fact"]: fmt += " {:,.2f}\n".format(iattr[p]) fmt += " \n" # End row fmt += "\n" return fmt def __iter__(self): """Iterate over the cells.""" iterator = (self[i] for i in range(len(self))) return iterator def __getitem__(self, indices): """ Return the boundaries of a single cell of the mesh. Parameters ---------- indices : int, slice, or tuple of int and slices Indices of a cell in the mesh. It can be a single integer or a single slice (for ravelled indices), or a tuple combining integers and slices for each direction. Returns ------- TensorCell or list of TensorCell """ # Handle non tuple indices if isinstance(indices, slice): cells = [self[i] for i in _slice_to_index(indices, len(self))] return cells if np.issubdtype(type(indices), np.integer): indices = self._sanitize_indices(indices) indices = np.unravel_index(indices, self.shape_cells, order="F") # Handle tuple indices if not isinstance(indices, tuple): raise ValueError( f"Invalid indices '{indices}'. " "It should be an int, a slice or a tuple of int and slices." ) if len(indices) != self.dim: raise ValueError( f"Invalid number of indices '{len(indices)}'. " f"It should match the number of dimensions of the mesh ({self.dim})." ) # Int indices only all_indices_are_ints = all(np.issubdtype(type(i), np.integer) for i in indices) if all_indices_are_ints: indices = self._sanitize_indices(indices) return self._get_cell(indices) # Slice and int indices indices_per_dim = [ ( _slice_to_index(index, self.shape_cells[dim]) if isinstance(index, slice) else [self._sanitize_indices(index, dim=dim)] ) for dim, index in enumerate(indices) ] # Combine the indices_per_dim using itertools.product. # Because we want to follow a FORTRAN order, we need to reverse the # order of the indices_per_dim and the indices. indices = (i[::-1] for i in itertools.product(*indices_per_dim[::-1])) cells = [self._get_cell(i) for i in indices] if not cells: return None return cells def _sanitize_indices(self, indices, dim=None): """ Sanitize integer indices for cell in the mesh. Convert negative indices into their corresponding positive values within the mesh. It works with a tuple of indices or with single int (ravelled indices). Parameters ---------- indices : int or tuple of int Indices of a single mesh cell. It can contain negative indices. dim : int or None Corresponding dimension of ``indices``, if it's a single int. If None and ``indices`` is an int, then ``indices`` will be assumed to be a ravelled index. If ``indices`` is a tuple, ``dim`` is ignored. Returns ------- int or tuple of int """ if isinstance(indices, tuple): indices = tuple( index if index >= 0 else index + self.shape_cells[i] for i, index in enumerate(indices) ) elif indices < 0: if dim is None: indices = indices + self.n_cells else: indices = indices + self.shape_cells[dim] return indices def _get_cell(self, indices): """Return a single cell in the mesh. Parameters ---------- indices : tuple of int Tuple containing the indices of the cell. Must have the same number of elements as the mesh dimensions. Returns ------- TensorCell """ assert all(index >= 0 for index in indices) if self.dim == 1: (i,) = indices x1, x2 = self.nodes_x[i], self.nodes_x[i + 1] origin = np.array([x1]) h = np.array([x2 - x1]) if self.dim == 2: i, j = indices x1, x2 = self.nodes_x[i], self.nodes_x[i + 1] y1, y2 = self.nodes_y[j], self.nodes_y[j + 1] origin = np.array([x1, y1]) h = np.array([x2 - x1, y2 - y1]) if self.dim == 3: i, j, k = indices x1, x2 = self.nodes_x[i], self.nodes_x[i + 1] y1, y2 = self.nodes_y[j], self.nodes_y[j + 1] z1, z2 = self.nodes_z[k], self.nodes_z[k + 1] origin = np.array([x1, y1, z1]) h = np.array([x2 - x1, y2 - y1, z2 - z1]) return TensorCell(h, origin, indices, self.shape_cells) # --------------- Geometries --------------------- @property def cell_volumes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_cell_volumes", None) is None: vh = self.h # Compute cell volumes if self.dim == 1: self._cell_volumes = mkvc(vh[0]) elif self.dim == 2: # Cell sizes in each direction self._cell_volumes = mkvc(np.outer(vh[0], vh[1])) elif self.dim == 3: # Cell sizes in each direction self._cell_volumes = mkvc(np.outer(mkvc(np.outer(vh[0], vh[1])), vh[2])) return self._cell_volumes @property def face_x_areas(self): """Return the areas of the x-faces. Calling this property will compute and return the areas of faces whose normal vector is along the x-axis. Returns ------- (n_faces_x) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* Numpy array of ones whose length is equal to the number of nodes - *2D:* Areas of x-faces (equivalent to the lengths of y-edges) - *3D:* Areas of x-faces """ if getattr(self, "_face_x_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1: areaFx = np.ones(n[0] + 1) elif self.dim == 2: areaFx = np.outer(np.ones(n[0] + 1), vh[1]) elif self.dim == 3: areaFx = np.outer(np.ones(n[0] + 1), mkvc(np.outer(vh[1], vh[2]))) self._face_x_areas = mkvc(areaFx) return self._face_x_areas @property def face_y_areas(self): """Return the areas of the y-faces. Calling this property will compute and return the areas of faces whose normal vector is along the y-axis. Note that only 2D and 3D tensor meshes have z-faces. Returns ------- (n_faces_y) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have y-faces - *2D:* Areas of y-faces (equivalent to the lengths of x-edges) - *3D:* Areas of y-faces """ if getattr(self, "_face_y_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1: raise Exception("1D meshes do not have y-Faces") elif self.dim == 2: areaFy = np.outer(vh[0], np.ones(n[1] + 1)) elif self.dim == 3: areaFy = np.outer(vh[0], mkvc(np.outer(np.ones(n[1] + 1), vh[2]))) self._face_y_areas = mkvc(areaFy) return self._face_y_areas @property def face_z_areas(self): """Return the areas of the z-faces. Calling this property will compute and return the areas of faces whose normal vector is along the z-axis. Note that only 3D tensor meshes will have z-faces. Returns ------- (n_faces_z) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have z-faces - *2D:* N/A since 2D meshes do not have z-faces - *3D:* Areas of z-faces """ if getattr(self, "_face_z_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1 or self.dim == 2: raise Exception("{}D meshes do not have z-Faces".format(self.dim)) elif self.dim == 3: areaFz = np.outer(vh[0], mkvc(np.outer(vh[1], np.ones(n[2] + 1)))) self._face_z_areas = mkvc(areaFz) return self._face_z_areas @property def face_areas(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 1: return self.face_x_areas elif self.dim == 2: return np.r_[self.face_x_areas, self.face_y_areas] elif self.dim == 3: return np.r_[self.face_x_areas, self.face_y_areas, self.face_z_areas] @property def edge_x_lengths(self): """Return the x-edge lengths. Calling this property will compute and return the lengths of edges parallel to the x-axis. Returns ------- (n_edges_x) numpy.ndarray X-edge lengths """ if getattr(self, "_edge_x_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1: edgeEx = vh[0] elif self.dim == 2: edgeEx = np.outer(vh[0], np.ones(n[1] + 1)) elif self.dim == 3: edgeEx = np.outer( vh[0], mkvc(np.outer(np.ones(n[1] + 1), np.ones(n[2] + 1))) ) self._edge_x_lengths = mkvc(edgeEx) return self._edge_x_lengths @property def edge_y_lengths(self): """Return the y-edge lengths. Calling this property will compute and return the lengths of edges parallel to the y-axis. Returns ------- (n_edges_y) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have y-edges - *2D:* Returns y-edge lengths - *3D:* Returns y-edge lengths """ if getattr(self, "_edge_y_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1: raise Exception("1D meshes do not have y-edges") elif self.dim == 2: edgeEy = np.outer(np.ones(n[0] + 1), vh[1]) elif self.dim == 3: edgeEy = np.outer( np.ones(n[0] + 1), mkvc(np.outer(vh[1], np.ones(n[2] + 1))) ) self._edge_y_lengths = mkvc(edgeEy) return self._edge_y_lengths @property def edge_z_lengths(self): """Return the z-edge lengths. Calling this property will compute and return the lengths of edges parallel to the z-axis. Returns ------- (n_edges_z) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have z-edges - *2D:* N/A since 2D meshes do not have z-edges - *3D:* Returns z-edge lengths """ if getattr(self, "_edge_z_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1 or self.dim == 2: raise Exception("{}D meshes do not have y-edges".format(self.dim)) elif self.dim == 3: edgeEz = np.outer( np.ones(n[0] + 1), mkvc(np.outer(np.ones(n[1] + 1), vh[2])) ) self._edge_z_lengths = mkvc(edgeEz) return self._edge_z_lengths @property def edge_lengths(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 1: return self.edge_x_lengths elif self.dim == 2: return np.r_[self.edge_x_lengths, self.edge_y_lengths] elif self.dim == 3: return np.r_[self.edge_x_lengths, self.edge_y_lengths, self.edge_z_lengths] return self._edge @property def face_boundary_indices(self): """Return the indices of the x, (y and z) boundary faces. For x, (y and z) faces, this property returns the indices of the faces on the boundaries. That is, the property returns the indices of the x-faces that lie on the x-boundary; likewise for y and z. Note that each Cartesian direction will have both a lower and upper boundary, and the property will return the indices corresponding to the lower and upper boundaries separately. E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total). In this case, the return is a list of length 4 organized [ind_Bx1, ind_Bx2, ind_By1, ind_By2]:: By2 + ------------- + | | | | Bx1 | | Bx2 | | | | + ------------- + By1 Returns ------- (dim * 2) list of numpy.ndarray of bool The length of list returned depends on the dimension of the mesh. And the length of each array containing the indices depends on the number of faces in each direction. For 1D, 2D and 3D tensor meshes, the returns take the following form: - *1D:* returns [ind_Bx1, ind_Bx2] - *2D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2] - *3D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2, ind_Bz1, ind_Bz2] Examples -------- Here, we construct a 4 by 3 cell 2D tensor mesh and return the indices of the x and y-boundary faces. In this case there are 3 x-faces on each x-boundary, and there are 4 y-faces on each y-boundary. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> hx = [1, 1, 1, 1] >>> hy = [2, 2, 2] >>> mesh = TensorMesh([hx, hy]) >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.face_boundary_indices >>> ax = plt.subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(*mesh.faces_x[ind_Bx1].T) >>> plt.show() """ if self.dim == 1: indxd = self.gridFx == min(self.gridFx) indxu = self.gridFx == max(self.gridFx) return indxd, indxu elif self.dim == 2: indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0]) indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0]) indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1]) indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1]) return indxd, indxu, indyd, indyu elif self.dim == 3: indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0]) indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0]) indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1]) indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1]) indzd = self.gridFz[:, 2] == min(self.gridFz[:, 2]) indzu = self.gridFz[:, 2] == max(self.gridFz[:, 2]) return indxd, indxu, indyd, indyu, indzd, indzu @property def cell_bounds(self): """The bounds of each cell. Return a 2D array with the coordinates that define the bounds of each cell in the mesh. Each row of the array contains the bounds for a particular cell in the following order: ``x1``, ``x2``, ``y1``, ``y2``, ``z1``, ``z2``, where ``x1 < x2``, ``y1 < y2`` and ``z1 < z2``. """ nodes = self.nodes.reshape((*self.shape_nodes, -1), order="F") min_nodes = nodes[(slice(-1),) * self.dim] min_nodes = min_nodes.reshape((self.n_cells, -1), order="F") max_nodes = nodes[(slice(1, None),) * self.dim] max_nodes = max_nodes.reshape((self.n_cells, -1), order="F") cell_bounds = np.stack((min_nodes, max_nodes), axis=-1) cell_bounds = cell_bounds.reshape((self.n_cells, -1)) return cell_bounds @property def cell_nodes(self): """The index of all nodes for each cell. The nodes for each cell are listed following an "F" order: the first coordinate (``x``) changes faster than the second one (``y``). If the mesh is 3D, the second coordinate (``y``) changes faster than the third one (``z``). Returns ------- numpy.ndarray of int Index array of shape (n_cells, 4) if 2D, or (n_cells, 8) if 3D Notes ----- For a 2D mesh, the nodes indices for a single cell are returned in the following order: .. code:: 2 -- 3 | | 0 -- 1 For a 3D mesh, the nodes indices for a single cell are returned in the following order: .. code:: 6-----7 /| /| 4-----5 | | | | | | 2---|-3 |/ |/ 0-----1 """ order = "F" nodes_indices = np.arange(self.n_nodes).reshape(self.shape_nodes, order=order) if self.dim == 1: cell_nodes = [ nodes_indices[:-1].reshape(-1, order=order), nodes_indices[1:].reshape(-1, order=order), ] elif self.dim == 2: cell_nodes = [ nodes_indices[:-1, :-1].reshape(-1, order=order), nodes_indices[1:, :-1].reshape(-1, order=order), nodes_indices[:-1, 1:].reshape(-1, order=order), nodes_indices[1:, 1:].reshape(-1, order=order), ] else: cell_nodes = [ nodes_indices[:-1, :-1, :-1].reshape(-1, order=order), nodes_indices[1:, :-1, :-1].reshape(-1, order=order), nodes_indices[:-1, 1:, :-1].reshape(-1, order=order), nodes_indices[1:, 1:, :-1].reshape(-1, order=order), nodes_indices[:-1, :-1, 1:].reshape(-1, order=order), nodes_indices[1:, :-1, 1:].reshape(-1, order=order), nodes_indices[:-1, 1:, 1:].reshape(-1, order=order), nodes_indices[1:, 1:, 1:].reshape(-1, order=order), ] cell_nodes = np.stack(cell_nodes, axis=-1) return cell_nodes @property def cell_boundary_indices(self): """Return the indices of the x, (y and z) boundary cells. This property returns the indices of the cells on the x, (y and z) boundaries, respectively. Note that each axis direction will have both a lower and upper boundary. The property will return the indices corresponding to the lower and upper boundaries separately. E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total). In this case, the return is a list of length 4 organized [ind_Bx1, ind_Bx2, ind_By1, ind_By2]:: By2 + ------------- + | | | | Bx1 | | Bx2 | | | | + ------------- + By1 Returns ------- (2 * dim) list of numpy.ndarray of bool The length of list returned depends on the dimension of the mesh (= 2 x dim). And the length of each array containing the indices is equal to the number of cells in the mesh. For 1D, 2D and 3D tensor meshes, the returns take the following form: - *1D:* returns [ind_Bx1, ind_Bx2] - *2D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2] - *3D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2, ind_Bz1, ind_Bz2] Examples -------- Here, we construct a 4 by 3 cell 2D tensor mesh and return the indices of the x and y-boundary cells. In this case there are 3 cells touching each x-boundary, and there are 4 cells touching each y-boundary. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> hx = [1, 1, 1, 1] >>> hy = [2, 2, 2] >>> mesh = TensorMesh([hx, hy]) >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.cell_boundary_indices >>> ax = plt.subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(*mesh.cell_centers[ind_Bx1].T) >>> plt.show() """ if self.dim == 1: indxd = self.gridCC == min(self.gridCC) indxu = self.gridCC == max(self.gridCC) return indxd, indxu elif self.dim == 2: indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0]) indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0]) indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1]) indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1]) return indxd, indxu, indyd, indyu elif self.dim == 3: indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0]) indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0]) indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1]) indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1]) indzd = self.gridCC[:, 2] == min(self.gridCC[:, 2]) indzu = self.gridCC[:, 2] == max(self.gridCC[:, 2]) return indxd, indxu, indyd, indyu, indzd, indzu def point2index(self, locs): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh locs = as_array_n_by_dim(locs, self.dim) # in each dimension do a sorted search within the nodes # arrays to find the containing cell in that dimension cell_bounds = [ self.nodes_x, ] if self.dim > 1: cell_bounds.append(self.nodes_y) if self.dim == 3: cell_bounds.append(self.nodes_z) # subtract 1 here because given the nodes [0, 1], the point 0.5 would be inserted # at index 1 to maintain the sorted list, but that corresponds to cell 0. # clipping here ensures that anything outside the mesh will return the nearest cell. multi_inds = tuple( np.clip(np.searchsorted(n, p) - 1, 0, len(n) - 2) for n, p in zip(cell_bounds, locs.T) ) # and of course, we are fortran ordered in a tensor mesh. if self.dim == 1: return multi_inds[0] else: return np.ravel_multi_index(multi_inds, self.shape_cells, order="F") def _repr_attributes(self): """Represent attributes of the mesh.""" attrs = {} attrs["names"] = ["x", "y", "z"][: self.dim] # Loop over dimensions. for i in range(self.dim): name = attrs["names"][i] # Name of this dimension attrs[name] = {} # Get min/max node. n_vector = getattr(self, "nodes_" + name) attrs[name]["min"] = np.nanmin(n_vector) attrs[name]["max"] = np.nanmax(n_vector) # Get min/max cell width. h_vector = self.h[i] attrs[name]["h_min"] = np.nanmin(h_vector) attrs[name]["h_max"] = np.nanmax(h_vector) # Get max stretching factor. if len(h_vector) < 2: attrs[name]["max_fact"] = 1.0 else: attrs[name]["max_fact"] = np.nanmax( np.r_[h_vector[:-1] / h_vector[1:], h_vector[1:] / h_vector[:-1]] ) # Add number of cells. attrs[name]["nC"] = self.shape_cells[i] return attrs # DEPRECATIONS areaFx = deprecate_property( "face_x_areas", "areaFx", removal_version="1.0.0", error=True ) areaFy = deprecate_property( "face_y_areas", "areaFy", removal_version="1.0.0", error=True ) areaFz = deprecate_property( "face_z_areas", "areaFz", removal_version="1.0.0", error=True ) edgeEx = deprecate_property( "edge_x_lengths", "edgeEx", removal_version="1.0.0", error=True ) edgeEy = deprecate_property( "edge_y_lengths", "edgeEy", removal_version="1.0.0", error=True ) edgeEz = deprecate_property( "edge_z_lengths", "edgeEz", removal_version="1.0.0", error=True ) faceBoundaryInd = deprecate_property( "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0", error=True, ) cellBoundaryInd = deprecate_property( "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0", error=True, ) def _slice_to_index(index_slice, end): """Generate indices from a slice. Parameters ---------- index_slice : slice Slice for cell indices along a single dimension end : int End of the slice. Will use this value as the stop in case the `index_slice.stop` is None. Returns ------- Generator """ if (start := index_slice.start) is None: start = 0 if (stop := index_slice.stop) is None: stop = end if (step := index_slice.step) is None: step = 1 if start < 0: start += end if stop < 0: stop += end if step < 0: return reversed(range(start, stop, abs(step))) return range(start, stop, step) ================================================ FILE: discretize/tests.py ================================================ """ =========================================== Testing Utilities (:mod:`discretize.tests`) =========================================== .. currentmodule:: discretize.tests This module contains utilities for convergence testing. Classes ------- .. autosummary:: :toctree: generated/ OrderTest Functions --------- .. autosummary:: :toctree: generated/ check_derivative rosenbrock get_quadratic setup_mesh assert_isadjoint """ # NOQA D205 import warnings import numpy as np import scipy.sparse as sp from discretize.utils import mkvc, example_curvilinear_grid, requires from discretize.tensor_mesh import TensorMesh from discretize.curvilinear_mesh import CurvilinearMesh from discretize.cylindrical_mesh import CylindricalMesh from discretize.utils.code_utils import deprecate_function from . import TreeMesh as Tree import unittest import inspect try: import getpass name = getpass.getuser()[0].upper() + getpass.getuser()[1:] except Exception: name = "You" happiness = [ "The test be workin!", "You get a gold star!", "Yay passed!", "Happy little convergence test!", "That was easy!", "Testing is important.", "You are awesome.", "Go Test Go!", "Once upon a time, a happy little test passed.", "And then everyone was happy.", "Not just a pretty face " + name, "You deserve a pat on the back!", "Well done " + name + "!", "Awesome, " + name + ", just awesome.", ] sadness = [ "No gold star for you.", "Try again soon.", "Thankfully, persistence is a great substitute for talent.", "It might be easier to call this a feature...", "Coffee break?", "Boooooooo :(", "Testing is important. Do it again.", "Did you put your clever trousers on today?", "Just think about a dancing dinosaur and life will get better!", "You had so much promise " + name + ", oh well...", name.upper() + " ERROR!", "Get on it " + name + "!", "You break it, you fix it.", ] _happiness_rng = np.random.default_rng() def _warn_random_test(): stack = inspect.stack() in_pytest = any(x[0].f_globals["__name__"].startswith("_pytest.") for x in stack) in_nosetest = any(x[0].f_globals["__name__"].startswith("nose.") for x in stack) if in_pytest or in_nosetest: test = "pytest" if in_pytest else "nosetest" warnings.warn( f"You are running a {test} without setting a random seed, the results might not " "be repeatable. For repeatable tests please pass an argument to `random seed` " "that is not `None`.", UserWarning, stacklevel=3, ) return in_pytest or in_nosetest def setup_mesh(mesh_type, nC, nDim, random_seed=None): """Generate arbitrary mesh for testing. For the mesh type, number of cells along each axis and dimension specified, **setup_mesh** will construct a random mesh that can be used for testing. By design, the domain width is 1 along each axis direction. Parameters ---------- mesh_type : str Defines the mesh type. Must be one of **{'uniformTensorMesh', 'randomTensorMesh', 'uniformCylindricalMesh', 'randomCylindricalMesh', 'uniformTree', 'randomTree', 'uniformCurv', 'rotateCurv', 'sphereCurv'}** nC : int Number of cells along each axis. If *mesh_type* is 'Tree', then *nC* defines the number of base mesh cells and must be a power of 2. nDim : int The dimension of the mesh. Must be 1, 2 or 3. random_seed : numpy.random.Generator, int, optional If ``random`` is in `mesh_type`, this is the random number generator to use for creating that random mesh. If an integer or None it is used to seed a new `numpy.random.default_rng`. Returns ------- discretize.base.BaseMesh A discretize mesh of class specified by the input argument *mesh_type* """ if "random" in mesh_type: if random_seed is None: _warn_random_test() rng = np.random.default_rng(random_seed) if "TensorMesh" in mesh_type: if "uniform" in mesh_type: h = [nC, nC, nC] elif "random" in mesh_type: h1 = rng.random(nC) * nC * 0.5 + nC * 0.5 h2 = rng.random(nC) * nC * 0.5 + nC * 0.5 h3 = rng.random(nC) * nC * 0.5 + nC * 0.5 h = [hi / np.sum(hi) for hi in [h1, h2, h3]] # normalize else: raise Exception("Unexpected mesh_type") mesh = TensorMesh(h[:nDim]) max_h = max([np.max(hi) for hi in mesh.h]) elif "CylindricalMesh" in mesh_type or "CylMesh" in mesh_type: if "uniform" in mesh_type: if "symmetric" in mesh_type: h = [nC, 1, nC] else: h = [nC, nC, nC] elif "random" in mesh_type: h1 = rng.random(nC) * nC * 0.5 + nC * 0.5 if "symmetric" in mesh_type: h2 = [ 2 * np.pi, ] else: h2 = rng.random(nC) * nC * 0.5 + nC * 0.5 h3 = rng.random(nC) * nC * 0.5 + nC * 0.5 h = [hi / np.sum(hi) for hi in [h1, h2, h3]] # normalize h[1] = h[1] * 2 * np.pi else: raise Exception("Unexpected mesh_type") if nDim == 2: mesh = CylindricalMesh([h[0], h[1]]) if "symmetric" in mesh_type: max_h = np.max(mesh.h[0]) else: max_h = max([np.max(hi) for hi in mesh.h]) elif nDim == 3: mesh = CylindricalMesh(h) if "symmetric" in mesh_type: max_h = max([np.max(hi) for hi in [mesh.h[0], mesh.h[2]]]) else: max_h = max([np.max(hi) for hi in mesh.h]) elif "Curv" in mesh_type: if "uniform" in mesh_type: kwrd = "rect" elif "rotate" in mesh_type: kwrd = "rotate" elif "sphere" in mesh_type: kwrd = "sphere" else: raise Exception("Unexpected mesh_type") if nDim == 1: raise Exception("Lom not supported for 1D") elif nDim == 2: X, Y = example_curvilinear_grid([nC, nC], kwrd) mesh = CurvilinearMesh([X, Y]) elif nDim == 3: X, Y, Z = example_curvilinear_grid([nC, nC, nC], kwrd) mesh = CurvilinearMesh([X, Y, Z]) max_h = 1.0 / nC elif "Tree" in mesh_type: if Tree is None: raise Exception("Tree Mesh not installed. Run 'python setup.py install'") nC *= 2 if "uniform" in mesh_type or "notatree" in mesh_type: h = [nC, nC, nC] elif "random" in mesh_type: h1 = rng.random(nC) * nC * 0.5 + nC * 0.5 h2 = rng.random(nC) * nC * 0.5 + nC * 0.5 h3 = rng.random(nC) * nC * 0.5 + nC * 0.5 h = [hi / np.sum(hi) for hi in [h1, h2, h3]] # normalize else: raise Exception("Unexpected mesh_type") levels = int(np.log(nC) / np.log(2)) mesh = Tree(h[:nDim], levels=levels) def function(cell): if "notatree" in mesh_type: return levels - 1 r = cell.center - 0.5 dist = np.sqrt(r.dot(r)) if dist < 0.2: return levels return levels - 1 mesh.refine(function) # mesh.number() # mesh.plot_grid(show_it=True) max_h = max([np.max(hi) for hi in mesh.h]) return mesh, max_h class OrderTest(unittest.TestCase): r"""Base class for testing convergence of discrete operators with respect to cell size. ``OrderTest`` is a base class for testing the order of convergence of discrete operators with respect to cell size. ``OrderTest`` is inherited by the test class for the given operator. Within the test class, the user sets the parameters for the convergence testing and defines a method :py:attr:`~OrderTest.getError` which defines the error as a norm of the residual (see example). OrderTest inherits from :class:`unittest.TestCase`. Attributes ---------- name : str Name the convergence test meshTypes : list of str List denoting the mesh types on which the convergence will be tested. List entries must be of the list {'uniformTensorMesh', 'randomTensorMesh', 'uniformCylindricalMesh', 'randomCylindricalMesh', 'uniformTree', 'randomTree', 'uniformCurv', 'rotateCurv', 'sphereCurv'} expectedOrders : float or list of float (default = 2.0) Defines the expect orders of convergence for all meshes defined in *meshTypes*. If list, must have same length as argument *meshTypes*. tolerance : float or list of float (default = 0.85) Defines tolerance for numerical approximate of convergence order. If list, must have same length as argument *meshTypes*. meshSizes : list of int From coarsest to finest, defines the number of mesh cells in each axis direction for the meshes used in the convergence test; e.g. [4, 8, 16, 32] meshDimension : int Mesh dimension. Must be 1, 2 or 3 random_seed : numpy.random.Generator, int, optional If ``random`` is in `mesh_type`, this is the random number generator used generate the random meshes, if an ``int`` or ``None``, it used to seed a new `numpy.random.default_rng`. Notes ----- Consider an operator :math:`A(f)` that acts on a test function :math:`f`. And let :math:`A_h (f)` be the discrete approximation to the original operator constructed on a mesh will cell size :math:`h`. ``OrderTest`` assesses the convergence of .. math:: error(h) = \| A_h(f) - A(f) \| as :math:`h \rightarrow 0`. Note that you can provide any norm to quantify the error. The convergence test is passed when the numerically estimated rate of convergence is within a specified tolerance of the expected convergence rate supplied by the user. Examples -------- Here, we utilize the ``OrderTest`` class to validate the rate of convergence for the :py:attr:`~discretize.differential_operators.face_divergence`. Our convergence test is done for a uniform 2D tensor mesh. Under the test class *TestDIV2D*, we define the static parameters for the order test. We then define a method *getError* for this class which returns the norm of some residual. With these two pieces defined, we can call the order test as shown below. >>> from discretize.tests import OrderTest >>> import unittest >>> import numpy as np >>> class TestDIV2D(OrderTest): ... # Static properties for OrderTest ... name = "Face Divergence 2D" ... meshTypes = ["uniformTensorMesh"] ... meshDimension = 2 ... expectedOrders = 2.0 ... tolerance = 0.85 ... meshSizes = [8, 16, 32, 64] ... def getError(self): ... # Test function ... fx = lambda x, y: np.sin(2 * np.pi * x) ... fy = lambda x, y: np.sin(2 * np.pi * y) ... # Analytic solution for operator acting on test function ... sol = lambda x, y: 2 * np.pi * (np.cos(2 * np.pi * x) + np.cos(2 * np.pi * y)) ... # Evaluate test function on faces ... f = np.r_[ ... fx(self.M.faces_x[:, 0], self.M.faces_x[:, 1]), ... fy(self.M.faces_y[:, 0], self.M.faces_y[:, 1]) ... ] ... # Analytic solution at cell centers ... div_f = sol(self.M.cell_centers[:, 0], self.M.cell_centers[:, 1]) ... # Numerical approximation of divergence at cell centers ... div_f_num = self.M.face_divergence * f ... # Define the error function as a norm ... err = np.linalg.norm((div_f_num - div_f), np.inf) ... return err ... def test_order(self): ... self.orderTest() """ name = "Order Test" expectedOrders = ( 2.0 # This can be a list of orders, must be the same length as meshTypes ) tolerance = 0.85 # This can also be a list, must be the same length as meshTypes meshSizes = [4, 8, 16, 32] meshTypes = ["uniformTensorMesh"] _meshType = meshTypes[0] meshDimension = 3 random_seed = None def setupMesh(self, nC): """Generate mesh and set as current mesh for testing. Parameters ---------- nC : int Number of cells along each axis. Returns ------- Float Maximum cell width for the mesh """ mesh, max_h = setup_mesh( self._meshType, nC, self.meshDimension, random_seed=self.random_seed ) self.M = mesh return max_h def getError(self): r"""Compute error defined as a norm of the residual. This method is overwritten within the test class of a particular operator. Within the method, we define a test function :math:`f`, the analytic solution of an operator :math:`A(f)` acting on the test function, and the numerical approximation obtained by applying the discretized operator :math:`A_h (f)`. **getError** is defined to return the norm of the residual as shown below: .. math:: error(h) = \| A_h(f) - A(f) \| """ return 1.0 def orderTest(self, random_seed=None): """Perform an order test. For number of cells specified in meshSizes setup mesh, call getError and prints mesh size, error, ratio between current and previous error, and estimated order of convergence. """ __tracebackhide__ = True if not isinstance(self.meshTypes, list): raise TypeError("meshTypes must be a list") if type(self.tolerance) is not list: self.tolerance = np.ones(len(self.meshTypes)) * self.tolerance # if we just provide one expected order, repeat it for each mesh type if isinstance(self.expectedOrders, (float, int)): self.expectedOrders = [self.expectedOrders for i in self.meshTypes] try: self.expectedOrders = list(self.expectedOrders) except TypeError: raise TypeError("expectedOrders must be array like") if len(self.expectedOrders) != len(self.meshTypes): raise ValueError( "expectedOrders must have the same length as the meshTypes" ) if random_seed is not None: self.random_seed = random_seed def test_func(n_cells): max_h = self.setupMesh(n_cells) err = self.getError() return err, max_h for mesh_type, order, tolerance in zip( self.meshTypes, self.tolerance, self.expectedOrders ): self._meshType = mesh_type assert_expected_order( test_func, self.meshSizes, expected_order=order, rtol=np.abs(1 - tolerance), test_type="mean_at_least", ) def assert_expected_order( func, n_cells, expected_order=2.0, rtol=0.15, test_type="mean" ): """Perform an order test. For number of cells specified in `mesh_sizes` call `func` and prints mesh size, error, ratio between current and previous error, and estimated order of convergence. Parameters ---------- func : callable Function which should accept an integer representing the number of discretizations on the domain and return a tuple of the error and the discretization widths. n_cells : array_like of int List of number of discretizations to pass to func. expected_order : float, optional The expected order of accuracy for you test rtol : float, optional The relative tolerance of the order test. test_type : {'mean', 'min', 'last', 'all', 'mean_at_least'} Which property of the list of calculated orders to test. Returns ------- numpy.ndarray The calculated order values on success Raises ------ AssertionError Notes ----- For the different ``test_type`` arguments, different properties of the order is tested: - `mean`: the mean value of all calculated orders is tested for approximate equality with the expected order. - `min`: The minimimum value of calculated orders is tested for approximate equality with the expected order. - `last`: The last calculated order is tested for approximate equality with the expected order. - `all`: All calculated orders are tested for approximate equality with the expected order. - `mean_at_least`: The mean is tested to be at least approximately the expected order. This is the default test for the previous ``OrderTest`` class in older versions of `discretize`. Examples -------- Testing the convergence order of an central difference operator >>> from discretize.tests import assert_expected_order >>> func = lambda y: np.cos(y) >>> func_deriv = lambda y: -np.sin(y) Define the function that returns the error and cell width for a given number of discretizations. >>> def deriv_error(n): ... # grid points ... nodes = np.linspace(0, 1, n+1) ... cc = 0.5 * (nodes[1:] + nodes[:-1]) ... dh = nodes[1]-nodes[0] ... # evaluate the function on nodes ... node_eval = func(nodes) ... # calculate the numerical derivative ... num_deriv = (node_eval[1:] - node_eval[:-1]) / dh ... # calculate the true derivative ... true_deriv = func_deriv(cc) ... # compare the L-inf norm of the error vector ... err = np.linalg.norm(num_deriv - true_deriv, ord=np.inf) ... return err, dh Then run the expected order test. >>> assert_expected_order(deriv_error, [10, 20, 30, 40, 50]) """ __tracebackhide__ = True n_cells = np.asarray(n_cells, dtype=int) if test_type not in ["mean", "min", "last", "all", "mean_at_least"]: raise ValueError orders = [] # Do first values: nc = n_cells[0] err_last, h_last = func(nc) print("_______________________________________________________") print(" nc | h | error | e(i-1)/e(i) | order ") print("~~~~~~|~~~~~~~~~|~~~~~~~~~~~~~|~~~~~~~~~~~~~|~~~~~~~~~~") print(f"{nc:^6d}|{h_last:^9.2e}|{err_last:^13.3e}| |") for nc in n_cells[1:]: err, h = func(nc) order = np.log(err / err_last) / np.log(h / h_last) print(f"{nc:^6d}|{h:^9.2e}|{err:^13.3e}|{err_last / err:^13.4f}|{order:^10.4f}") err_last = err h_last = h orders.append(order) print("-------------------------------------------------------") try: if test_type == "mean": np.testing.assert_allclose(np.mean(orders), expected_order, rtol=rtol) elif test_type == "mean_at_least": test = np.mean(orders) > expected_order * (1 - rtol) if not test: raise AssertionError( f"\nOrder mean {np.mean(orders)} is not greater than the expected order " f"{expected_order} within the tolerance {rtol}." ) elif test_type == "min": np.testing.assert_allclose(np.min(orders), expected_order, rtol=rtol) elif test_type == "last": np.testing.assert_allclose(orders[-1], expected_order, rtol=rtol) elif test_type == "all": np.testing.assert_allclose(orders, expected_order, rtol=rtol) print(_happiness_rng.choice(happiness)) except AssertionError as err: print(_happiness_rng.choice(sadness)) raise err return orders def rosenbrock(x, return_g=True, return_H=True): """Evaluate the Rosenbrock function. This is mostly used for testing Gauss-Newton schemes Parameters ---------- x : numpy.ndarray The (x0, x1) location for the Rosenbrock test return_g : bool, optional If *True*, return the gradient at *x* return_H : bool, optional If *True*, return the approximate Hessian at *x* Returns ------- tuple Rosenbrock function evaluated at (x0, x1), the gradient at (x0, x1) if *return_g = True* and the Hessian at (x0, x1) if *return_H = True* """ f = 100 * (x[1] - x[0] ** 2) ** 2 + (1 - x[0]) ** 2 g = np.array( [2 * (200 * x[0] ** 3 - 200 * x[0] * x[1] + x[0] - 1), 200 * (x[1] - x[0] ** 2)] ) H = sp.csr_matrix( np.array( [[-400 * x[1] + 1200 * x[0] ** 2 + 2, -400 * x[0]], [-400 * x[0], 200]] ) ) out = (f,) if return_g: out += (g,) if return_H: out += (H,) return out if len(out) > 1 else out[0] def check_derivative( fctn, x0, num=7, plotIt=False, dx=None, expectedOrder=2, tolerance=0.85, eps=1e-10, ax=None, random_seed=None, ): """Perform a basic derivative check. Compares error decay of 0th and 1st order Taylor approximation at point x0 for a randomized search direction. Parameters ---------- fctn : callable The function to test. x0 : numpy.ndarray Point at which to check derivative num : int, optional number of times to reduce step length to evaluate derivative plotIt : bool, optional If *True*, plot the convergence of the approximation of the derivative dx : numpy.ndarray, optional Step direction. By default, this parameter is set to *None* and a random step direction is chosen using `rng`. expectedOrder : int, optional The expected order of convergence for the numerical derivative tolerance : float, optional The tolerance on the expected order eps : float, optional A threshold value for approximately equal to zero ax : matplotlib.pyplot.Axes, optional An axis object for the convergence plot if *plotIt = True*. Otherwise, the function will create a new axis. random_seed : numpy.random.Generator, int, optional If `dx` is ``None``, this is the random number generator to use for generating a step direction. If an integer or None, it is used to seed a new `numpy.random.default_rng`. Returns ------- bool Whether you passed the test. Examples -------- >>> from discretize import tests, utils >>> import numpy as np >>> import matplotlib.pyplot as plt >>> rng = np.random.default_rng(786412) >>> def simplePass(x): ... return np.sin(x), utils.sdiag(np.cos(x)) >>> passed = tests.check_derivative(simplePass, rng.standard_normal(5), random_seed=rng) ==================== check_derivative ==================== iter h |ft-f0| |ft-f0-h*J0*dx| Order --------------------------------------------------------- 0 1.00e-01 1.690e-01 8.400e-03 nan 1 1.00e-02 1.636e-02 8.703e-05 1.985 2 1.00e-03 1.630e-03 8.732e-07 1.999 3 1.00e-04 1.629e-04 8.735e-09 2.000 4 1.00e-05 1.629e-05 8.736e-11 2.000 5 1.00e-06 1.629e-06 8.736e-13 2.000 6 1.00e-07 1.629e-07 8.822e-15 1.996 ========================= PASS! ========================= Once upon a time, a happy little test passed. """ __tracebackhide__ = True # matplotlib is a soft dependencies for discretize, # lazy-loaded to decrease load time of discretize. try: import matplotlib import matplotlib.pyplot as plt except ImportError: matplotlib = False print("{0!s} check_derivative {1!s}".format("=" * 20, "=" * 20)) print( "iter h |ft-f0| |ft-f0-h*J0*dx| Order\n{0!s}".format(("-" * 57)) ) f0, J0 = fctn(x0) x0 = mkvc(x0) if dx is None: if random_seed is None: _warn_random_test() rng = np.random.default_rng(random_seed) dx = rng.standard_normal(len(x0)) h = np.logspace(-1, -num, num) E0 = np.ones(h.shape) E1 = np.ones(h.shape) def l2norm(x): # because np.norm breaks if they are scalars? return np.sqrt(np.real(np.vdot(x, x))) for i in range(num): # Evaluate at test point ft, Jt = fctn(x0 + h[i] * dx) # 0th order Taylor E0[i] = l2norm(ft - f0) # 1st order Taylor if inspect.isfunction(J0): E1[i] = l2norm(ft - f0 - h[i] * J0(dx)) else: # We assume it is a numpy.ndarray E1[i] = l2norm(ft - f0 - h[i] * J0.dot(dx)) order0 = np.log10(E0[:-1] / E0[1:]) order1 = np.log10(E1[:-1] / E1[1:]) print( " {0:d} {1:1.2e} {2:1.3e} {3:1.3e} {4:1.3f}".format( i, h[i], E0[i], E1[i], np.nan if i == 0 else order1[i - 1] ) ) @requires({"matplotlib": matplotlib}) def _plot_it(axes, passed): if axes is None: axes = plt.subplot(111) axes.loglog(h, E0, "b") axes.loglog(h, E1, "g--") axes.set_title( "Check Derivative - {0!s}".format(("PASSED :)" if passed else "FAILED :(")) ) axes.set_xlabel("h") axes.set_ylabel("Error") leg = axes.legend( [r"$\mathcal{O}(h)$", r"$\mathcal{O}(h^2)$"], loc="best", title=r"$f(x + h\Delta x) - f(x) - h g(x) \Delta x - \mathcal{O}(h^2) = 0$", frameon=False, ) plt.setp(leg.get_title(), fontsize=15) plt.show() # Ensure we are about precision order0 = order0[E0[1:] > eps] order1 = order1[E1[1:] > eps] # belowTol = order1.size == 0 and order0.size >= 0 # # Make sure we get the correct order # correctOrder = order1.size > 0 and np.mean(order1) > tolerance * expectedOrder # # passTest = belowTol or correctOrder try: if order1.size == 0: # This should happen if all of the 1st order taylor approximation errors # were below epsilon, common if the original function was linear. # Thus it has no higher order derivatives. pass else: order_mean = np.mean(order1) expected = tolerance * expectedOrder test = order_mean > expected if not test: raise AssertionError( f"\n Order mean {order_mean} is not greater than" f" {expected} = tolerance: {tolerance} " f"* expected order: {expectedOrder}." ) print("{0!s} PASS! {1!s}".format("=" * 25, "=" * 25)) print(_happiness_rng.choice(happiness) + "\n") if plotIt: _plot_it(ax, True) except AssertionError as err: print( "{0!s}\n{1!s} FAIL! {2!s}\n{3!s}".format( "*" * 57, "<" * 25, ">" * 25, "*" * 57 ) ) print(_happiness_rng.choice(sadness) + "\n") if plotIt: _plot_it(ax, False) raise err return True def get_quadratic(A, b, c=0): r"""Return a function that evaluates the given quadratic. Given **A**, **b** and *c*, this returns a function that evaluates the quadratic for a vector **x**. Where :math:`\mathbf{A} \in \mathbb{R}^{NxN}`, :math:`\mathbf{b} \in \mathbb{R}^N` and :math:`c` is a constant, this function evaluates the following quadratic: .. math:: Q( \mathbf{x} ) = \frac{1}{2} \mathbf{x^T A x + b^T x} + c for a vector :math:`\mathbf{x}`. It also optionally returns the gradient of the above equation, and its Hessian. Parameters ---------- A : (N, N) numpy.ndarray A square matrix b : (N) numpy.ndarray A vector c : float A constant Returns ------- function : The callable function that returns the quadratic evaluation, and optionally its gradient, and Hessian. """ def Quadratic(x, return_g=True, return_H=True): f = 0.5 * x.dot(A.dot(x)) + b.dot(x) + c out = (f,) if return_g: g = A.dot(x) + b out += (g,) if return_H: H = A out += (H,) return out if len(out) > 1 else out[0] return Quadratic def assert_isadjoint( forward, adjoint, shape_u, shape_v, complex_u=False, complex_v=False, clinear=True, rtol=1e-6, atol=0.0, assert_error=True, random_seed=None, ): r"""Do a dot product test for the forward operator and its adjoint operator. Dot product test to verify the correctness of the adjoint operator :math:`F^H` of the forward operator :math:`F`. .. math:: \mathbf{v}^H ( \mathbf{F} \mathbf{u} ) = ( \mathbf{F}^H \mathbf{v} )^H \mathbf{u} Parameters ---------- forward : callable Forward operator. adjoint : callable Adjoint operator. shape_u : int, tuple of int Shape of vector ``u`` passed in to ``forward``; it is accordingly the expected shape of the vector returned from the ``adjoint``. shape_v : int, tuple of int Shape of vector ``v`` passed in to ``adjoint``; it is accordingly the expected shape of the vector returned from the ``forward``. complex_u : bool, default: False If True, vector ``u`` passed to ``forward`` is a complex vector; accordingly the ``adjoint`` is expected to return a complex vector. complex_v : bool, default: False If True, vector ``v`` passed to ``adjoint`` is a complex vector; accordingly the ``forward`` is expected to return a complex vector. clinear : bool, default: True If operator is complex-linear (True) or real-linear (False). rtol : float, default: 1e-6 Relative tolerance. atol : float, default: 0.0 Absolute tolerance. assert_error : bool, default: True By default this test is an assertion (silent if passed, raising an assertion error if failed). If set to False, the result of the test is returned as boolean and a message is printed. random_seed : numpy.random.Generator, int, optional The random number generator to use for the adjoint test. If an integer or None it is used to seed a new `numpy.random.default_rng`. Returns ------- passed : bool, optional Result of the dot product test; only returned if ``assert_error`` is False. Raises ------ AssertionError If the dot product test fails (only if assert_error=True). """ __tracebackhide__ = True if random_seed is None: _warn_random_test() rng = np.random.default_rng(random_seed) def random(size, iscomplex): """Create random data of size and dtype of .""" out = rng.standard_normal(size) if iscomplex: out = out + 1j * rng.standard_normal(size) return out # Create random vectors u and v. u = random(np.prod(shape_u), complex_u).reshape(shape_u) v = random(np.prod(shape_v), complex_v).reshape(shape_v) # Carry out dot product test. fwd_u = forward(u) adj_v = adjoint(v) if clinear: lhs = np.vdot(v, fwd_u) # lhs := v^H * (fwd * u) rhs = np.vdot(adj_v, u) # rhs := (adj * v)^H * u else: lhs = np.vdot(v.real, fwd_u.real) + np.vdot(v.imag, fwd_u.imag) rhs = np.vdot(adj_v.real, u.real) + np.vdot(adj_v.imag, u.imag) # Check if they are the same. if assert_error: np.testing.assert_allclose( rhs, lhs, rtol=rtol, atol=atol, err_msg="Adjoint test failed" ) else: passed = np.allclose(rhs, lhs, rtol=rtol, atol=atol) print( f"Adjoint test {'PASSED' if passed else 'FAILED'} :: " f"{abs(rhs-lhs):.3e} < {atol+rtol*abs(lhs):.3e} :: " f"|rhs-lhs| < atol + rtol|lhs|" ) return passed def assert_cell_intersects_geometric( cell, points, edges=None, faces=None, as_refine=False ): """Assert if a cell intersects a convex polygon. Parameters ---------- cell : tree_mesh.TreeCell Must have cell.origin and cell.h properties points : (*, dim) array_like The points of the geometric object. edges : (*, 2) array_like of int, optional The 2 indices into points defining each edge faces : (*, 3) array_like of int, optional The 3 indices into points which lie on each face. These are used to define the face normals from the three points as ``norm = cross(p1 - p0, p2 - p0)``. as_refine : bool, or int If ``True`` (or a nonzero integer), this function will not assert and instead return either 0, -1, or the integer making it suitable (but slow) for refining a TreeMesh. Returns ------- int Raises ------ AssertionError """ __tracebackhide__ = True x0 = cell.origin xF = x0 + cell.h points = np.atleast_2d(points) if edges is not None: edges = np.atleast_2d(edges) if edges.shape[-1] != 2: raise ValueError("Last dimension of edges must be 2.") if faces is not None: faces = np.atleast_2d(faces) if faces.shape[-1] != 3: raise ValueError("Last dimension of faces must be 3.") do_asserts = not as_refine level = -1 if as_refine and not isinstance(as_refine, bool): level = int(as_refine) dim = points.shape[-1] # first the bounding box tests (associated with the 3 face normals of the cell mins = points.min(axis=0) for i_d in range(dim): if do_asserts: assert mins[i_d] <= xF[i_d] else: if mins[i_d] > xF[i_d]: return 0 maxs = points.max(axis=0) for i_d in range(dim): if do_asserts: assert maxs[i_d] >= x0[i_d] else: if maxs[i_d] < x0[i_d]: return 0 # create array of all the box points if edges is not None or faces is not None: box_points = np.meshgrid(*list(zip(x0, xF))) box_points = np.stack(box_points, axis=-1).reshape(-1, dim) def project_min_max(points, axis): ps = points @ axis return ps.min(), ps.max() if edges is not None and dim > 1: box_dirs = np.eye(dim) edge_dirs = points[edges[:, 1]] - points[edges[:, 0]] # perform the edge-edge intersection tests # these project all points onto the axis formed by the cross # product of the geometric edges and the bounding box's edges/faces normals for i in range(edges.shape[0]): for j in range(dim): if dim == 3: axis = np.cross(edge_dirs[i], box_dirs[j]) else: axis = [-edge_dirs[i, 1], edge_dirs[i, 0]] bmin, bmax = project_min_max(box_points, axis) gmin, gmax = project_min_max(points, axis) if do_asserts: assert bmax >= gmin and bmin <= gmax else: if bmax < gmin or bmin > gmax: return 0 if faces is not None and dim > 2: face_normals = np.cross( points[faces[:, 1]] - points[faces[:, 0]], points[faces[:, 2]] - points[faces[:, 0]], ) for i in range(faces.shape[0]): bmin, bmax = project_min_max(box_points, face_normals[i]) gmin, gmax = project_min_max(points, face_normals[i]) if do_asserts: assert bmax >= gmin and bmin <= gmax else: if bmax < gmin or bmin > gmax: return 0 if not do_asserts: return level # DEPRECATIONS setupMesh = deprecate_function( setup_mesh, "setupMesh", removal_version="1.0.0", error=True ) Rosenbrock = deprecate_function( rosenbrock, "Rosenbrock", removal_version="1.0.0", error=True ) checkDerivative = deprecate_function( check_derivative, "checkDerivative", removal_version="1.0.0", error=True ) getQuadratic = deprecate_function( get_quadratic, "getQuadratic", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/tree_mesh.py ================================================ """Module containing the TreeMesh implementation.""" import warnings # ___ ___ ___ ___ ___ ___ # /\ \ /\ \ /\ \ /\ \ /\ \ /\ \ # /::\ \ /::\ \ \:\ \ /::\ \ /::\ \ /::\ \ # /:/\:\ \ /:/\:\ \ \:\ \ /:/\:\ \ /:/\:\ \ /:/\:\ \ # /:/ \:\ \ /:/ \:\ \ /::\ \ /::\~\:\ \ /::\~\:\ \ /::\~\:\ \ # /:/__/ \:\__\/:/__/ \:\__\ /:/\:\__\/:/\:\ \:\__\/:/\:\ \:\__\/:/\:\ \:\__\ # \:\ \ /:/ /\:\ \ \/__//:/ \/__/\/_|::\/:/ /\:\~\:\ \/__/\:\~\:\ \/__/ # \:\ /:/ / \:\ \ /:/ / |:|::/ / \:\ \:\__\ \:\ \:\__\ # \:\/:/ / \:\ \ \/__/ |:|\/__/ \:\ \/__/ \:\ \/__/ # \::/ / \:\__\ |:| | \:\__\ \:\__\ # \/__/ \/__/ \|__| \/__/ \/__/ # # # # .----------------.----------------. # /| /| /| # / | / | / | # / | 6 / | 7 / | # / | / | / | # .----------------.----+-----------. | # /| . ---------/|----.----------/|----. # / | /| / | /| / | /| # / | / | 4 / | / | 5 / | / | # / | / | / | / | / | / | # . -------------- .----------------. |/ | # | . ---+------|----.----+------|----. | # | /| .______|___/|____.______|___/|____. # | / | / 2 | / | / 3 | / | / # | / | / | / | / | / | / # . ---+---------- . ---+---------- . | / # | |/ | |/ | |/ z # | . ----------|----.-----------|----. ^ y # | / 0 | / 1 | / | / # | / | / | / | / # | / | / | / o----> x # . -------------- . -------------- . # # # Face Refinement: # # 2_______________3 _______________ # | | | | | # ^ | | | 2 | 3 | # | | | | | | # | | x | ---> |-------+-------| # t1 | | | | | # | | | 0 | 1 | # |_______________| |_______|_______| # 0 t0--> 1 # # # Face and Edge naming conventions: # # fZp # | # 6 ------eX3------ 7 # /| | / | # /eZ2 . / eZ3 # eY2 | fYp eY3 | # / | / fXp| # 4 ------eX2----- 5 | # |fXm 2 -----eX1--|---- 3 z # eZ0 / | eY1 ^ y # | eY0 . fYm eZ1 / | / # | / | | / | / # 0 ------eX0------1 o----> x # | # fZm # # # fX fY # 2___________3 2___________3 # | e1 | | e1 | # | | | | # e0 | x | e2 z e0 | x | e2 z # | | ^ | | ^ # |___________| |___> y |___________| |___> x # 0 e3 1 0 e3 1 # fZ # 2___________3 # | e1 | # | | # e0 | x | e2 y # | | ^ # |___________| |___> x # 0 e3 1 from discretize.base import BaseTensorMesh from discretize.operators import InnerProducts, DiffOperators from discretize.mixins import InterfaceMixins, TreeMeshIO from discretize._extensions.tree_ext import ( # noqa: F401 _TreeMesh, TreeCell, TreeMeshNotFinalizedError, ) import numpy as np import scipy.sparse as sp from discretize.utils.code_utils import deprecate_property from scipy.spatial import Delaunay class TreeMesh( _TreeMesh, InnerProducts, DiffOperators, BaseTensorMesh, TreeMeshIO, InterfaceMixins, ): """Class for QuadTree (2D) and OcTree (3D) meshes. Tree meshes are numerical grids where the dimensions of each cell are powers of 2 larger than some base cell dimension. Unlike the :class:`~discretize.TensorMesh` class, gridded locations and numerical operators for instances of ``TreeMesh`` cannot be simply constructed using tensor products. Furthermore, each cell is an instance of :class:`~discretize.tree_mesh.TreeCell` . Each cell of a `TreeMesh` has a certain level associated with it which describes its height in the structure of the tree. The tree mesh is refined by specifying what level you want in a given location. They start at level 0, defining the largest possible cell on the mesh, and go to a level of `max_level` describing the finest possible cell on the mesh. TreeMesh` contains several refinement functions used to design the mesh, starting with a general purpose refinement function, along with several faster specialized refinement functions based on fundamental geometric entities: - `refine` -> The general purpose refinement function supporting arbitrary defined functions - `insert_cells` - `refine_ball` - `refine_line` - `refine_triangle` - `refine_box` - `refine_tetrahedron` - `refine_vertical_trianglular_prism` - `refine_bounding_box` - `refine_points` - `refine_surface` Like array indexing in python, you can also supply negative indices as a level arguments to these functions to index levels in a reveresed order (i.e. -1 is equivalent to `max_level`). Parameters ---------- h : (dim) iterable of int, numpy.ndarray, or tuple Defines the cell widths of the *underlying tensor mesh* along each axis. The length of the iterable object is equal to the dimension of the mesh (2 or 3). For a 3D mesh, the list would have the form *[hx, hy, hz]*. The number of cells along each axis **must be a power of 2** . Along each axis, the user has 3 choices for defining the cells widths for the underlying tensor mesh: - :class:`int` -> A unit interval is equally discretized into `N` cells. - :class:`numpy.ndarray` -> The widths are explicity given for each cell - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])* where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional) is a padding factor denoting exponential increase/decrease in the cell width for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]* origin : (dim) iterable, default: 0 Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost corner. By default, the mesh is anchored such that its origin is at [0, 0, 0]. For each dimension (x, y or z), The user may set the origin 2 ways: - a ``scalar`` which explicitly defines origin along that dimension. - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along each axis is the first node location ('0'), in the center ('C') or the last node location ('N') (see Examples). diagonal_balance : bool, optional Whether to balance cells along the diagonal of the tree during construction. This will affect all calls to refine the tree. Examples -------- Here we generate a basic 2D tree mesh. >>> from discretize import TreeMesh >>> import numpy as np >>> import matplotlib.pyplot as plt Define base mesh (domain and finest discretization), >>> dh = 5 # minimum cell width (base mesh cell width) >>> nbc = 64 # number of base mesh cells >>> h = dh * np.ones(nbc) >>> mesh = TreeMesh([h, h]) Define corner points for a rectangular box, and subdived the mesh within the box to the maximum refinement level. >>> x0s = [120.0, 80.0] >>> x1s = [240.0, 160.0] >>> levels = [mesh.max_level] >>> mesh.refine_box(x0s, x1s, levels) >>> mesh.plot_grid() >>> plt.show() """ _meshType = "TREE" _aliases = { **BaseTensorMesh._aliases, **DiffOperators._aliases, **{ "ntN": "n_total_nodes", "ntEx": "n_total_edges_x", "ntEy": "n_total_edges_y", "ntEz": "n_total_edges_z", "ntE": "n_total_edges", "ntFx": "n_total_faces_x", "ntFy": "n_total_faces_y", "ntFz": "n_total_faces_z", "ntF": "n_total_faces", "nhN": "n_hanging_nodes", "nhEx": "n_hanging_edges_x", "nhEy": "n_hanging_edges_y", "nhEz": "n_hanging_edges_z", "nhE": "n_hanging_edges", "nhFx": "n_hanging_faces_x", "nhFy": "n_hanging_faces_y", "nhFz": "n_hanging_faces_z", "nhF": "n_hanging_faces", "gridhN": "hanging_nodes", "gridhFx": "hanging_faces_x", "gridhFy": "hanging_faces_y", "gridhFz": "hanging_faces_z", "gridhEx": "hanging_edges_x", "gridhEy": "hanging_edges_y", "gridhEz": "hanging_edges_z", }, } _items = {"h", "origin", "cell_state"} # inheriting stuff from BaseTensorMesh that isn't defined in _QuadTree def __init__(self, h=None, origin=None, diagonal_balance=None, **kwargs): if "x0" in kwargs: origin = kwargs.pop("x0") if diagonal_balance is None: diagonal_balance = False warnings.warn( "In discretize v1.0 the TreeMesh will change the default value of " "diagonal_balance to True, which will likely slightly change meshes " "you have previously created. " "If you need to keep the current behavior, explicitly set " "diagonal_balance=False.", FutureWarning, stacklevel=2, ) super().__init__(h=h, origin=origin, diagonal_balance=diagonal_balance) cell_state = kwargs.pop("cell_state", None) cell_indexes = kwargs.pop("cell_indexes", None) cell_levels = kwargs.pop("cell_levels", None) if cell_state is None: if cell_indexes is not None and cell_levels is not None: cell_state = {} cell_state["indexes"] = cell_indexes cell_state["levels"] = cell_levels if cell_state is not None: indexes = cell_state["indexes"] levels = cell_state["levels"] self.__setstate__((indexes, levels)) def __repr__(self): """Plain text representation.""" mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad")) top = "\n" + mesh_name + ": {0:2.2f}% filled\n\n".format(self.fill * 100) # Number of cells per level level_count = self._count_cells_per_index() non_zero_levels = np.nonzero(level_count)[0] cell_display = ["Level : Number of cells"] cell_display.append("-----------------------") for level in non_zero_levels: cell_display.append("{:^5} : {:^15}".format(level, level_count[level])) cell_display.append("-----------------------") cell_display.append("Total : {:^15}".format(self.nC)) extent_display = [" Mesh Extent "] extent_display.append(" min , max ") extent_display.append(" ---------------------------") dim_label = {0: "x", 1: "y", 2: "z"} for dim in range(self.dim): n_vector = getattr(self, "nodes_" + dim_label[dim]) extent_display.append( "{}: {:^13},{:^13}".format(dim_label[dim], n_vector[0], n_vector[-1]) ) # Return partial information if mesh is not finalized if not self.finalized: top = f"\n {mesh_name} (non finalized)\n\n" return top + "\n".join(extent_display) for i, line in enumerate(extent_display): if i == len(cell_display): cell_display.append(" " * (len(cell_display[0]) - 3 - len(line))) cell_display[i] += 3 * " " + line h_display = [" Cell Widths "] h_display.append(" min , max ") h_display.append("-" * (len(h_display[0]))) h_gridded = self.h_gridded mins = np.min(h_gridded, axis=0) maxs = np.max(h_gridded, axis=0) for dim in range(self.dim): h_display.append("{:^10}, {:^10}".format(mins[dim], maxs[dim])) for i, line in enumerate(h_display): if i == len(cell_display): cell_display.append(" " * len(cell_display[0])) cell_display[i] += 3 * " " + line return top + "\n".join(cell_display) def _repr_html_(self): """HTML representation.""" mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad")) style = " style='padding: 5px 20px 5px 20px;'" dim_label = {0: "x", 1: "y", 2: "z"} if not self.finalized: style_bold = '"font-weight: bold; font-size: 1.2em; text-align: center;"' style_regular = '"font-size: 1.2em; text-align: center;"' output = [ "", # need to close this tag "", f"{mesh_name}", f"(non finalized)", "", "", # need to close this tag "", "", f'Mesh extent', "", "", "", f"min", f"max", "", ] for dim in range(self.dim): n_vector = getattr(self, "nodes_" + dim_label[dim]) output += [ "", f"{dim_label[dim]}", f"{n_vector[0]}", f"{n_vector[-1]}", "", ] output += ["", ""] return "\n".join(output) level_count = self._count_cells_per_index() non_zero_levels = np.nonzero(level_count)[0] h_gridded = self.h_gridded mins = np.min(h_gridded, axis=0) maxs = np.max(h_gridded, axis=0) # Cell level table: cel_tbl = "\n" cel_tbl += "\n" cel_tbl += "Level\n" cel_tbl += "Number of cells\n" cel_tbl += "\n" for level in non_zero_levels: cel_tbl += "\n" cel_tbl += "{}\n".format(level) cel_tbl += "{}\n".format(level_count[level]) cel_tbl += "\n" cel_tbl += "\n" cel_tbl += ( " Total \n" ) cel_tbl += " {} \n".format(self.nC) cel_tbl += "\n" cel_tbl += "\n" det_tbl = "\n" det_tbl += "\n" det_tbl += "\n" det_tbl += "Mesh extent\n" det_tbl += "Cell widths\n" det_tbl += "\n" det_tbl += "\n" det_tbl += "\n" det_tbl += "min\n" det_tbl += "max\n" det_tbl += "min\n" det_tbl += "max\n" det_tbl += "\n" for dim in range(self.dim): n_vector = getattr(self, "nodes_" + dim_label[dim]) det_tbl += "\n" det_tbl += "{}\n".format(dim_label[dim]) det_tbl += "{}\n".format(n_vector[0]) det_tbl += "{}\n".format(n_vector[-1]) det_tbl += "{}\n".format(mins[dim]) det_tbl += "{}\n".format(maxs[dim]) det_tbl += "\n" det_tbl += "\n" full_tbl = "\n" full_tbl += "\n" full_tbl += "{}\n".format( mesh_name ) full_tbl += "{0:2.2f}% filled\n".format( 100 * self.fill ) full_tbl += "\n" full_tbl += "\n" full_tbl += "\n" full_tbl += cel_tbl full_tbl += "\n" full_tbl += "\n" full_tbl += det_tbl full_tbl += "\n" full_tbl += "\n" full_tbl += "\n" return full_tbl @BaseTensorMesh.origin.setter def origin(self, value): # NOQA D102 # first use the BaseTensorMesh to set the origin to handle "0, C, N" BaseTensorMesh.origin.fset(self, value) # then update the TreeMesh with the hidden value self._set_origin(self._origin) def refine_bounding_box( self, points, level=-1, padding_cells_by_level=None, finalize=True, diagonal_balance=None, ): """Refine within a bounding box based on the maximum and minimum extent of scattered points. This function refines the tree mesh based on the bounding box defined by the maximum and minimum extent of the scattered input `points`. It will refine the tree mesh for each level given. It also optionally pads the bounding box at each level based on the number of padding cells at each dimension. Parameters ---------- points : (N, dim) array_like The bounding box will be the maximum and minimum extent of these points level : int, optional The level of the treemesh to refine the bounding box to. Negative values index tree levels backwards, (e.g. `-1` is `max_level`). padding_cells_by_level : None, int, (n_level) array_like or (n_level, dim) array_like, optional The number of cells to pad the bounding box at each level of refinement. If a single number, each level below ``level`` will be padded with those number of cells. If array_like, `n_level`s below ``level`` will be padded, where if a 1D array, each dimension will be padded with the same number of cells, or 2D array supports variable padding along each dimension. None implies no padding. finalize : bool, optional Whether to finalize the mesh after the call. diagonal_balance : None or bool, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. See Also -------- refine_box Examples -------- Given a set of points, we want to refine the tree mesh with the bounding box that surrounds those points. The arbitrary points we use for this example are uniformly scattered between [3/8, 5/8] in the first and second dimension. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> mesh = discretize.TreeMesh([32, 32]) >>> rng = np.random.default_rng(852) >>> points = rng.random((20, 2)) * 0.25 + 3/8 Now we want to refine to the maximum level, with no padding the in `x` direction and `2` cells in `y`. At the second highest level we want 2 padding cells in each direction beyond that. >>> padding = [[0, 2], [2, 2]] >>> mesh.refine_bounding_box(points, -1, padding) For demonstration we, overlay the bounding box to show the effects of padding. >>> ax = mesh.plot_grid() >>> rect = patches.Rectangle([3/8, 3/8], 1/4, 1/4, facecolor='none', edgecolor='r', linewidth=3) >>> ax.add_patch(rect) >>> plt.show() """ bsw = np.min(np.atleast_2d(points), axis=0) tnw = np.max(np.atleast_2d(points), axis=0) if level < 0: level = (self.max_level + 1) - (abs(level) % (self.max_level + 1)) if level > self.max_level: raise IndexError(f"Level beyond max octree level, {self.max_level}") # pad based on the number of cells at each level if padding_cells_by_level is None: padding_cells_by_level = np.array([0]) padding_cells_by_level = np.asarray(padding_cells_by_level) if padding_cells_by_level.ndim == 0 and level > 1: padding_cells_by_level = np.full(level - 1, padding_cells_by_level) padding_cells_by_level = np.atleast_1d(padding_cells_by_level) if ( padding_cells_by_level.ndim == 2 and padding_cells_by_level.shape[1] != self.dim ): raise ValueError("incorrect dimension for padding_cells_by_level.") h_min = np.r_[[h.min() for h in self.h]] x0 = [] xF = [] ls = [] # The zip will short circuit on the shorter array. for lv, n_pad in zip(np.arange(level, 1, -1), padding_cells_by_level): padding_at_level = n_pad * h_min * 2 ** (self.max_level - lv) bsw = bsw - padding_at_level tnw = tnw + padding_at_level x0.append(bsw) xF.append(tnw) ls.append(lv) self.refine_box( x0, xF, ls, finalize=finalize, diagonal_balance=diagonal_balance ) def refine_points( self, points, level=-1, padding_cells_by_level=None, finalize=True, diagonal_balance=None, ): """Refine the mesh at given points to the prescribed level. This function refines the tree mesh around the `points`. It will refine the tree mesh for each level given. It also optionally radially pads around each point at each level with the number of padding cells given. Parameters ---------- points : (N, dim) array_like The points to be refined around. level : int, optional The level of the tree mesh to refine to. Negative values index tree levels backwards, (e.g. `-1` is `max_level`). padding_cells_by_level : None, int, (n_level) array_like, optional The number of cells to pad the bounding box at each level of refinement. If a single number, each level below ``level`` will be padded with those number of cells. If array_like, `n_level`s below ``level`` will be padded. None implies no padding. finalize : bool, optional Whether to finalize the mesh after the call. diagonal_balance : None or bool, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. See Also -------- refine_ball, insert_cells Examples -------- Given a set of points, we want to refine the tree mesh around these points to a prescribed level with a certain amount of padding. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> mesh = discretize.TreeMesh([32, 32]) Now we want to refine to the maximum level with 1 cell padding around the point, and then refine at the second highest level with at least 3 cells beyond that. >>> points = np.array([[0.1, 0.3], [0.6, 0.8]]) >>> padding = [1, 3] >>> mesh.refine_points(points, -1, padding) >>> ax = mesh.plot_grid() >>> ax.scatter(*points.T, color='C1') >>> plt.show() """ if level < 0: level = (self.max_level + 1) - (abs(level) % (self.max_level + 1)) if level > self.max_level: raise IndexError(f"Level beyond max octree level, {self.max_level}") # pad based on the number of cells at each level if padding_cells_by_level is None: padding_cells_by_level = np.array([0]) padding_cells_by_level = np.asarray(padding_cells_by_level) if padding_cells_by_level.ndim == 0 and level > 1: padding_cells_by_level = np.full(level - 1, padding_cells_by_level) padding_cells_by_level = np.atleast_1d(padding_cells_by_level) h_min = np.max([h.min() for h in self.h]) radius_at_level = 0.0 for lv, n_pad in zip(np.arange(level, 1, -1), padding_cells_by_level): radius_at_level += n_pad * h_min * 2 ** (self.max_level - lv) if radius_at_level == 0: self.insert_cells( points, lv, finalize=False, diagonal_balance=diagonal_balance ) else: self.refine_ball( points, radius_at_level, lv, finalize=False, diagonal_balance=diagonal_balance, ) if finalize: self.finalize() def refine_surface( self, xyz, level=-1, padding_cells_by_level=None, pad_up=False, pad_down=True, finalize=True, diagonal_balance=None, ): """Refine along a surface triangulated from xyz to the prescribed level. This function refines the mesh based on a triangulated surface from the input points. Every cell that intersects the surface will be refined to the given level. It also optionally pads the surface at each level based on the number of padding cells at each dimension. It does so by stretching the bounding box of the surface in each dimension. Parameters ---------- xyz : (N, dim) array_like or tuple The points defining the surface. Will be triangulated along the horizontal dimensions. You are able to supply your own triangulation in 3D by passing a tuple of (`xyz`, `triangle_indices`). level : int, optional The level of the tree mesh to refine to. Negative values index tree levels backwards, (e.g. `-1` is `max_level`). padding_cells_by_level : None, int, (n_level) array_like or (n_level, dim) array_like, optional The number of cells to pad the bounding box at each level of refinement. If a single number, each level below ``level`` will be padded with those number of cells. If array_like, `n_level`s below ``level`` will be padded, where if a 1D array, each dimension will be padded with the same number of cells, or 2D array supports variable padding along each dimension. None implies no padding. pad_up : bool, optional Whether to pad above the surface. pad_down : bool, optional Whether to pad below the surface. finalize : bool, optional Whether to finalize the mesh after the call. diagonal_balance : None or bool, optional Whether to balance cells diagonally in the refinement, `None` implies using the same setting used to instantiate the TreeMesh`. See Also -------- refine_triangle, refine_vertical_trianglular_prism Examples -------- In 2D we define the surface as a line segment, which we would like to refine along. >>> import discretize >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> mesh = discretize.TreeMesh([32, 32]) This surface is a simple sine curve, which we would like to pad with at least 2 cells vertically below at the maximum level, and 3 cells below that at the second highest level. >>> x = np.linspace(0.2, 0.8, 51) >>> z = 0.25*np.sin(2*np.pi*x)+0.5 >>> xz = np.c_[x, z] >>> mesh.refine_surface(xz, -1, [[0, 2], [0, 3]]) >>> ax = mesh.plot_grid() >>> ax.plot(x, z, color='C1') >>> plt.show() In 3D we define a grid of surface locations with there corresponding elevations. In this example we pad 2 cells at the finest level below the surface, and 3 cells down at the next level. >>> mesh = discretize.TreeMesh([32, 32, 32]) >>> x, y = np.mgrid[0.2:0.8:21j, 0.2:0.8:21j] >>> z = 0.125*np.sin(2*np.pi*x) + 0.5 + 0.125 * np.cos(2 * np.pi * y) >>> points = np.stack([x, y, z], axis=-1).reshape(-1, 3) >>> mesh.refine_surface(points, -1, [[0, 0, 2], [0, 0, 3]]) >>> v = mesh.cell_levels_by_index(np.arange(mesh.n_cells)) >>> fig, axs = plt.subplots(1, 3, figsize=(12,4)) >>> mesh.plot_slice(v, ax=axs[0], normal='x', grid=True, clim=[2, 5]) >>> mesh.plot_slice(v, ax=axs[1], normal='y', grid=True, clim=[2, 5]) >>> mesh.plot_slice(v, ax=axs[2], normal='z', grid=True, clim=[2, 5]) >>> plt.show() """ if level < 0: level = (self.max_level + 1) - (abs(level) % (self.max_level + 1)) if level > self.max_level: raise IndexError(f"Level beyond max octree level, {self.max_level}") # pad based on the number of cells at each level if padding_cells_by_level is None: padding_cells_by_level = np.array([0]) padding_cells_by_level = np.asarray(padding_cells_by_level) if padding_cells_by_level.ndim == 0 and level > 1: padding_cells_by_level = np.full(level - 1, padding_cells_by_level) padding_cells_by_level = np.atleast_1d(padding_cells_by_level) if ( padding_cells_by_level.ndim == 2 and padding_cells_by_level.shape[1] != self.dim ): raise ValueError("incorrect dimension for padding_cells_by_level.") if self.dim == 2: xyz = np.asarray(xyz) sorter = np.argsort(xyz[:, 0]) xyz = xyz[sorter] n_ps = len(xyz) inds = np.arange(n_ps) simps1 = np.c_[inds[:-1], inds[1:], inds[:-1]] + [0, 0, n_ps] simps2 = np.c_[inds[:-1], inds[1:], inds[1:]] + [n_ps, n_ps, 0] simps = np.r_[simps1, simps2] else: if isinstance(xyz, tuple): xyz, simps = xyz xyz = np.asarray(xyz) simps = np.asarray(simps) else: xyz = np.asarray(xyz) triang = Delaunay(xyz[:, :2]) simps = triang.simplices n_ps = len(xyz) # calculate bounding box for padding bb_min = np.min(xyz, axis=0)[:-1] bb_max = np.max(xyz, axis=0)[:-1] half_width = (bb_max - bb_min) / 2 center = (bb_max + bb_min) / 2 points = np.empty((n_ps, self.dim)) points[:, -1] = xyz[:, -1] h_min = np.r_[[h.min() for h in self.h]] pad = 0.0 for lv, n_pad in zip(np.arange(level, 1, -1), padding_cells_by_level): pad += n_pad * h_min * 2 ** (self.max_level - lv) h = 0 if pad_up: h += pad[-1] if pad_down: h += pad[-1] points[:, -1] = xyz[:, -1] - pad[-1] horizontal_expansion = (half_width + pad[:-1]) / half_width points[:, :-1] = horizontal_expansion * (xyz[:, :-1] - center) + center if self.dim == 2: triangles = np.r_[points, points + [0, h]][simps] self.refine_triangle( triangles, lv, finalize=False, diagonal_balance=diagonal_balance ) else: triangles = points[simps] self.refine_vertical_trianglular_prism( triangles, h, lv, finalize=False, diagonal_balance=diagonal_balance ) if finalize: self.finalize() @property def total_nodes(self): """Gridded hanging and non-hanging nodes locations. This property returns a numpy array of shape ``(n_total_nodes, dim)`` containing gridded locations for all hanging and non-hanging nodes in the mesh. Returns ------- (n_total_nodes, dim) numpy.ndarray of float Gridded hanging and non-hanging node locations """ self._error_if_not_finalized("total_nodes") return np.vstack((self.nodes, self.hanging_nodes)) @property def vntF(self): """Vector number of total faces along each axis. This property returns the total number of hanging and non-hanging faces along each axis direction. The returned quantity is a list of integers of the form [nFx,nFy,nFz]. Returns ------- list of int Vector number of total faces along each axis """ return [self.ntFx, self.ntFy] + ([] if self.dim == 2 else [self.ntFz]) @property def vntE(self): """Vector number of total edges along each axis. This property returns the total number of hanging and non-hanging edges along each axis direction. The returned quantity is a list of integers of the form [nEx,nEy,nEz]. Returns ------- list of int Vector number of total edges along each axis """ return [self.ntEx, self.ntEy] + ([] if self.dim == 2 else [self.ntEz]) @property def stencil_cell_gradient(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_stencil_cell_gradient", None) is None: self._stencil_cell_gradient = sp.vstack( [self.stencil_cell_gradient_x, self.stencil_cell_gradient_y] ) if self.dim == 3: self._stencil_cell_gradient = sp.vstack( [self._stencil_cell_gradient, self.stencil_cell_gradient_z] ) return self._stencil_cell_gradient @property def cell_gradient(self): # NOQA D102 # Documentation inherited from discretize.operators.DifferentialOperators if getattr(self, "_cell_gradient", None) is None: i_s = self.face_boundary_indices ix = np.ones(self.nFx) ix[i_s[0]] = 0.0 ix[i_s[1]] = 0.0 Pafx = sp.diags(ix) iy = np.ones(self.nFy) iy[i_s[2]] = 0.0 iy[i_s[3]] = 0.0 Pafy = sp.diags(iy) MfI = self.get_face_inner_product(invert_matrix=True) if self.dim == 2: Pi = sp.block_diag([Pafx, Pafy]) elif self.dim == 3: iz = np.ones(self.nFz) iz[i_s[4]] = 0.0 iz[i_s[5]] = 0.0 Pafz = sp.diags(iz) Pi = sp.block_diag([Pafx, Pafy, Pafz]) self._cell_gradient = ( -Pi * MfI * self.face_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient @property def cell_gradient_x(self): # NOQA D102 # Documentation inherited from discretize.operators.DifferentialOperators if getattr(self, "_cell_gradient_x", None) is None: nFx = self.nFx i_s = self.face_boundary_indices ix = np.ones(self.nFx) ix[i_s[0]] = 0.0 ix[i_s[1]] = 0.0 Pafx = sp.diags(ix) MfI = self.get_face_inner_product(invert_matrix=True) MfIx = sp.diags(MfI.diagonal()[:nFx]) self._cell_gradient_x = ( -Pafx * MfIx * self.face_x_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient_x @property def cell_gradient_y(self): # NOQA D102 # Documentation inherited from discretize.operators.DifferentialOperators if getattr(self, "_cell_gradient_y", None) is None: nFx = self.nFx nFy = self.nFy i_s = self.face_boundary_indices iy = np.ones(self.nFy) iy[i_s[2]] = 0.0 iy[i_s[3]] = 0.0 Pafy = sp.diags(iy) MfI = self.get_face_inner_product(invert_matrix=True) MfIy = sp.diags(MfI.diagonal()[nFx : nFx + nFy]) self._cell_gradient_y = ( -Pafy * MfIy * self.face_y_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient_y @property def cell_gradient_z(self): # NOQA D102 # Documentation inherited from discretize.operators.DifferentialOperators if self.dim == 2: raise TypeError("z derivative not defined in 2D") if getattr(self, "_cell_gradient_z", None) is None: nFx = self.nFx nFy = self.nFy i_s = self.face_boundary_indices iz = np.ones(self.nFz) iz[i_s[4]] = 0.0 iz[i_s[5]] = 0.0 Pafz = sp.diags(iz) MfI = self.get_face_inner_product(invert_matrix=True) MfIz = sp.diags(MfI.diagonal()[nFx + nFy :]) self._cell_gradient_z = ( -Pafz * MfIz * self.face_z_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient_z @property def face_x_divergence(self): # NOQA D102 # Documentation inherited from discretize.operators.DifferentialOperators if getattr(self, "_face_x_divergence", None) is None: self._face_x_divergence = self.face_divergence[:, : self.nFx] return self._face_x_divergence @property def face_y_divergence(self): # NOQA D102 # Documentation inherited from discretize.operators.DifferentialOperators if getattr(self, "_face_y_divergence", None) is None: self._face_y_divergence = self.face_divergence[ :, self.nFx : self.nFx + self.nFy ] return self._face_y_divergence @property def face_z_divergence(self): # NOQA D102 # Documentation inherited from discretize.operators.DifferentialOperators if getattr(self, "_face_z_divergence", None) is None: self._face_z_divergence = self.face_divergence[:, self.nFx + self.nFy :] return self._face_z_divergence def point2index(self, locs): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.get_containing_cells(locs) def cell_levels_by_index(self, indices): """Fast function to return a list of levels for the given cell indices. Parameters ---------- indices: (N) array_like Cell indexes to query Returns ------- (N) numpy.ndarray of int Levels for the cells. """ return self._cell_levels_by_indexes(indices) def get_interpolation_matrix( # NOQA D102 self, locs, location_type="cell_centers", zeros_outside=False, **kwargs ): # Documentation inherited from discretize.base.BaseMesh if "locType" in kwargs: raise TypeError( "The locType keyword argument has been removed, please use location_type. " "This will be removed in discretize 1.0.0" ) if "zerosOutside" in kwargs: raise TypeError( "The zerosOutside keyword argument has been removed, please use zeros_outside. " "This will be removed in discretize 1.0.0" ) location_type = self._parse_location_type(location_type) if self.dim == 2 and "z" in location_type: raise NotImplementedError("Unable to interpolate from Z edges/faces in 2D") locs = self._require_ndarray_with_dim("locs", locs, ndim=2, dtype=np.float64) if location_type == "nodes": Av = self._getNodeIntMat(locs, zeros_outside) elif location_type in ["edges_x", "edges_y", "edges_z"]: Av = self._getEdgeIntMat(locs, zeros_outside, location_type[-1]) elif location_type in ["faces_x", "faces_y", "faces_z"]: Av = self._getFaceIntMat(locs, zeros_outside, location_type[-1]) elif location_type in ["cell_centers"]: Av = self._getCellIntMat(locs, zeros_outside) else: raise ValueError( "Location must be a grid location, not {}".format(location_type) ) return Av @property def permute_cells(self): """Permutation matrix re-ordering of cells sorted by x, then y, then z. Returns ------- (n_cells, n_cells) scipy.sparse.csr_matrix """ # TODO: cache these? P = np.lexsort(self.gridCC.T) # sort by x, then y, then z return sp.identity(self.nC).tocsr()[P] @property def permute_faces(self): """Permutation matrix re-ordering of faces sorted by x, then y, then z. Returns ------- (n_faces, n_faces) scipy.sparse.csr_matrix """ # TODO: cache these? Px = np.lexsort(self.gridFx.T) Py = np.lexsort(self.gridFy.T) + self.nFx if self.dim == 2: P = np.r_[Px, Py] else: Pz = np.lexsort(self.gridFz.T) + (self.nFx + self.nFy) P = np.r_[Px, Py, Pz] return sp.identity(self.nF).tocsr()[P] @property def permute_edges(self): """Permutation matrix re-ordering of edges sorted by x, then y, then z. Returns ------- (n_edges, n_edges) scipy.sparse.csr_matrix """ # TODO: cache these? Px = np.lexsort(self.gridEx.T) Py = np.lexsort(self.gridEy.T) + self.nEx if self.dim == 2: P = np.r_[Px, Py] if self.dim == 3: Pz = np.lexsort(self.gridEz.T) + (self.nEx + self.nEy) P = np.r_[Px, Py, Pz] return sp.identity(self.nE).tocsr()[P] @property def cell_state(self): """The current state of the cells on the mesh. This represents the x, y, z indices of the cells in the base tensor mesh, as well as their levels. It can be used to reconstruct the mesh. Returns ------- dict dictionary with two entries: - ``"indexes"``: the indexes of the cells - ``"levels"``: the levels of the cells """ indexes, levels = self.__getstate__() return {"indexes": indexes.tolist(), "levels": levels.tolist()} def validate(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.finalized def equals(self, other): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh try: if self.finalized and other.finalized: return super().equals(other) except AttributeError: pass return False def __reduce__(self): """Return the necessary items to reconstruct this object's state.""" return TreeMesh, (self.h, self.origin, False), self.__getstate__() cellGrad = deprecate_property( "cell_gradient", "cellGrad", removal_version="1.0.0", error=True ) cellGradx = deprecate_property( "cell_gradient_x", "cellGradx", removal_version="1.0.0", error=True ) cellGrady = deprecate_property( "cell_gradient_y", "cellGrady", removal_version="1.0.0", error=True ) cellGradz = deprecate_property( "cell_gradient_z", "cellGradz", removal_version="1.0.0", error=True ) cellGradStencil = deprecate_property( "cell_gradient_stencil", "cellGradStencil", removal_version="1.0.0", error=True, ) faceDivx = deprecate_property( "face_x_divergence", "faceDivx", removal_version="1.0.0", error=True ) faceDivy = deprecate_property( "face_y_divergence", "faceDivy", removal_version="1.0.0", error=True ) faceDivz = deprecate_property( "face_z_divergence", "faceDivz", removal_version="1.0.0", error=True ) maxLevel = deprecate_property( "max_used_level", "maxLevel", removal_version="1.0.0", error=True ) areaFx = deprecate_property( "face_x_areas", "areaFx", removal_version="1.0.0", error=True ) areaFy = deprecate_property( "face_y_areas", "areaFy", removal_version="1.0.0", error=True ) areaFz = deprecate_property( "face_z_areas", "areaFz", removal_version="1.0.0", error=True ) edgeEx = deprecate_property( "edge_x_lengths", "edgeEx", removal_version="1.0.0", error=True ) edgeEy = deprecate_property( "edge_y_lengths", "edgeEy", removal_version="1.0.0", error=True ) edgeEz = deprecate_property( "edge_z_lengths", "edgeEz", removal_version="1.0.0", error=True ) permuteCC = deprecate_property( "permute_cells", "permuteCC", removal_version="1.0.0", error=True ) permuteF = deprecate_property( "permute_faces", "permuteF", removal_version="1.0.0", error=True ) permuteE = deprecate_property( "permute_edges", "permuteE", removal_version="1.0.0", error=True ) faceBoundaryInd = deprecate_property( "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0", error=True, ) cellBoundaryInd = deprecate_property( "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0", error=True, ) _aveCC2FxStencil = deprecate_property( "average_cell_to_total_face_x", "_aveCC2FxStencil", removal_version="1.0.0", error=True, ) _aveCC2FyStencil = deprecate_property( "average_cell_to_total_face_y", "_aveCC2FyStencil", removal_version="1.0.0", error=True, ) _aveCC2FzStencil = deprecate_property( "average_cell_to_total_face_z", "_aveCC2FzStencil", removal_version="1.0.0", error=True, ) _cellGradStencil = deprecate_property( "stencil_cell_gradient", "_cellGradStencil", removal_version="1.0.0", error=True, ) _cellGradxStencil = deprecate_property( "stencil_cell_gradient_x", "_cellGradxStencil", removal_version="1.0.0", error=True, ) _cellGradyStencil = deprecate_property( "stencil_cell_gradient_y", "_cellGradyStencil", removal_version="1.0.0", error=True, ) _cellGradzStencil = deprecate_property( "stencil_cell_gradient_z", "_cellGradzStencil", removal_version="1.0.0", error=True, ) ================================================ FILE: discretize/unstructured_mesh.py ================================================ """Module containing unstructured meshes for discretize.""" import numpy as np import scipy.sparse as sp from scipy.spatial import KDTree from discretize.utils import Identity, invert_blocks, spzeros, cross2d from discretize.base import BaseMesh from discretize._extensions.simplex_helpers import ( _build_faces_edges, _build_adjacency, _directed_search, _interp_cc, ) from discretize.mixins import InterfaceMixins, SimplexMeshIO class SimplexMesh(BaseMesh, SimplexMeshIO, InterfaceMixins): """Class for traingular (2D) and tetrahedral (3D) meshes. Simplex is the abstract term for triangular like elements in an arbitrary dimension. Simplex meshes are subdivided into trianglular (in 2D) or tetrahedral (in 3D) elements. They are capable of representing abstract geometric surfaces, with widely variable element sizes. Parameters ---------- nodes : (n_nodes, dim) array_like of float Defines every node of the mesh. simplices : (n_cells, dim+1) array_like of int This array defines the connectivity of nodes to form cells. Each element indexes into the `nodes` array. Each row defines which nodes make a given cell. This array is sorted along each row and then stored on the mesh. Notes ----- Only rudimentary checking of the input nodes and simplices is performed, only checking for degenerate simplices who have zero volume. There are no checks for overlapping cells, or for the quality of the mesh. Examples -------- Here we generate a basic 2D triangular mesh, by triangulating a rectangular domain. >>> from discretize import SimplexMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib.tri as tri First we define the nodes of our mesh >>> X, Y = np.mgrid[-20:20:21j, -10:10:11j] >>> nodes = np.c_[X.reshape(-1), Y.reshape(-1)] Then we triangulate the nodes, here we use matplotlib, but you could also use scipy's Delaunay, or any other triangular mesh generator. Essentailly we are creating every node in the mesh, and a list of triangles/tetrahedrals defining which nodes make of each cell. >>> triang = tri.Triangulation(nodes[:, 0], nodes[:, 1]) >>> simplices = triang.triangles Finally we can assemble them into a SimplexMesh >>> mesh = SimplexMesh(nodes, simplices) >>> mesh.plot_grid() >>> plt.show() """ _meshType = "simplex" _items = {"nodes", "simplices"} def __init__(self, nodes, simplices): # grab copies of the nodes and simplices for protection nodes = np.asarray(nodes) simplices = np.asarray(simplices) dim = nodes.shape[1] if dim not in [3, 2]: raise ValueError( f"Mesh must be either 2 or 3 dimensions, nodes has {dim} dimension." ) if simplices.shape[1] != dim + 1: raise ValueError( "simplices second dimension is not compatible with the mesh dimension. " f"Saw {simplices.shape[1]}, and expected {dim + 1}." ) self._nodes = nodes.copy() self._nodes.setflags(write="false") self._simplices = simplices.copy() # sort the simplices by node index to simplify further functions... self._simplices.sort(axis=1) self._simplices.setflags(write="false") if self.cell_volumes.min() == 0.0: raise ValueError("Triangulation contains degenerate simplices") self._number() def _number(self): items = _build_faces_edges(self.simplices) self._simplex_faces = np.array(items[0]) self._faces = np.array(items[1]) self._simplex_edges = np.array(items[2]) self._edges = np.array(items[3]) self._n_faces = self._faces.shape[0] self._n_edges = self._edges.shape[0] if self.dim == 3: self._face_edges = np.array(items[4]) @property def simplices(self): """The node indices for all simplexes of the mesh. This array defines the connectivity of the mesh. For each simplex this array is sorted by the node index. Returns ------- (n_cells, dim + 1) numpy.ndarray of int """ return self._simplices @property def neighbors(self): """ The adjacancy graph of the mesh. This array contains the adjacent cell index for each cell of the mesh. For each cell the i'th neighbor is opposite the i'th node, across the i'th face. If a cell has no neighbor in a particular direction, then it is listed as having -1. This also implies that this is a boundary face. Returns ------- (n_cells, dim + 1) numpy.ndarray of int """ if getattr(self, "_neighbors", None) is None: self._neighbors = np.array( _build_adjacency(self._simplex_faces, self.n_faces) ) return self._neighbors @property def transform_and_shift(self): """ The barycentric transformation matrix and shift. Returns ------- transform : (n_cells, dim, dim) numpy.ndarray shift : (n_cells) numpy.ndarray """ if getattr(self, "_transform", None) is None: # compute the barycentric transforms points = self.nodes simplices = self.simplices shift = points[self.simplices[:, -1]] T = (points[simplices[:, :-1]] - shift[:, None, :]).transpose((0, 2, 1)) self._transform = invert_blocks(T) self._shift = shift return self._transform, self._shift @property def dim(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.nodes.shape[-1] @property def n_nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self._nodes.shape[0] @property def nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self._nodes @property def n_cells(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.simplices.shape[0] @property def cell_centers(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_cell_centers", None) is None: self._cell_centers = np.mean(self.nodes[self.simplices], axis=1) return self._cell_centers @property def cell_volumes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_cell_volumes", None) is None: simplex_nodes = self._nodes[self.simplices] mats = np.pad(simplex_nodes, ((0, 0), (0, 0), (0, 1)), constant_values=1) V1 = np.abs(np.linalg.det(mats)) V1 /= 6 if self.dim == 3 else 2 self._cell_volumes = V1 return self._cell_volumes @property def n_edges(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self._n_edges @property def edges(self): # NOQA D102 if getattr(self, "_edge_locs", None) is None: self._edge_locs = np.mean(self.nodes[self._edges], axis=1) # Documentation inherited from discretize.base.BaseMesh return self._edge_locs @property def edge_tangents(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_edge_tangents", None) is None: tangents = np.diff(self.nodes[self._edges], axis=1).squeeze() tangents /= np.linalg.norm(tangents, axis=-1)[:, None] self._edge_tangents = tangents return self._edge_tangents @property def edge_lengths(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_edge_lengths", None) is None: self._edge_lengths = np.linalg.norm( np.diff(self.nodes[self._edges], axis=1).squeeze(), axis=-1 ) return self._edge_lengths @property def n_faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self._n_faces @property def faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_face_locs", None) is None: self._face_locs = np.mean(self.nodes[self._faces], axis=1) return self._face_locs @property def face_areas(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: return self.edge_lengths else: if getattr(self, "_face_areas", None) is None: face_nodes = self._nodes[self._faces] v01 = face_nodes[:, 1] - face_nodes[:, 0] v02 = face_nodes[:, 2] - face_nodes[:, 0] self._face_areas = np.linalg.norm(np.cross(v01, v02), axis=1) / 2 return self._face_areas @property def face_normals(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_face_normals", None) is None: if self.dim == 2: # Take the normal as being the cross product of edge_tangents # and a unit vector in a "3rd" dimension. normal = np.c_[self.edge_tangents[:, 1], -self.edge_tangents[:, 0]] else: # define normal as |01 x 02| # therefore clockwise path about the normal is 0->1->2->0 face_nodes = self._nodes[self._faces] v01 = face_nodes[:, 1] - face_nodes[:, 0] v02 = face_nodes[:, 2] - face_nodes[:, 0] normal = np.cross(v01, v02) normal /= np.linalg.norm(normal, axis=1)[:, None] self._face_normals = normal return self._face_normals @property def face_divergence(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_face_divergence", None) is None: areas = self.face_areas normals = self.face_normals # approx outward normals (to figure out if face normal is opposite the outward direction) test = self.faces[self._simplex_faces] - self.cell_centers[:, None, :] dirs = np.einsum("ijk,ijk->ij", normals[self._simplex_faces], test) Aijs = areas[self._simplex_faces] / self.cell_volumes[:, None] Aijs[dirs < 0] *= -1 Aijs = Aijs.reshape(-1) ind_ptr = (self.dim + 1) * np.arange(self.n_cells + 1) col_inds = self._simplex_faces.reshape(-1) self._face_divergence = sp.csr_matrix( (Aijs, col_inds, ind_ptr), shape=(self.n_cells, self.n_faces) ) return self._face_divergence @property def nodal_gradient(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_nodal_gradient", None) is None: ind_ptr = 2 * np.arange(self.n_edges + 1) col_inds = self._edges.reshape(-1) Aijs = ((1.0 / self.edge_lengths[:, None]) * [-1, 1]).reshape(-1) self._nodal_gradient = sp.csr_matrix( (Aijs, col_inds, ind_ptr), shape=(self.n_edges, self.n_nodes) ) return self._nodal_gradient @property def edge_curl(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_edge_curl", None) is None: dim = self.dim n_edges = self.n_edges if dim == 2: face_edges = self._simplex_edges face_nodes = self.nodes[self.simplices] face_areas = self.cell_volumes n_faces = self.n_cells else: face_edges = self._face_edges face_nodes = self.nodes[self._faces] face_areas = self.face_areas n_faces = self.n_faces ind_ptr = 3 * np.arange(n_faces + 1) col_inds = face_edges.reshape(-1) # edge tangents point from lower node to higher node # clockwise about the face normal on face is path from 0 -> 1 -> 2 -> 0 # so for each face can take the dot product of the path with the edge tangent l01 = face_nodes[:, 1] - face_nodes[:, 0] # path opposite node 2 l12 = face_nodes[:, 2] - face_nodes[:, 1] # path opposite node 0 l20 = face_nodes[:, 0] - face_nodes[:, 2] # path opposite node 1 face_path_tangents = self.edge_tangents[face_edges] if dim == 2: # need to do an adjustment in 2D for the places where the simplices # are not oriented counter clockwise about the +z axis # cp = np.cross(l01, -l20) # cp is a bunch of 1s (where simplices are CCW) and -1s (where simplices are CW) # (but we take the sign here to guard against numerical precision) cp = np.sign(cross2d(l20, l01)) face_areas = face_areas * cp # don't due *= here Aijs = ( np.c_[ np.einsum("ij,ij->i", face_path_tangents[:, 0], l12), np.einsum("ij,ij->i", face_path_tangents[:, 1], l20), np.einsum("ij,ij->i", face_path_tangents[:, 2], l01), ] / face_areas[:, None] ).reshape(-1) self._edge_curl = sp.csr_matrix( (Aijs, col_inds, ind_ptr), shape=(n_faces, n_edges) ) return self._edge_curl def __validate_model(self, model, invert_model=False): n_cells = self.n_cells dim = self.dim # determines the tensor type of the model and reshapes it properly model = np.atleast_1d(model) n_aniso = ((dim + 1) * dim) // 2 if model.size == n_aniso * n_cells: # model is fully anisotropic # reshape it into a stack of dim x dim matrices if model.ndim == 1: model = model.reshape((-1, n_aniso), order="F") vals = model if self.dim == 2: model = np.stack( [[vals[:, 0], vals[:, 2]], [vals[:, 2], vals[:, 1]]] ).transpose((2, 0, 1)) else: model = np.stack( [ [vals[:, 0], vals[:, 3], vals[:, 4]], [vals[:, 3], vals[:, 1], vals[:, 5]], [vals[:, 4], vals[:, 5], vals[:, 2]], ] ).transpose((2, 0, 1)) elif model.size == dim * n_cells: if model.ndim == 1: model = model.reshape((n_cells, dim), order="F") model = model.reshape(-1) if invert_model: if model.ndim == 1: model = 1.0 / model else: model = invert_blocks(model) return model def __get_inner_product_projection_matrices( self, i_type, with_volume=True, return_pointers=True ): if getattr(self, "_proj_stash", None) is None: self._proj_stash = {} key = (i_type, with_volume) if key not in self._proj_stash: dim = self.dim n_cells = self.n_cells if i_type == "F": vecs = self.face_normals n_items = self.n_faces simplex_items = self._simplex_faces if dim == 2: node_items = np.array([[1, 2], [0, 2], [0, 1]]) else: node_items = np.array([[1, 2, 3], [0, 2, 3], [0, 1, 3], [0, 1, 2]]) elif i_type == "E": vecs = self.edge_tangents n_items = self.n_edges simplex_items = self._simplex_edges if dim == 2: node_items = np.array([[1, 2], [0, 2], [0, 1]]) elif dim == 3: node_items = np.array([[1, 2, 3], [0, 2, 4], [0, 1, 5], [3, 4, 5]]) Ps = [] # Precalc indptr and values for the projection matrix P_indptr = np.arange(dim * n_cells + 1) ones = np.ones(dim * n_cells) if with_volume: V = np.sqrt(self.cell_volumes / (dim + 1)) # precalculate indices for the block diagonal matrix d = np.ones(dim, dtype=int)[:, None] * np.arange(dim) t = np.arange(n_cells) T_col_inds = (d + t[:, None, None] * dim).reshape(-1) T_ind_ptr = dim * np.arange(dim * n_cells + 1) for i in range(dim + 1): # array which selects the items associated with node i of each simplex... item_inds = np.take(simplex_items, node_items[i], axis=1) P_col_inds = item_inds.reshape(-1) P = sp.csr_matrix( (ones, P_col_inds, P_indptr), shape=(dim * n_cells, n_items) ) item_vectors = vecs[item_inds] trans_inv = invert_blocks(item_vectors) if with_volume: trans_inv *= V[:, None, None] T = sp.csr_matrix( (trans_inv.reshape(-1), T_col_inds, T_ind_ptr), shape=(dim * n_cells, dim * n_cells), ) Ps.append(T @ P) self._proj_stash[key] = (Ps, T_col_inds, T_ind_ptr) Ps, T_col_inds, T_ind_ptr = self._proj_stash[key] if return_pointers: return Ps, (T_col_inds, T_ind_ptr) else: return Ps def __get_inner_product(self, i_type, model, invert_model): Ps, (T_col_inds, T_ind_ptr) = self.__get_inner_product_projection_matrices( i_type ) n_cells = self.n_cells dim = self.dim if model is None: Mu = Identity() else: model = self.__validate_model(model, invert_model) if model.size == 1: Mu = sp.diags((model,), (0,), shape=(dim * n_cells, dim * n_cells)) elif model.size == n_cells: Mu = sp.diags(np.repeat(model, dim)) elif model.size == dim * n_cells: # diagonally anisotropic model Mu = sp.diags(model) elif model.size == (dim * dim) * n_cells: Mu = sp.csr_matrix( (model.reshape(-1), T_col_inds, T_ind_ptr), shape=(dim * n_cells, dim * n_cells), ) else: raise ValueError("Unrecognized size of model vector") A = np.sum([P.T @ Mu @ P for P in Ps]) return A def get_face_inner_product( # NOQA D102 self, model=None, invert_model=False, invert_matrix=False, do_fast=True, ): # Documentation inherited from discretize.base.BaseMesh if invert_matrix: raise NotImplementedError( "The inverse of the inner product matrix with a tetrahedral mesh is not supported." ) return self.__get_inner_product("F", model, invert_model) def get_edge_inner_product( # NOQA D102 self, model=None, invert_model=False, invert_matrix=False, do_fast=True, ): # Documentation inherited from discretize.base.BaseMesh if invert_matrix: raise NotImplementedError( "The inverse of the inner product matrix with a tetrahedral mesh is not supported." ) return self.__get_inner_product("E", model, invert_model) def __get_inner_product_deriv_func(self, i_type, model): Ps, _ = self.__get_inner_product_projection_matrices(i_type) dim = self.dim n_cells = self.n_cells if model.size == 1: tensor_type = 0 elif model.size == n_cells: tensor_type = 1 col_inds = np.repeat(np.arange(n_cells), dim) ind_ptr = np.arange(n_cells * dim + 1) elif model.size == dim * n_cells: tensor_type = 2 col_inds = np.arange(dim * n_cells).reshape((n_cells, dim), order="F") col_inds = col_inds.reshape(-1) ind_ptr = np.arange(n_cells * dim + 1) elif model.size == (((dim + 1) * dim) // 2) * n_cells: tensor_type = 3 # create a stencil that goes from the model vector ordering # into the anisotropy tensor if dim == 2: stencil = np.array([[0, 2], [2, 1]]) elif dim == 3: stencil = np.array([[0, 3, 4], [3, 1, 5], [4, 5, 2]]) col_inds = n_cells * stencil + np.arange(n_cells)[:, None, None] col_inds = col_inds.reshape(-1) ind_ptr = dim * np.arange(n_cells * dim + 1) else: raise ValueError("Unrecognized size of model vector") inv_items = model.size if i_type == "F": n_items = self.n_faces elif i_type == "E": n_items = self.n_edges def func(v): dMdm = spzeros(n_items, inv_items) if tensor_type == 0: for P in Ps: dMdm = dMdm + sp.csr_matrix( (P.T * (P * v), (range(n_items), np.zeros(n_items))), shape=(n_items, inv_items), ) elif tensor_type == 1: for P in Ps: ys = P @ v dMdm = dMdm + P.T @ sp.csr_matrix( (ys, col_inds, ind_ptr), shape=(n_cells * dim, inv_items) ) elif tensor_type == 2: for P in Ps: ys = P @ v dMdm = dMdm + P.T @ sp.csr_matrix( (ys, col_inds, ind_ptr), shape=(n_cells * dim, inv_items) ) elif tensor_type == 3: for P in Ps: ys = P @ v ys = np.repeat(ys, dim).reshape((-1, dim, dim)) ys = ys.transpose((0, 2, 1)).reshape(-1) dMdm = dMdm + P.T @ sp.csr_matrix( (ys, col_inds, ind_ptr), shape=(n_cells * dim, inv_items) ) return dMdm return func def get_face_inner_product_deriv( # NOQA D102 self, model, do_fast=True, invert_model=False, invert_matrix=False ): # Documentation inherited from discretize.base.BaseMesh if invert_model: raise NotImplementedError( "Inverted model derivatives are not supported here" ) if invert_matrix: raise NotImplementedError("Inverted matrix derivatives are not supported") return self.__get_inner_product_deriv_func("F", model) def get_edge_inner_product_deriv( # NOQA D102 self, model, do_fast=True, invert_model=False, invert_matrix=False ): # Documentation inherited from discretize.base.BaseMesh if invert_model: raise NotImplementedError( "Inverted model derivatives are not supported here" ) if invert_matrix: raise NotImplementedError("Inverted matrix derivatives are not supported") return self.__get_inner_product_deriv_func("E", model) def _get_edge_surf_int_proj_mats(self, only_boundary=False, with_area=True): """Return the projection operators for integrating edges on each face. Parameters ---------- only_boundary : bool, optional Whether to only operate on the boundary faces or not. with_area : bool, optional Whether to include the face area. Returns ------- list of (3 * n_faces, n_edges) scipy.sparse.csr_matrix """ # edges associated with each face... can just get the indices of the curl... face_edges = self._face_edges face_areas = self.face_areas if only_boundary: bf_inds = self.boundary_face_list face_edges = face_edges[bf_inds] face_areas = face_areas[bf_inds] face_normals = self.boundary_face_outward_normals else: face_normals = self.face_normals # face_edges for these are: edge_inds = [[1, 2], [0, 2], [0, 1]] n_f = face_edges.shape[0] ones = np.ones(n_f * 2) P_indptr = np.arange(2 * n_f + 1) d = np.ones(3, dtype=int)[:, None] * np.arange(2) t = np.arange(n_f) T_col_inds = (d + t[:, None, None] * 2).reshape(-1) T_ind_ptr = 2 * np.arange(3 * n_f + 1) Ps = [] # translate c to fortran ordering C2F_col_inds = np.arange(n_f * 3).reshape((-1, 3), order="C").reshape(-1) C2F_row_inds = np.arange(n_f * 3).reshape((-1, 3), order="F").reshape(-1) C2F = sp.csr_matrix( (np.ones(n_f * 3), (C2F_row_inds, C2F_col_inds)), shape=(n_f * 3, n_f * 3) ) for i in range(3): # matrix which selects the edges associate with each of the nodes of each boundary face node_edges = face_edges[:, edge_inds[i]] P = sp.csr_matrix( (ones, node_edges.reshape(-1), P_indptr), shape=(2 * n_f, self.n_edges) ) edge_dirs = self.edge_tangents[node_edges] t_for = np.concatenate((edge_dirs, face_normals[:, None, :]), axis=1) t_inv = invert_blocks(t_for) t_inv = t_inv[:, :, :-1] / 3 # n_edges_per_thing if with_area: t_inv *= face_areas[:, None, None] T = C2F @ sp.csr_matrix( (t_inv.reshape(-1), T_col_inds, T_ind_ptr), shape=(3 * n_f, 2 * n_f), ) Ps.append((T @ P)) return Ps @property def cell_centers_tree(self): """A KDTree object built from the cell centers. Returns ------- scipy.spatial.KDTree """ if getattr(self, "_cc_tree", None) is None: self._cc_tree = KDTree(self.cell_centers) return self._cc_tree def point2index(self, locs): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh tree = self.cell_centers_tree # for each location, find the nearest cell center as an initial guess for # the nearest simplex, then use a directed search to further refine _, nearest_cc = tree.query(locs) nodes = self.nodes simplex_nodes = self.simplices transform, shift = self.transform_and_shift return _directed_search( np.atleast_2d(locs), np.atleast_1d(nearest_cc), nodes, simplex_nodes, self.neighbors, transform, shift, return_bary=False, ) def get_interpolation_matrix( # NOQA D102 self, loc, location_type="cell_centers", zeros_outside=False, **kwargs ): # Documentation inherited from discretize.base.BaseMesh location_type = self._parse_location_type(location_type) tree = self.cell_centers_tree # for each location, find the nearest cell center as an initial guess for # the nearest simplex, then use a directed search to further refine loc = np.atleast_2d(loc) _, nearest_cc = tree.query(loc) nodes = self.nodes simplex_nodes = self.simplices transform, shift = self.transform_and_shift inds, barys = _directed_search( loc, nearest_cc, nodes, simplex_nodes, self.neighbors, transform, shift, zeros_outside=zeros_outside, return_bary=True, ) if zeros_outside: barys[inds == -1] = 0.0 n_loc = len(loc) location_type = self._parse_location_type(location_type) if location_type == "nodes": nodes_per_cell = self.dim + 1 ind_ptr = nodes_per_cell * np.arange(n_loc + 1) col_inds = simplex_nodes[inds].reshape(-1) Aij = barys.reshape(-1) n_items = self.n_nodes elif location_type == "cell_centers": # detemine which node each point is closest to. which_node = simplex_nodes[inds, np.argmax(barys, axis=-1)] # this matrix can be used to lookup which cells a given node touch, # which will also be the cells used to interpolate from. mat = self.average_node_to_cell.T[which_node].tocsr() # this will overwrite the "mat" matrices data to create the interpolation _interp_cc(loc, self.cell_centers, mat.data, mat.indices, mat.indptr) if zeros_outside: e = np.ones(n_loc) e[inds == -1] = 0.0 mat = sp.diags(e, format="csr") @ mat return mat else: component = location_type[-1] if component == "x": i_dir = 0 elif component == "y": i_dir = 1 else: i_dir = -1 if location_type[:-2] == "edges": # grab the barycentric transforms associated with each simplex: ts = transform[inds, :, i_dir] ts = np.hstack((ts, -ts.sum(axis=1)[:, None])) # edges_x, edges_y, edges_z # grab edge lengths lengths = self.edge_lengths # use 1-form Whitney basis functions for (edges) edges = self._simplex_edges[inds] # (1, 2), (0, 2), (0, 1) e0 = (barys[:, 1] * ts[:, 2] - barys[:, 2] * ts[:, 1]) * lengths[ edges[:, 0] ] e1 = (barys[:, 0] * ts[:, 2] - barys[:, 2] * ts[:, 0]) * lengths[ edges[:, 1] ] e2 = (barys[:, 0] * ts[:, 1] - barys[:, 1] * ts[:, 0]) * lengths[ edges[:, 2] ] if self.dim == 3: # (0, 3), (1, 3), (2, 3) e3 = (barys[:, 0] * ts[:, 3] - barys[:, 3] * ts[:, 0]) * lengths[ edges[:, 3] ] e4 = (barys[:, 1] * ts[:, 3] - barys[:, 3] * ts[:, 1]) * lengths[ edges[:, 4] ] e5 = (barys[:, 2] * ts[:, 3] - barys[:, 3] * ts[:, 2]) * lengths[ edges[:, 5] ] Aij = np.c_[e0, e1, e2, e3, e4, e5].reshape(-1) ind_ptr = 6 * np.arange(n_loc + 1) else: Aij = np.c_[e0, e1, e2].reshape(-1) ind_ptr = 3 * np.arange(n_loc + 1) col_inds = edges.reshape(-1) n_items = self.n_edges elif location_type[:-2] == "faces": # grab the barycentric transforms associated with each simplex: ts = transform[inds, :] ts = np.hstack((ts, -ts.sum(axis=1)[:, None])) # use Whitney 2 - form basis functions for face vector interp faces = self._simplex_faces[inds] areas = self.face_areas # [1, 2], [0, 2], [0, 1] if self.dim == 2: # i j k # t0 t1 t2 # 0 0 1 # t1 * i - t0 * j f0 = ( cross2d(barys[:, 1:], ts[:, 1:, 1 - i_dir]) * areas[faces[:, 0]] ) f1 = ( cross2d(barys[:, [0, 2]], ts[:, [0, 2], 1 - i_dir]) * areas[faces[:, 1]] ) f2 = ( cross2d(barys[:, :-1], ts[:, :-1, 1 - i_dir]) * areas[faces[:, 2]] ) if i_dir == 1: f0 *= -1 f1 *= -1 f2 *= -1 Aij = np.c_[f0, f1, f2].reshape(-1) ind_ptr = 3 * np.arange(n_loc + 1) else: # [1, 2, 3], [0, 2, 3], [0, 1, 3], [0, 1, 2] # f123 = (L1 * G_2 x G_3 + L2 * G_3 x G_1 + L3 * G_1 x G_2) # f023 = (L0 * G_2 x G_3 + L2 * G_3 x G_0 + L3 * G_0 x G_2) # f013 = (L0 * G_1 x G_3 + L1 * G_3 x G_0 + L3 * G_0 x G_1) # f012 = (L0 * G_1 x G_2 + L1 * G_2 x G_0 + L2 * G_0 x G_1) f0 = ( 2 * ( barys[:, 1] * (np.cross(ts[:, 2], ts[:, 3])[:, i_dir]) + barys[:, 2] * (np.cross(ts[:, 3], ts[:, 1])[:, i_dir]) + barys[:, 3] * (np.cross(ts[:, 1], ts[:, 2])[:, i_dir]) ) * areas[faces[:, 0]] ) f1 = ( 2 * ( barys[:, 0] * (np.cross(ts[:, 2], ts[:, 3])[:, i_dir]) + barys[:, 2] * (np.cross(ts[:, 3], ts[:, 0])[:, i_dir]) + barys[:, 3] * (np.cross(ts[:, 0], ts[:, 2])[:, i_dir]) ) * areas[faces[:, 1]] ) f2 = ( 2 * ( barys[:, 0] * (np.cross(ts[:, 1], ts[:, 3])[:, i_dir]) + barys[:, 1] * (np.cross(ts[:, 3], ts[:, 0])[:, i_dir]) + barys[:, 3] * (np.cross(ts[:, 0], ts[:, 1])[:, i_dir]) ) * areas[faces[:, 2]] ) f3 = ( 2 * ( barys[:, 0] * (np.cross(ts[:, 1], ts[:, 2])[:, i_dir]) + barys[:, 1] * (np.cross(ts[:, 2], ts[:, 0])[:, i_dir]) + barys[:, 2] * (np.cross(ts[:, 0], ts[:, 1])[:, i_dir]) ) * areas[faces[:, 3]] ) Aij = np.c_[f0, f1, f2, f3].reshape(-1) ind_ptr = 4 * np.arange(n_loc + 1) col_inds = faces.reshape(-1) n_items = self.n_faces return sp.csr_matrix((Aij, col_inds, ind_ptr), shape=(n_loc, n_items)) @property def average_node_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_cell", None) is None: nodes_per_cell = self.dim + 1 n_cells = self.n_cells ind_ptr = nodes_per_cell * np.arange(n_cells + 1) col_inds = self.simplices.reshape(-1) Aij = np.full(nodes_per_cell * n_cells, 1 / nodes_per_cell) self._average_node_to_cell = sp.csr_matrix( (Aij, col_inds, ind_ptr), shape=(n_cells, self.n_nodes) ) return self._average_node_to_cell @property def average_node_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_face", None) is None: nodes_per_face = self.dim n_faces = self.n_faces ind_ptr = nodes_per_face * np.arange(n_faces + 1) col_inds = self._faces.reshape(-1) Aij = np.full(nodes_per_face * n_faces, 1 / nodes_per_face) self._average_node_to_face = sp.csr_matrix( (Aij, col_inds, ind_ptr), shape=(n_faces, self.n_nodes) ) return self._average_node_to_face @property def average_node_to_edge(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_node_to_edge", None) is None: n_edges = self.n_edges ind_ptr = 2 * np.arange(n_edges + 1) col_inds = self._edges.reshape(-1) Aij = np.full(2 * n_edges, 0.5) self._average_node_to_edge = sp.csr_matrix( (Aij, col_inds, ind_ptr), shape=(n_edges, self.n_nodes) ) return self._average_node_to_edge @property def average_cell_to_node(self): """Averaging matrix from cell centers to nodes. The averaging operation uses a volume weighting scheme. Returns ------- (n_nodes, n_cells) scipy.sparse.csr_matrix """ # this reproduces linear functions everywhere except on the boundary nodes if getattr(self, "_average_cell_to_node", None) is None: simps = self.simplices cells = np.broadcast_to( np.arange(self.n_cells)[:, None], simps.shape ).reshape(-1) weights = np.broadcast_to(self.cell_volumes[:, None], simps.shape).reshape( -1 ) simps = simps.reshape(-1) A = sp.csr_matrix( (weights, (simps, cells)), shape=(self.n_nodes, self.n_cells) ) norm = sp.diags(1.0 / np.asarray(A.sum(axis=1))[:, 0]) self._average_cell_to_node = norm @ A return self._average_cell_to_node @property def average_cell_to_edge(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh # Simple averaging of all cells with a common edge if getattr(self, "_average_cell_to_edge", None) is None: simps = self._simplex_edges cells = np.broadcast_to( np.arange(self.n_cells)[:, None], simps.shape ).reshape(-1) weights = np.broadcast_to(self.cell_volumes[:, None], simps.shape).reshape( -1 ) simps = simps.reshape(-1) A = sp.csr_matrix( (weights, (simps, cells)), shape=(self.n_edges, self.n_cells) ) norm = sp.diags(1.0 / np.asarray(A.sum(axis=1))[:, 0]) self._average_cell_to_edge = norm @ A return self._average_cell_to_edge @property def average_face_to_cell_vector(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_face_to_cell_vector", None) is None: dim = self.dim n_cells = self.n_cells n_faces = self.n_faces nodes_per_cell = dim + 1 Av = sp.csr_matrix((dim * n_cells, n_faces)) Ps = self.__get_inner_product_projection_matrices( "F", with_volume=False, return_pointers=False ) for P in Ps: Av = Av + 1 / (nodes_per_cell) * P # Av needs to be re-ordered to comply with discretize standard ind = np.arange(Av.shape[0]).reshape(n_cells, -1).flatten(order="F") P = sp.eye(Av.shape[0], format="csr")[ind] self._average_face_to_cell_vector = P @ Av return self._average_face_to_cell_vector @property def average_edge_to_cell_vector(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_edge_to_cell_vector", None) is None: dim = self.dim n_cells = self.n_cells n_edges = self.n_edges nodes_per_cell = dim + 1 Av = sp.csr_matrix((dim * n_cells, n_edges)) # Precalc indptr and values for the projection matrix Ps = self.__get_inner_product_projection_matrices( "E", with_volume=False, return_pointers=False ) for P in Ps: Av = Av + 1 / (nodes_per_cell) * P # Av needs to be re-ordered to comply with discretize standard ind = np.arange(Av.shape[0]).reshape(n_cells, -1).flatten(order="F") P = sp.eye(Av.shape[0], format="csr")[ind] self._average_edge_to_cell_vector = P @ Av return self._average_edge_to_cell_vector @property def average_face_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_face_to_cell", None) is None: n_cells = self.n_cells n_faces = self.n_faces col_inds = self._simplex_faces n_face_per_cell = col_inds.shape[1] Aij = np.full((n_cells, n_face_per_cell), 1.0 / n_face_per_cell) row_ptr = np.arange(n_cells + 1) * (n_face_per_cell) self._average_face_to_cell = sp.csr_matrix( (Aij.reshape(-1), col_inds.reshape(-1), row_ptr), shape=(n_cells, n_faces), ) return self._average_face_to_cell @property def average_edge_to_cell(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_edge_to_cell", None) is None: n_cells = self.n_cells n_edges = self.n_edges col_inds = self._simplex_edges n_edge_per_cell = col_inds.shape[1] Aij = np.full((n_cells, n_edge_per_cell), 1.0 / (n_edge_per_cell)) row_ptr = np.arange(n_cells + 1) * (n_edge_per_cell) self._average_edge_to_cell = sp.csr_matrix( (Aij.reshape(-1), col_inds.reshape(-1), row_ptr), shape=(n_cells, n_edges), ) return self._average_edge_to_cell @property def average_edge_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: # in 2D edges are at the same location as faces return sp.eye(self.n_edges, self.n_edges) if getattr(self, "_average_edge_to_face", None) is None: # in 3D there are three edges per face n_faces = self.n_faces n_edges = self.n_edges face_edges = self._face_edges ind_ptr = 3 * np.arange(n_faces + 1) col_inds = face_edges.reshape(-1) Aijs = np.full(3 * self.n_faces, 1 / 3) self._average_edge_to_face = sp.csr_matrix( (Aijs, col_inds, ind_ptr), shape=(n_faces, n_edges) ) return self._average_edge_to_face @property def average_cell_to_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if getattr(self, "_average_cell_to_face", None) is None: A = self.average_face_to_cell.T row_sum = np.asarray(A.sum(axis=-1))[:, 0] row_sum[row_sum == 0.0] = 1.0 self._average_cell_to_face = sp.diags(1.0 / row_sum) @ A return self._average_cell_to_face @property def stencil_cell_gradient(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh # An operator that differences cells on each side of a face # in the direction of the face normal if getattr(self, "_stencil_cell_gradient", None) is None: tests = self.cell_centers[:, None, :] - self.faces[self._simplex_faces] Aij = np.sign( np.einsum( "ijk, ijk -> ij", tests, self.face_normals[self._simplex_faces] ) ).reshape(-1) ind_ptr = 3 * np.arange(self.n_cells + 1) col_inds = self._simplex_faces.reshape(-1) self._stencil_cell_gradient = sp.csr_matrix( (Aij, col_inds, ind_ptr), shape=(self.n_cells, self.n_faces) ).T return self._stencil_cell_gradient @property def boundary_face_list(self): """Boolean array of faces that lie on the boundary of the mesh. Returns ------- (n_faces) numpy.ndarray of bool """ if getattr(self, "_boundary_face_list", None) is None: ind_dir = np.where(self.neighbors == -1) self._is_boundary_face = self._simplex_faces[ind_dir] return self._is_boundary_face @property def project_face_to_boundary_face(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return sp.eye(self.n_faces, format="csr")[self.boundary_face_list] @property def project_edge_to_boundary_edge(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: return self.project_face_to_boundary_face bound_edges = np.unique(self._face_edges[self.boundary_face_list]) return sp.eye(self.n_edges, format="csr")[bound_edges] @property def project_node_to_boundary_node(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh bound_nodes = np.unique(self._faces[self.boundary_face_list]) return sp.eye(self.n_nodes, format="csr")[bound_nodes] @property def boundary_nodes(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh bound_nodes = np.unique(self._faces[self.boundary_face_list]) return self.nodes[bound_nodes] @property def boundary_edges(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh if self.dim == 2: return self.boundary_faces bound_nodes = np.unique(self._face_edges[self.boundary_face_list]) return self.edges[bound_nodes] @property def boundary_faces(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh return self.faces[self.boundary_face_list] @property def boundary_face_outward_normals(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh bound_cells, which_face = np.where(self.neighbors == -1) bound_faces = self._simplex_faces[(bound_cells, which_face)] bound_face_normals = self.face_normals[bound_faces] out_ish = self.faces[bound_faces] - self.cell_centers[bound_cells] direc = np.sign(np.einsum("ij,ij->i", bound_face_normals, out_ish)) boundary_face_outward_normals = direc[:, None] * bound_face_normals return boundary_face_outward_normals @property def boundary_face_scalar_integral(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh P = self.project_face_to_boundary_face w_h_dot_normal = np.sum( (P @ self.face_normals) * self.boundary_face_outward_normals, axis=-1 ) A = sp.diags(self.face_areas) @ P.T @ sp.diags(w_h_dot_normal) return A @property def boundary_node_vector_integral(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh Pn = self.project_node_to_boundary_node Pf = self.project_face_to_boundary_face n_boundary_nodes = Pn.shape[0] dA = self.boundary_face_outward_normals * (Pf @ self.face_areas)[:, None] Av = Pf @ self.average_node_to_face @ Pn.T u_dot_ds = Av.T @ dA diags = u_dot_ds.T offsets = n_boundary_nodes * np.arange(self.dim) return Pn.T @ sp.diags( diags, offsets, shape=(n_boundary_nodes, self.dim * n_boundary_nodes) ) @property def boundary_edge_vector_integral(self): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh boundary_faces = self.boundary_face_list if self.dim == 2: boundary_face_edges = boundary_faces dA = ( self.boundary_face_outward_normals * self.face_areas[boundary_faces][:, None] ) # projection matrices # for each edge on boundary faces Pe = self.project_edge_to_boundary_edge n_boundary_edges, n_edges = Pe.shape index = boundary_face_edges w_cross_n = cross2d(dA, self.edge_tangents[index]) M_be = ( sp.csr_matrix((w_cross_n, (index, index)), shape=(n_edges, n_edges)) @ Pe.T ) else: Ps = self._get_edge_surf_int_proj_mats(only_boundary=True, with_area=True) # the cross product of a vector defined on each face with the face outward normal... # so V cross n = -n cross V cx = sp.diags(self.boundary_face_outward_normals[:, 0]) cy = sp.diags(self.boundary_face_outward_normals[:, 1]) cz = sp.diags(self.boundary_face_outward_normals[:, 2]) # the negative cross mat cross_mat = sp.bmat([[None, cz, -cy], [-cz, None, cx], [cy, -cx, None]]) Pf = self.project_face_to_boundary_face Pe = self.project_edge_to_boundary_edge Av = (Pf @ self.average_edge_to_face) @ Pe.T Av = cross_mat @ sp.block_diag((Av, Av, Av)) M_be = np.sum(Ps).T @ Av return M_be def __reduce__(self): """Return the class and attributes necessary to reconstruct the mesh.""" return self.__class__, ( self.nodes, self.simplices, ) ================================================ FILE: discretize/utils/__init__.py ================================================ """ ======================================================== Utility Classes and Functions (:mod:`discretize.utils`) ======================================================== .. currentmodule:: discretize.utils The ``utils`` package contains utilities for helping with common operations involving discrete meshes Utility Classes =============== .. autosummary:: :toctree: generated/ TensorType Zero Identity Utility Functions ================= Code Utilities -------------- .. autosummary:: :toctree: generated/ is_scalar as_array_n_by_dim requires Coordinate Transform Utilities ------------------------------ .. autosummary:: :toctree: generated/ rotate_points_from_normals rotation_matrix_from_normals cylindrical_to_cartesian cartesian_to_cylindrical Interpolation Utilities ----------------------- .. autosummary:: :toctree: generated/ interpolation_matrix volume_average IO utilities ------------ .. autosummary:: :toctree: generated/ load_mesh download Matrix Utilities ---------------- .. autosummary:: :toctree: generated/ mkvc sdiag sdinv speye kron3 spzeros ddx av av_extrap ndgrid ind2sub sub2ind get_subarray inverse_3x3_block_diagonal inverse_2x2_block_diagonal invert_blocks make_property_tensor inverse_property_tensor cross2d Mesh Utilities -------------- .. autosummary:: :toctree: generated/ unpack_widths closest_points_index extract_core_mesh random_model refine_tree_xyz active_from_xyz mesh_builder_xyz Utilities for Curvilinear Meshes -------------------------------- .. autosummary:: :toctree: generated/ example_curvilinear_grid volume_tetrahedron face_info index_cube """ from discretize.utils.code_utils import is_scalar, as_array_n_by_dim, requires from discretize.utils.matrix_utils import ( mkvc, sdiag, sdinv, speye, kron3, spzeros, ddx, av, av_extrap, ndgrid, make_boundary_bool, ind2sub, sub2ind, get_subarray, inverse_3x3_block_diagonal, inverse_2x2_block_diagonal, invert_blocks, TensorType, make_property_tensor, inverse_property_tensor, Zero, Identity, ) from discretize.utils.mesh_utils import ( unpack_widths, closest_points_index, extract_core_mesh, random_model, refine_tree_xyz, active_from_xyz, mesh_builder_xyz, example_simplex_mesh, ) from discretize.utils.curvilinear_utils import ( example_curvilinear_grid, volume_tetrahedron, face_info, index_cube, ) from discretize.utils.interpolation_utils import interpolation_matrix, volume_average from discretize.utils.coordinate_utils import ( rotate_points_from_normals, rotation_matrix_from_normals, cyl2cart, cart2cyl, cylindrical_to_cartesian, cartesian_to_cylindrical, # rotate_vec_cyl2cart ) from discretize.utils.io_utils import download, load_mesh # DEPRECATIONS from discretize.utils.code_utils import isScalar, asArray_N_x_Dim from discretize.utils.matrix_utils import ( sdInv, getSubArray, inv3X3BlockDiagonal, inv2X2BlockDiagonal, makePropertyTensor, invPropertyTensor, cross2d, ) from discretize.utils.mesh_utils import ( meshTensor, closestPoints, ExtractCoreMesh, ) from discretize.utils.curvilinear_utils import ( exampleLrmGrid, volTetra, indexCube, faceInfo, ) from discretize.utils.interpolation_utils import interpmat from discretize.utils.coordinate_utils import ( rotationMatrixFromNormals, rotatePointsFromNormals, ) ================================================ FILE: discretize/utils/code_utils.py ================================================ """Utilities for common operations within code.""" import numpy as np import warnings SCALARTYPES = (complex, float, int, np.number) def is_scalar(f): """Determine if the input argument is a scalar. The function **is_scalar** returns *True* if the input is an integer, float or complex number. The function returns *False* otherwise. Parameters ---------- f : object Any input quantity Returns ------- bool - *True* if the input argument is an integer, float or complex number - *False* otherwise """ if isinstance(f, SCALARTYPES): return True elif ( isinstance(f, np.ndarray) and f.size == 1 and isinstance(f.reshape(-1)[0], SCALARTYPES) ): return True return False def as_array_n_by_dim(pts, dim): """Coerce the given array to have *dim* columns. The function **as_array_n_by_dim** will examine the *pts* array, and coerce it to be at least if the number of columns is equal to *dim*. This is similar to the :func:`numpy.atleast_2d`, except that it ensures that then input has *dim* columns, and it appends a :data:`numpy.newaxis` to 1D arrays instead of prepending. Parameters ---------- pts : array_like array to check. dim : int The number of columns which *pts* should have Returns ------- (n_pts, dim) numpy.ndarray verified array """ pts = np.asarray(pts) if dim > 1: pts = np.atleast_2d(pts) elif len(pts.shape) == 1: pts = pts[:, np.newaxis] if pts.shape[1] != dim: raise ValueError( "pts must be a column vector of shape (nPts, {0:d}) not ({1:d}, {2:d})".format( *((dim,) + pts.shape) ) ) return pts def requires(modules): """Decorate a function with soft dependencies. This function was inspired by the `requires` function of pysal, which is released under the 'BSD 3-Clause "New" or "Revised" License'. https://github.com/pysal/pysal/blob/master/pysal/lib/common.py Parameters ---------- modules : dict Dictionary containing soft dependencies, e.g., {'matplotlib': matplotlib}. Returns ------- decorated_function : function Original function if all soft dependencies are met, otherwise it returns an empty function which prints why it is not running. """ # Check the required modules, add missing ones in the list `missing`. missing = [] for key, item in modules.items(): if item is False: missing.append(key) def decorated_function(function): """Wrap function.""" if not missing: return function else: def passer(*args, **kwargs): print(("Missing dependencies: {d}.".format(d=missing))) print(("Not running `{}`.".format(function.__name__))) return passer return decorated_function def deprecate_class( removal_version=None, new_location=None, future_warn=False, error=False ): """Decorate a class as deprecated. Parameters ---------- removal_version : str, optional Which version the class will be removed in. new_location : str, optional A new package location for the class. future_warn : bool, optional Whether to issue a FutureWarning, or a DeprecationWarning. error : bool, optional Throw error if deprecated class no longer implemented """ if future_warn: warn = FutureWarning else: warn = DeprecationWarning def decorator(cls): my_name = cls.__name__ parent_name = cls.__bases__[0].__name__ message = f"{my_name} has been deprecated, please use {parent_name}." if error: message = f"{my_name} has been removed, please use {parent_name}." elif removal_version is not None: message += ( f" It will be removed in version {removal_version} of discretize." ) else: message += " It will be removed in a future version of discretize." # stash the original initialization of the class cls._old__init__ = cls.__init__ def __init__(self, *args, **kwargs): if error: raise NotImplementedError(message) else: warnings.warn(message, warn, stacklevel=2) self._old__init__(*args, **kwargs) cls.__init__ = __init__ if new_location is not None: parent_name = f"{new_location}.{parent_name}" cls.__doc__ = f""" This class has been deprecated, see `{parent_name}` for documentation""" return cls return decorator def deprecate_module( old_name, new_name, removal_version=None, future_warn=False, error=False ): """Deprecate a module. Parameters ---------- old_name : str The old name of the module. new_name : str The new name of the module. removal_version : str, optional Which version the class will be removed in. future_warn : bool, optional Whether to issue a FutureWarning, or a DeprecationWarning. error : bool, default: ``False`` Throw error if deprecated module no longer implemented """ if future_warn: warn = FutureWarning else: warn = DeprecationWarning message = f"The {old_name} module has been deprecated, please use {new_name}." if error: message = f"{old_name} has been removed, please use {new_name}." elif removal_version is not None: message += f" It will be removed in version {removal_version} of discretize" else: message += " It will be removed in a future version of discretize." message += " Please update your code accordingly." if error: raise NotImplementedError(message) else: warnings.warn(message, warn, stacklevel=2) def deprecate_property( new_name, old_name, removal_version=None, future_warn=False, error=False ): """Deprecate a class property. Parameters ---------- new_name : str The new name of the property. old_name : str The old name of the property. removal_version : str, optional Which version the class will be removed in. future_warn : bool, optional Whether to issue a FutureWarning, or a DeprecationWarning. error : bool, default: ``False`` Throw error if deprecated property no longer implemented """ if future_warn: warn = FutureWarning else: warn = DeprecationWarning if error: tag = "" elif removal_version is not None: tag = f" It will be removed in version {removal_version} of discretize." else: tag = " It will be removed in a future version of discretize." def get_dep(self): class_name = type(self).__name__ message = ( f"{class_name}.{old_name} has been deprecated, please use {class_name}.{new_name}." + tag ) if error: raise NotImplementedError(message.replace("deprecated", "removed")) else: warnings.warn(message, warn, stacklevel=2) return getattr(self, new_name) def set_dep(self, other): class_name = type(self).__name__ message = ( f"{class_name}.{old_name} has been deprecated, please use {class_name}.{new_name}." + tag ) if error: raise NotImplementedError(message.replace("deprecated", "removed")) else: warnings.warn(message, warn, stacklevel=2) setattr(self, new_name, other) doc = f""" `{old_name}` has been deprecated. See `{new_name}` for documentation. See Also -------- {new_name} """ return property(get_dep, set_dep, None, doc) def deprecate_method( new_name, old_name, removal_version=None, future_warn=False, error=False ): """Deprecate a class method. Parameters ---------- new_name : str The new name of the method. old_name : str The old name of the method. removal_version : str, optional Which version the class will be removed in. future_warn : bool, optional Whether to issue a FutureWarning, or a DeprecationWarning. error : bool, default: ``False`` Throw error if deprecated method no longer implemented """ if future_warn: warn = FutureWarning else: warn = DeprecationWarning if error: tag = "" elif removal_version is not None: tag = f" It will be removed in version {removal_version} of discretize." else: tag = " It will be removed in a future version of discretize." def new_method(self, *args, **kwargs): class_name = type(self).__name__ message = ( f"{class_name}.{old_name} has been deprecated, please use " f"{class_name}.{new_name}.{tag}" ) if error: raise NotImplementedError(message.replace("deprecated", "removed")) else: warnings.warn( message, warn, stacklevel=2, ) return getattr(self, new_name)(*args, **kwargs) doc = f""" `{old_name}` has been deprecated. See `{new_name}` for documentation See Also -------- {new_name} """ if error: doc = doc.replace("deprecated", "removed") new_method.__doc__ = doc return new_method def deprecate_function( new_function, old_name, removal_version=None, future_warn=False, error=False ): """Deprecate a function. Parameters ---------- new_function : callable The new function. old_name : str The old name of the function. removal_version : str, optional Which version the class will be removed in. future_warn : bool, optional Whether to issue a FutureWarning, or a DeprecationWarning. error : bool, default: ``False`` Throw error if deprecated function no longer implemented """ if future_warn: warn = FutureWarning else: warn = DeprecationWarning new_name = new_function.__name__ if error: tag = "" elif removal_version is not None: tag = f" It will be removed in version {removal_version} of discretize." else: tag = " It will be removed in a future version of discretize." message = f"{old_name} has been deprecated, please use {new_name}." + tag def dep_function(*args, **kwargs): if error: raise NotImplementedError(message.replace("deprecated", "removed")) else: warnings.warn( message, warn, stacklevel=2, ) return new_function(*args, **kwargs) doc = f""" `{old_name}` has been deprecated. See `{new_name}` for documentation See Also -------- {new_name} """ if error: doc = doc.replace("deprecated", "removed") dep_function.__doc__ = doc return dep_function # DEPRECATIONS isScalar = deprecate_function( is_scalar, "isScalar", removal_version="1.0.0", error=True ) asArray_N_x_Dim = deprecate_function( as_array_n_by_dim, "asArray_N_x_Dim", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/utils/codeutils.py ================================================ from discretize.utils.code_utils import * # NOQA F401,F403 raise ImportError( "Importing from discretize.codeutils is deprecated behavoir. Please import " "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", ) ================================================ FILE: discretize/utils/coordinate_utils.py ================================================ """Simple utilities for coordinate transformations.""" import numpy as np from discretize.utils.matrix_utils import mkvc from discretize.utils.code_utils import as_array_n_by_dim, deprecate_function def cylindrical_to_cartesian(grid, vec=None): r"""Transform from cylindrical to cartesian coordinates. Transform a grid or a vector from cylindrical coordinates :math:`(r, \theta, z)` to Cartesian coordinates :math:`(x, y, z)`. :math:`\theta` is given in radians. Parameters ---------- grid : (n, 3) array_like Location points defined in cylindrical coordinates :math:`(r, \theta, z)`. vec : (n, 3) array_like, optional Vector defined in cylindrical coordinates :math:`(r, \theta, z)` at the locations grid. Will also except a flattend array in column major order with the same number of elements. Returns ------- (n, 3) numpy.ndarray If `vec` is ``None``, this returns the transformed `grid` array, otherwise this is the transformed `vec` array. Examples -------- Here, we convert a series of vectors in 3D space from cylindrical coordinates to Cartesian coordinates. >>> from discretize.utils import cylindrical_to_cartesian >>> import numpy as np Construct original set of vectors in cylindrical coordinates >>> r = np.ones(9) >>> phi = np.linspace(0, 2*np.pi, 9) >>> z = np.linspace(-4., 4., 9) >>> u = np.c_[r, phi, z] >>> u array([[ 1. , 0. , -4. ], [ 1. , 0.78539816, -3. ], [ 1. , 1.57079633, -2. ], [ 1. , 2.35619449, -1. ], [ 1. , 3.14159265, 0. ], [ 1. , 3.92699082, 1. ], [ 1. , 4.71238898, 2. ], [ 1. , 5.49778714, 3. ], [ 1. , 6.28318531, 4. ]]) Create equivalent set of vectors in Cartesian coordinates >>> v = cylindrical_to_cartesian(u) >>> v array([[ 1.00000000e+00, 0.00000000e+00, -4.00000000e+00], [ 7.07106781e-01, 7.07106781e-01, -3.00000000e+00], [ 6.12323400e-17, 1.00000000e+00, -2.00000000e+00], [-7.07106781e-01, 7.07106781e-01, -1.00000000e+00], [-1.00000000e+00, 1.22464680e-16, 0.00000000e+00], [-7.07106781e-01, -7.07106781e-01, 1.00000000e+00], [-1.83697020e-16, -1.00000000e+00, 2.00000000e+00], [ 7.07106781e-01, -7.07106781e-01, 3.00000000e+00], [ 1.00000000e+00, -2.44929360e-16, 4.00000000e+00]]) """ grid = np.atleast_2d(grid) if vec is None: return np.hstack( [ mkvc(grid[:, 0] * np.cos(grid[:, 1]), 2), mkvc(grid[:, 0] * np.sin(grid[:, 1]), 2), mkvc(grid[:, 2], 2), ] ) vec = np.asanyarray(vec) if len(vec.shape) == 1 or vec.shape[1] == 1: vec = vec.reshape(grid.shape, order="F") x = vec[:, 0] * np.cos(grid[:, 1]) - vec[:, 1] * np.sin(grid[:, 1]) y = vec[:, 0] * np.sin(grid[:, 1]) + vec[:, 1] * np.cos(grid[:, 1]) newvec = [x, y] if grid.shape[1] == 3: z = vec[:, 2] newvec += [z] return np.vstack(newvec).T def cyl2cart(grid, vec=None): """Transform from cylindrical to cartesian coordinates. An alias for `cylindrical_to_cartesian``. See Also -------- cylindrical_to_cartesian """ return cylindrical_to_cartesian(grid, vec) def cartesian_to_cylindrical(grid, vec=None): r"""Transform from cartesian to cylindrical coordinates. Transform a grid or a vector from Cartesian coordinates :math:`(x, y, z)` to cylindrical coordinates :math:`(r, \theta, z)`. Parameters ---------- grid : (n, 3) array_like Location points defined in Cartesian coordinates :math:`(x, y z)`. vec : (n, 3) array_like, optional Vector defined in Cartesian coordinates. This also accepts a flattened array with the same total elements in column major order. Returns ------- (n, 3) numpy.ndarray If `vec` is ``None``, this returns the transformed `grid` array, otherwise this is the transformed `vec` array. Examples -------- Here, we convert a series of vectors in 3D space from Cartesian coordinates to cylindrical coordinates. >>> from discretize.utils import cartesian_to_cylindrical >>> import numpy as np Create set of vectors in Cartesian coordinates >>> r = np.ones(9) >>> phi = np.linspace(0, 2*np.pi, 9) >>> z = np.linspace(-4., 4., 9) >>> x = r*np.cos(phi) >>> y = r*np.sin(phi) >>> u = np.c_[x, y, z] >>> u array([[ 1.00000000e+00, 0.00000000e+00, -4.00000000e+00], [ 7.07106781e-01, 7.07106781e-01, -3.00000000e+00], [ 6.12323400e-17, 1.00000000e+00, -2.00000000e+00], [-7.07106781e-01, 7.07106781e-01, -1.00000000e+00], [-1.00000000e+00, 1.22464680e-16, 0.00000000e+00], [-7.07106781e-01, -7.07106781e-01, 1.00000000e+00], [-1.83697020e-16, -1.00000000e+00, 2.00000000e+00], [ 7.07106781e-01, -7.07106781e-01, 3.00000000e+00], [ 1.00000000e+00, -2.44929360e-16, 4.00000000e+00]]) Compute equivalent set of vectors in cylindrical coordinates >>> v = cartesian_to_cylindrical(u) >>> v array([[ 1.00000000e+00, 0.00000000e+00, -4.00000000e+00], [ 1.00000000e+00, 7.85398163e-01, -3.00000000e+00], [ 1.00000000e+00, 1.57079633e+00, -2.00000000e+00], [ 1.00000000e+00, 2.35619449e+00, -1.00000000e+00], [ 1.00000000e+00, 3.14159265e+00, 0.00000000e+00], [ 1.00000000e+00, -2.35619449e+00, 1.00000000e+00], [ 1.00000000e+00, -1.57079633e+00, 2.00000000e+00], [ 1.00000000e+00, -7.85398163e-01, 3.00000000e+00], [ 1.00000000e+00, -2.44929360e-16, 4.00000000e+00]]) """ grid = as_array_n_by_dim(grid, 3) theta = np.arctan2(grid[:, 1], grid[:, 0]) if vec is None: return np.c_[np.linalg.norm(grid[:, :2], axis=-1), theta, grid[:, 2]] vec = as_array_n_by_dim(vec, 3) return np.hstack( [ mkvc(np.cos(theta) * vec[:, 0] + np.sin(theta) * vec[:, 1], 2), mkvc(-np.sin(theta) * vec[:, 0] + np.cos(theta) * vec[:, 1], 2), mkvc(vec[:, 2], 2), ] ) def cart2cyl(grid, vec=None): """Transform from cartesian to cylindrical coordinates. An alias for cartesian_to_cylindrical See Also -------- cartesian_to_cylindrical """ return cartesian_to_cylindrical(grid, vec) def rotation_matrix_from_normals(v0, v1, tol=1e-20): r"""Generate a 3x3 rotation matrix defining the rotation from vector v0 to v1. This function uses Rodrigues' rotation formula to generate the rotation matrix :math:`\mathbf{A}` going from vector :math:`\mathbf{v_0}` to vector :math:`\mathbf{v_1}`. Thus: .. math:: \mathbf{Av_0} = \mathbf{v_1} For detailed desciption of the algorithm, see https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula Parameters ---------- v0 : (3) numpy.ndarray Starting orientation direction v1 : (3) numpy.ndarray Finishing orientation direction tol : float, optional Numerical tolerance. If the length of the rotation axis is below this value, it is assumed to be no rotation, and an identity matrix is returned. Returns ------- (3, 3) numpy.ndarray The rotation matrix from v0 to v1. """ # ensure both v0, v1 are vectors of length 1 if len(v0) != 3: raise ValueError("Length of n0 should be 3") if len(v1) != 3: raise ValueError("Length of n1 should be 3") # ensure both are true normals n0 = v0 * 1.0 / np.linalg.norm(v0) n1 = v1 * 1.0 / np.linalg.norm(v1) n0dotn1 = n0.dot(n1) # define the rotation axis, which is the cross product of the two vectors rotAx = np.cross(n0, n1) if np.linalg.norm(rotAx) < tol: return np.eye(3, dtype=float) rotAx *= 1.0 / np.linalg.norm(rotAx) cosT = n0dotn1 / (np.linalg.norm(n0) * np.linalg.norm(n1)) sinT = np.sqrt(1.0 - n0dotn1**2) ux = np.array( [ [0.0, -rotAx[2], rotAx[1]], [rotAx[2], 0.0, -rotAx[0]], [-rotAx[1], rotAx[0], 0.0], ], dtype=float, ) return np.eye(3, dtype=float) + sinT * ux + (1.0 - cosT) * (ux.dot(ux)) def rotate_points_from_normals(xyz, v0, v1, x0=np.r_[0.0, 0.0, 0.0]): r"""Rotate a set of xyz locations about a specified point. Rotate a grid of Cartesian points about a location x0 according to the rotation defined from vector v0 to v1. Let :math:`\mathbf{x}` represent an input xyz location, let :math:`\mathbf{x_0}` be the origin of rotation, and let :math:`\mathbf{R}` denote the rotation matrix from vector v0 to v1. Where :math:`\mathbf{x'}` is the new xyz location, this function outputs the following operation for all input locations: .. math:: \mathbf{x'} = \mathbf{R (x - x_0)} + \mathbf{x_0} Parameters ---------- xyz : (n, 3) numpy.ndarray locations to rotate v0 : (3) numpy.ndarray Starting orientation direction v1 : (3) numpy.ndarray Finishing orientation direction x0 : (3) numpy.ndarray, optional The origin of rotation. Returns ------- (n, 3) numpy.ndarray The rotated xyz locations. """ # Compute rotation matrix between v0 and v1 R = rotation_matrix_from_normals(v0, v1) if xyz.shape[1] != 3: raise ValueError("Grid of xyz points should be n x 3") if len(x0) != 3: raise ValueError("x0 should have length 3") # Define origin X0 = np.ones([xyz.shape[0], 1]) * mkvc(x0) return (xyz - X0).dot(R.T) + X0 # equivalent to (R*(xyz - X0)).T + X0 rotationMatrixFromNormals = deprecate_function( rotation_matrix_from_normals, "rotationMatrixFromNormals", removal_version="1.0.0", error=True, ) rotatePointsFromNormals = deprecate_function( rotate_points_from_normals, "rotatePointsFromNormals", removal_version="1.0.0", error=True, ) ================================================ FILE: discretize/utils/coordutils.py ================================================ from discretize.utils.coordinate_utils import * # NOQA F401,F403 raise ImportError( "Importing from discretize.coordutils is deprecated behavoir. Please import " "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", ) ================================================ FILE: discretize/utils/curvilinear_utils.py ================================================ """Functions for working with curvilinear meshes.""" import numpy as np from discretize.utils.matrix_utils import mkvc, ndgrid, sub2ind from discretize.utils.code_utils import deprecate_function def example_curvilinear_grid(nC, exType): """Create the gridded node locations for a curvilinear mesh. Parameters ---------- nC : list of int list of number of cells in each dimension. Must be length 2 or 3 exType : {"rect", "rotate", "sphere"} String specifying the style of example curvilinear mesh. Returns ------- list of numpy.ndarray List containing the gridded x, y (and z) node locations for the curvilinear mesh. """ if not isinstance(nC, list): raise TypeError("nC must be a list containing the number of nodes") if len(nC) != 2 and len(nC) != 3: raise ValueError("nC must either two or three dimensions") exType = exType.lower() possibleTypes = ["rect", "rotate", "sphere"] if exType not in possibleTypes: raise TypeError("Not a possible example type.") if exType == "rect": return list( ndgrid([np.cumsum(np.r_[0, np.ones(nx) / nx]) for nx in nC], vector=False) ) elif exType == "sphere": nodes = list( ndgrid( [np.cumsum(np.r_[0, np.ones(nx) / nx]) - 0.5 for nx in nC], vector=False ) ) nodes = np.stack(nodes, axis=-1) nodes = 2 * nodes # L_inf distance to center r0 = np.linalg.norm(nodes, ord=np.inf, axis=-1) # L2 distance to center r2 = np.linalg.norm(nodes, axis=-1) r0[r0 == 0.0] = 1.0 r2[r2 == 0.0] = 1.0 scale = r0 / r2 nodes = nodes * scale[..., None] nodes = np.transpose(nodes, (-1, *np.arange(len(nC)))) nodes = [node for node in nodes] # turn it into a list return nodes elif exType == "rotate": if len(nC) == 2: X, Y = ndgrid( [np.cumsum(np.r_[0, np.ones(nx) / nx]) for nx in nC], vector=False ) amt = 0.5 - np.sqrt((X - 0.5) ** 2 + (Y - 0.5) ** 2) amt[amt < 0] = 0 return [X + (-(Y - 0.5)) * amt, Y + (+(X - 0.5)) * amt] elif len(nC) == 3: X, Y, Z = ndgrid( [np.cumsum(np.r_[0, np.ones(nx) / nx]) for nx in nC], vector=False ) amt = 0.5 - np.sqrt((X - 0.5) ** 2 + (Y - 0.5) ** 2 + (Z - 0.5) ** 2) amt[amt < 0] = 0 return [ X + (-(Y - 0.5)) * amt, Y + (-(Z - 0.5)) * amt, Z + (-(X - 0.5)) * amt, ] def volume_tetrahedron(xyz, A, B, C, D): r"""Return the tetrahedron volumes for a specified set of verticies. Let *xyz* be an (n, 3) array denoting a set of vertex locations. Any 4 vertex locations *a, b, c* and *d* can be used to define a tetrahedron. For the set of tetrahedra whose verticies are indexed in vectors *A, B, C* and *D*, this function returns the corresponding volumes. See algorithm: https://en.wikipedia.org/wiki/Tetrahedron#Volume .. math:: vol = {1 \over 6} \big | ( \mathbf{a - d} ) \cdot ( ( \mathbf{b - d} ) \times ( \mathbf{c - d} ) ) \big | Parameters ---------- xyz : (n_pts, 3) numpy.ndarray x,y, and z locations for all verticies A : (n_tetra) numpy.ndarray of int Vector containing the indicies for the **a** vertex locations B : (n_tetra) numpy.ndarray of int Vector containing the indicies for the **b** vertex locations C : (n_tetra) numpy.ndarray of int Vector containing the indicies for the **c** vertex locations D : (n_tetra) numpy.ndarray of int Vector containing the indicies for the **d** vertex locations Returns ------- (n_tetra) numpy.ndarray Volumes of the tetrahedra whose vertices are indexed by *A, B, C* and *D*. Examples -------- Here we define a small 3D tensor mesh. 4 nodes are chosen to be the verticies of a tetrahedron. We compute the volume of this tetrahedron. Note that xyz locations for the verticies can be scattered and do not require regular spacing. >>> from discretize.utils import volume_tetrahedron >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl Define corners of a uniform cube >>> h = [1, 1] >>> mesh = TensorMesh([h, h, h]) >>> xyz = mesh.nodes Specify the indicies of the corner points >>> A = np.array([0]) >>> B = np.array([6]) >>> C = np.array([8]) >>> D = np.array([24]) Compute volume for all tetrahedra and the extract first one >>> vol = volume_tetrahedron(xyz, A, B, C, D) >>> vol = vol[0] >>> vol array([1.33333333]) Plotting small mesh and tetrahedron >>> fig = plt.figure(figsize=(7, 7)) >>> ax = plt.subplot(projection='3d') >>> mesh.plot_grid(ax=ax) >>> k = [0, 6, 8, 0, 24, 6, 24, 8] >>> xyz_tetra = xyz[k, :] >>> ax.plot(xyz_tetra[:, 0], xyz_tetra[:, 1], xyz_tetra[:, 2], 'r') >>> ax.text(-0.25, 0., 3., 'Volume of the tetrahedron: {:g} $m^3$'.format(vol)) >>> plt.show() """ AD = xyz[A, :] - xyz[D, :] BD = xyz[B, :] - xyz[D, :] CD = xyz[C, :] - xyz[D, :] V = ( (BD[:, 0] * CD[:, 1] - BD[:, 1] * CD[:, 0]) * AD[:, 2] - (BD[:, 0] * CD[:, 2] - BD[:, 2] * CD[:, 0]) * AD[:, 1] + (BD[:, 1] * CD[:, 2] - BD[:, 2] * CD[:, 1]) * AD[:, 0] ) return np.abs(V / 6) def index_cube(nodes, grid_shape, n=None): """Return the index of nodes on a tensor (or curvilinear) mesh. For 2D tensor meshes, each cell is defined by nodes *A, B, C* and *D*. And for 3D tensor meshes, each cell is defined by nodes *A* through *H* (see below). *index_cube* outputs the indices for the specified node(s) for all cells in the mesh. TWO DIMENSIONS:: node(i,j+1) node(i+i,j+1) B -------------- C | | | cell(i,j) | | I | | | A -------------- D node(i,j) node(i+1,j) THREE DIMENSIONS:: node(i,j+1,k+1) node(i+1,j+1,k+1) F ---------------- G /| / | / | / | / | / | node(i,j,k+1) node(i+1,j,k+1) E --------------- H | | B -----------|---- C | / cell(i,j,k) | / | / I | / | / | / A --------------- D node(i,j,k) node(i+1,j,k) Parameters ---------- nodes : str String specifying which nodes to return. For 2D meshes, *nodes* must be a string containing combinations of the characters 'A', 'B', 'C', or 'D'. For 3D meshes, *nodes* can also be 'E', 'F', 'G', or 'H'. Note that order is preserved. E.g. if we want to return the C, D and A node indices in that particular order, we input *nodes* = 'CDA'. grid_shape : list of int Number of nodes along the i,j,k directions; e.g. [ni,nj,nk] nc : list of int Number of cells along the i,j,k directions; e.g. [nci,ncj,nck] Returns ------- index : tuple of numpy.ndarray Each entry of the tuple is a 1D :class:`numpy.ndarray` containing the indices of the nodes specified in the input *nodes* in the order asked; e.g. if *nodes* = 'DCBA', the tuple returned is ordered (D,C,B,A). Examples -------- Here, we construct a small 2D tensor mesh (works for a curvilinear mesh as well) and use *index_cube* to find the indices of the 'A' and 'C' nodes. We then plot the mesh, as well as the 'A' and 'C' node locations. >>> from discretize import TensorMesh >>> from discretize.utils import index_cube >>> from matplotlib import pyplot as plt >>> import numpy as np Create a simple tensor mesh. >>> n_cells = 5 >>> h = 2*np.ones(n_cells) >>> mesh = TensorMesh([h, h], x0='00') Get indices of 'A' and 'C' nodes for all cells. >>> A, C = index_cube('AC', [n_cells+1, n_cells+1]) Plot mesh and the locations of the A and C nodes >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_axes([0.1, 0.1, 0.8, 0.8]) >>> mesh.plot_grid(ax=ax1) >>> ax1.scatter(mesh.nodes[A, 0], mesh.nodes[A, 1], 100, 'r', marker='^') >>> ax1.scatter(mesh.nodes[C, 0], mesh.nodes[C, 1], 100, 'g', marker='v') >>> ax1.set_title('A nodes (red) and C nodes (green)') >>> plt.show() """ if not isinstance(nodes, str): raise TypeError("Nodes must be a str variable: e.g. 'ABCD'") nodes = nodes.upper() try: dim = len(grid_shape) if n is None: n = tuple(x - 1 for x in grid_shape) except TypeError: return TypeError("grid_shape must be iterable") # Make sure that we choose from the possible nodes. possibleNodes = "ABCD" if dim == 2 else "ABCDEFGH" for node in nodes: if node not in possibleNodes: raise ValueError("Nodes must be chosen from: '{0!s}'".format(possibleNodes)) if dim == 2: ij = ndgrid(np.arange(n[0]), np.arange(n[1])) i, j = ij[:, 0], ij[:, 1] elif dim == 3: ijk = ndgrid(np.arange(n[0]), np.arange(n[1]), np.arange(n[2])) i, j, k = ijk[:, 0], ijk[:, 1], ijk[:, 2] else: raise Exception("Only 2 and 3 dimensions supported.") nodeMap = { "A": [0, 0, 0], "B": [0, 1, 0], "C": [1, 1, 0], "D": [1, 0, 0], "E": [0, 0, 1], "F": [0, 1, 1], "G": [1, 1, 1], "H": [1, 0, 1], } out = () for node in nodes: shift = nodeMap[node] if dim == 2: out += (sub2ind(grid_shape, np.c_[i + shift[0], j + shift[1]]).flatten(),) elif dim == 3: out += ( sub2ind( grid_shape, np.c_[i + shift[0], j + shift[1], k + shift[2]] ).flatten(), ) return out def face_info(xyz, A, B, C, D, average=True, normalize_normals=True, **kwargs): r"""Return normal surface vectors and areas for a given set of faces. Let *xyz* be an (n, 3) array denoting a set of vertex locations. Now let vertex locations *a, b, c* and *d* define a quadrilateral (regular or irregular) in 2D or 3D space. For this quadrilateral, we organize the vertices as follows: CELL VERTICES:: a -------Vab------- b / / / / Vda (X) Vbc / / / / d -------Vcd------- c where the normal vector *(X)* is pointing into the page. For a set of quadrilaterals whose vertices are indexed in arrays *A, B, C* and *D* , this function returns the normal surface vector(s) and the area for each quadrilateral. At each vertex, there are 4 cross-products that can be used to compute the vector normal the surface defined by the quadrilateral. In 3D space however, the vertices indexed may not define a quadrilateral exactly and thus the normal vectors computed at each vertex might not be identical. In this case, you may choose output the normal vector at *a, b, c* and *d* or compute the average normal surface vector as follows: .. math:: \bf{n} = \frac{1}{4} \big ( \bf{v_{ab} \times v_{da}} + \bf{v_{bc} \times v_{ab}} + \bf{v_{cd} \times v_{bc}} + \bf{v_{da} \times v_{cd}} \big ) For computing the surface area, we assume the vertices define a quadrilateral. Parameters ---------- xyz : (n, 3) numpy.ndarray The x,y, and z locations for all verticies A : (n_face) numpy.ndarray Vector containing the indicies for the **a** vertex locations B : (n_face) numpy.ndarray Vector containing the indicies for the **b** vertex locations C : (n_face) numpy.ndarray Vector containing the indicies for the **c** vertex locations D : (n_face) numpy.ndarray Vector containing the indicies for the **d** vertex locations average : bool, optional If *True*, the function returns the average surface normal vector for each surface. If *False* , the function will return the normal vectors computed at the *A, B, C* and *D* vertices in a cell array {nA,nB,nC,nD}. normalize_normal : bool, optional If *True*, the function will normalize the surface normal vectors. This is applied regardless of whether the *average* parameter is set to *True* or *False*. If *False*, the vectors are not normalized. Returns ------- N : (n_face) numpy.ndarray or (4) list of (n_face) numpy.ndarray Normal vector(s) for each surface. If *average* = *True*, the function returns an ndarray with the average surface normal vectos. If *average* = *False* , the function returns a cell array {nA,nB,nC,nD} containing the normal vectors computed using each vertex of the surface. area : (n_face) numpy.ndarray The surface areas. Examples -------- Here we define a set of vertices for a tensor mesh. We then index 4 vertices for an irregular quadrilateral. The function *face_info* is used to compute the normal vector and the surface area. >>> from discretize.utils import face_info >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl Define Corners of a uniform cube. >>> h = [1, 1] >>> mesh = TensorMesh([h, h, h]) >>> xyz = mesh.nodes Choose the face indices, >>> A = np.array([0]) >>> B = np.array([4]) >>> C = np.array([26]) >>> D = np.array([18]) Compute average surface normal vector (normalized), >>> nvec, area = face_info(xyz, A, B, C, D) >>> nvec, area (array([[-0.70710678, 0.70710678, 0. ]]), array([4.24264069])) Plot surface for example 1 on mesh >>> fig = plt.figure(figsize=(7, 7)) >>> ax = plt.subplot(projection='3d') >>> mesh.plot_grid(ax=ax) >>> k = [0, 4, 26, 18, 0] >>> xyz_quad = xyz[k, :] >>> ax.plot(xyz_quad[:, 0], xyz_quad[:, 1], xyz_quad[:, 2], 'r') >>> ax.text(-0.25, 0., 3., 'Area of the surface: {:g} $m^2$'.format(area[0])) >>> ax.text(-0.25, 0., 2.8, 'Normal vector: ({:.2f}, {:.2f}, {:.2f})'.format( ... nvec[0, 0], nvec[0, 1], nvec[0, 2]) ... ) >>> plt.show() In our second example, the vertices are unable to define a flat surface in 3D space. However, we will demonstrate the *face_info* returns the average normal vector and an approximate surface area. Define the face indicies >>> A = np.array([0]) >>> B = np.array([5]) >>> C = np.array([26]) >>> D = np.array([18]) Compute average surface normal vector >>> nvec, area = face_info(xyz, A, B, C, D) >>> nvec, area (array([[-0.4472136 , 0.89442719, 0. ]]), array([2.23606798])) Plot surface for example 2 on mesh >>> fig = plt.figure(figsize=(7, 7)) >>> ax = plt.subplot(projection='3d') >>> mesh.plot_grid(ax=ax) >>> k = [0, 5, 26, 18, 0] >>> xyz_quad = xyz[k, :] >>> ax.plot(xyz_quad[:, 0], xyz_quad[:, 1], xyz_quad[:, 2], 'g') >>> ax.text(-0.25, 0., 3., 'Area of the surface: {:g} $m^2$'.format(area[0])) >>> ax.text(-0.25, 0., 2.8, 'Average normal vector: ({:.2f}, {:.2f}, {:.2f})'.format( ... nvec[0, 0], nvec[0, 1], nvec[0, 2]) ... ) >>> plt.show() """ if "normalizeNormals" in kwargs: raise TypeError( "The normalizeNormals keyword argument has been removed, please use normalize_normals. " "This will be removed in discretize 1.0.0", ) if not isinstance(average, bool): raise TypeError("average must be a boolean") if not isinstance(normalize_normals, bool): raise TypeError("normalize_normals must be a boolean") AB = xyz[B, :] - xyz[A, :] BC = xyz[C, :] - xyz[B, :] CD = xyz[D, :] - xyz[C, :] DA = xyz[A, :] - xyz[D, :] def cross(X, Y): return np.c_[ X[:, 1] * Y[:, 2] - X[:, 2] * Y[:, 1], X[:, 2] * Y[:, 0] - X[:, 0] * Y[:, 2], X[:, 0] * Y[:, 1] - X[:, 1] * Y[:, 0], ] nA = cross(AB, DA) nB = cross(BC, AB) nC = cross(CD, BC) nD = cross(DA, CD) def length(x): return np.sqrt(x[:, 0] ** 2 + x[:, 1] ** 2 + x[:, 2] ** 2) def normalize(x): return x / np.kron(np.ones((1, x.shape[1])), mkvc(length(x), 2)) if average: # average the normals at each vertex. N = (nA + nB + nC + nD) / 4 # this is intrinsically weighted by area # normalize N = normalize(N) else: if normalize_normals: N = [normalize(nA), normalize(nB), normalize(nC), normalize(nD)] else: N = [nA, nB, nC, nD] # Area calculation # # Approximate by 4 different triangles, and divide by 2. # Each triangle is one half of the length of the cross product # # So also could be viewed as the average parallelogram. # # TODO: This does not compute correctly for concave quadrilaterals area = (length(nA) + length(nB) + length(nC) + length(nD)) / 4 return N, area exampleLrmGrid = deprecate_function( example_curvilinear_grid, "exampleLrmGrid", removal_version="1.0.0", error=True, ) volTetra = deprecate_function( volume_tetrahedron, "volTetra", removal_version="1.0.0", error=True ) indexCube = deprecate_function( index_cube, "indexCube", removal_version="1.0.0", error=True ) faceInfo = deprecate_function( face_info, "faceInfo", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/utils/curvutils.py ================================================ from discretize.utils.curvilinear_utils import * # NOQA F401,F403 raise ImportError( "Importing from discretize.curvutils is deprecated behavoir. Please import " "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", ) ================================================ FILE: discretize/utils/interpolation_utils.py ================================================ """Utilities for creating averaging operators.""" import numpy as np import scipy.sparse as sp from discretize.utils.matrix_utils import mkvc, sub2ind from discretize.utils.code_utils import deprecate_function try: from discretize._extensions import interputils_cython as pyx _interp_point_1D = pyx._interp_point_1D _interpmat1D = pyx._interpmat1D _interpmat2D = pyx._interpmat2D _interpmat3D = pyx._interpmat3D _vol_interp = pyx._tensor_volume_averaging _interpCython = True except ImportError as err: print(err) import os # Check if being called from non-standard location (i.e. a git repository) # is tree_ext.cpp here? will not be in the folder if installed to site-packages... file_test = ( os.path.dirname(os.path.abspath(__file__)) + "/_extensions/interputils_cython.pyx" ) if os.path.isfile(file_test): # Then we are being run from a repository print( """ Unable to import interputils_cython. It would appear that discretize is being imported from its repository. If this is intentional, you need to run: python setup.py build_ext --inplace to build the cython code. """ ) _interpCython = False def interpolation_matrix(locs, x, y=None, z=None): """ Generate interpolation matrix which maps a tensor quantity to a set of locations. This function generates a sparse matrix for interpolating tensor quantities to a set of specified locations. It uses nD linear interpolation. The user may generate the interpolation matrix for tensor quantities that live on 1D, 2D or 3D tensors. This functionality is frequently used to interpolate quantites from cell centers or nodes to specified locations. In higher dimensions the ordering of the output has the 1st dimension changing the quickest. Parameters ---------- locs : (n, dim) numpy.ndarray The locations for the interpolated values. Here *n* is the number of locations and *dim* is the dimension (1, 2 or 3) x : (nx) numpy.ndarray Vector defining the locations of the tensor along the x-axis y : (ny) numpy.ndarray, optional Vector defining the locations of the tensor along the y-axis. Required if ``dim`` is 2. z : (nz) numpy.ndarray, optional Vector defining the locations of the tensor along the z-axis. Required if ``dim`` is 3. Returns ------- (n, nx * ny * nz) scipy.sparse.csr_matrix A sparse matrix which interpolates the tensor quantity on cell centers or nodes to the set of specified locations. Examples -------- Here is a 1D example where a function evaluated on a regularly spaced grid is interpolated to a set of random locations. To compare the accuracy, the function is evaluated at the set of random locations. >>> from discretize.utils import interpolation_matrix >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> rng = np.random.default_rng(14) Create an interpolation matrix >>> locs = rng.random(50)*0.8+0.1 >>> x = np.linspace(0, 1, 7) >>> dense = np.linspace(0, 1, 200) >>> fun = lambda x: np.cos(2*np.pi*x) >>> Q = interpolation_matrix(locs, x) Plot original function and interpolation >>> fig1 = plt.figure(figsize=(5, 3)) >>> ax = fig1.add_axes([0.1, 0.1, 0.8, 0.8]) >>> ax.plot(dense, fun(dense), 'k:', lw=3) >>> ax.plot(x, fun(x), 'ks', markersize=8) >>> ax.plot(locs, Q*fun(x), 'go', markersize=4) >>> ax.plot(locs, fun(locs), 'rs', markersize=4) >>> ax.legend( ... [ ... 'True Function', ... 'True (discrete loc.)', ... 'Interpolated (computed)', ... 'True (interp. loc.)' ... ], ... loc='upper center' ... ) >>> plt.show() Here, demonstrate a similar example on a 2D mesh using a 2D Gaussian distribution. We interpolate the Gaussian from the nodes to cell centers and examine the relative error. >>> hx = np.ones(10) >>> hy = np.ones(10) >>> mesh = TensorMesh([hx, hy], x0='CC') >>> def fun(x, y): ... return np.exp(-(x**2 + y**2)/2**2) Define the the value at the mesh nodes, >>> nodes = mesh.nodes >>> val_nodes = fun(nodes[:, 0], nodes[:, 1]) >>> centers = mesh.cell_centers >>> A = interpolation_matrix( ... centers, mesh.nodes_x, mesh.nodes_y ... ) >>> val_interp = A.dot(val_nodes) Plot the interpolated values, along with the true values at cell centers, >>> val_centers = fun(centers[:, 0], centers[:, 1]) >>> fig = plt.figure(figsize=(11,3.3)) >>> clim = (0., 1.) >>> ax1 = fig.add_subplot(131) >>> ax2 = fig.add_subplot(132) >>> ax3 = fig.add_subplot(133) >>> mesh.plot_image(val_centers, ax=ax1, clim=clim) >>> mesh.plot_image(val_interp, ax=ax2, clim=clim) >>> mesh.plot_image(val_centers-val_interp, ax=ax3, clim=clim) >>> ax1.set_title('Analytic at Centers') >>> ax2.set_title('Interpolated from Nodes') >>> ax3.set_title('Relative Error') >>> plt.show() """ npts = locs.shape[0] locs = locs.astype(float) x = x.astype(float) if y is None and z is None: shape = [x.size] inds, vals = _interpmat1D(mkvc(locs), x) elif z is None: y = y.astype(float) shape = [x.size, y.size] inds, vals = _interpmat2D(locs, x, y) else: y = y.astype(float) z = z.astype(float) shape = [x.size, y.size, z.size] inds, vals = _interpmat3D(locs, x, y, z) I = np.repeat(range(npts), 2 ** len(shape)) J = sub2ind(shape, inds) Q = sp.csr_matrix((vals, (I, J)), shape=(npts, np.prod(shape))) return Q def volume_average(mesh_in, mesh_out, values=None, output=None): """Volume averaging interpolation between meshes. This volume averaging function looks for overlapping cells in each mesh, and weights the output values by the partial volume ratio of the overlapping input cells. The volume average operation should result in an output such that ``np.sum(mesh_in.cell_volumes*values)`` = ``np.sum(mesh_out.cell_volumes*output)``, when the input and output meshes have the exact same extent. When the output mesh extent goes beyond the input mesh, it is assumed to have constant values in that direction. When the output mesh extent is smaller than the input mesh, only the overlapping extent of the input mesh contributes to the output. This function operates in three different modes. If only *mesh_in* and *mesh_out* are given, the returned value is a ``scipy.sparse.csr_matrix`` that represents this operation (so it could potentially be applied repeatedly). If *values* is given, the volume averaging is performed right away (without internally forming the matrix) and the returned value is the result of this. If *output* is given as well, it will be filled with the values of the operation and then returned (assuming it has the correct ``dtype``). Parameters ---------- mesh_in : ~discretize.TensorMesh or ~discretize.TreeMesh Input mesh (the mesh you are interpolating from) mesh_out : ~discretize.TensorMesh or ~discretize.TreeMesh Output mesh (the mesh you are interpolating to) values : (mesh_in.n_cells) numpy.ndarray, optional Array with values defined at the cells of ``mesh_in`` output : (mesh_out.n_cells) numpy.ndarray of float, optional Output array to be overwritten Returns ------- (mesh_out.n_cells, mesh_in.n_cells) scipy.sparse.csr_matrix or (mesh_out.n_cells) numpy.ndarray If *values* = *None* , the returned value is a matrix representing this operation, otherwise it is a :class:`numpy.ndarray` of the result of the operation. Examples -------- Create two meshes with the same extent, but different divisions (the meshes do not have to be the same extent). >>> import numpy as np >>> from discretize import TensorMesh >>> rng = np.random.default_rng(853) >>> h1 = np.ones(32) >>> h2 = np.ones(16)*2 >>> mesh_in = TensorMesh([h1, h1]) >>> mesh_out = TensorMesh([h2, h2]) Create a random model defined on the input mesh, and use volume averaging to interpolate it to the output mesh. >>> from discretize.utils import volume_average >>> model1 = rng.random(mesh_in.nC) >>> model2 = volume_average(mesh_in, mesh_out, model1) Because these two meshes' cells are perfectly aligned, but the output mesh has 1 cell for each 4 of the input cells, this operation should effectively look like averaging each of those cells values >>> import matplotlib.pyplot as plt >>> plt.figure(figsize=(6, 3)) >>> ax1 = plt.subplot(121) >>> mesh_in.plot_image(model1, ax=ax1) >>> ax2 = plt.subplot(122) >>> mesh_out.plot_image(model2, ax=ax2) >>> plt.show() """ try: in_type = mesh_in._meshType out_type = mesh_out._meshType except AttributeError: raise TypeError("Both input and output mesh must be valid discetize meshes") valid_meshs = ["TENSOR", "TREE"] if in_type not in valid_meshs or out_type not in valid_meshs: raise NotImplementedError( f"Volume averaging is only implemented for TensorMesh and TreeMesh, " f"not {type(mesh_in).__name__} and/or {type(mesh_out).__name__}" ) if mesh_in.dim != mesh_out.dim: raise ValueError("Both meshes must have the same dimension") if values is not None and len(values) != mesh_in.nC: raise ValueError( "Input array does not have the same length as the number of cells in input mesh" ) if output is not None and len(output) != mesh_out.nC: raise ValueError( "Output array does not have the same length as the number of cells in output mesh" ) if values is not None: values = np.asarray(values, dtype=np.float64) if output is not None: output = np.asarray(output, dtype=np.float64) if in_type == "TENSOR": if out_type == "TENSOR": return _vol_interp(mesh_in, mesh_out, values, output) elif out_type == "TREE": return mesh_out._vol_avg_from_tens(mesh_in, values, output) elif in_type == "TREE": if out_type == "TENSOR": return mesh_in._vol_avg_to_tens(mesh_out, values, output) elif out_type == "TREE": return mesh_out._vol_avg_from_tree(mesh_in, values, output) else: raise TypeError("Unsupported mesh types") interpmat = deprecate_function( interpolation_matrix, "interpmat", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/utils/interputils.py ================================================ from discretize.utils.interpolation_utils import * # NOQA F401,F403 raise ImportError( "Importing from discretize.interputils is deprecated behavoir. Please import " "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", ) ================================================ FILE: discretize/utils/io_utils.py ================================================ """Simple input/output routines.""" from urllib.request import urlretrieve import os import importlib import json def load_mesh(file_name): """Load discretize mesh saved to json file. For a discretize mesh that has been converted to dictionary and written to a json file, the function **load_mesh** loads the json file and reconstructs the mesh object. Parameters ---------- file_name : str Name of the json file being read in. Contains all information required to reconstruct the mesh. Returns ------- discretize.base.BaseMesh A discretize mesh defined by the class and parameters stored in the json file """ with open(file_name, "r") as outfile: jsondict = json.load(outfile) module_name = jsondict.pop( "__module__", "discretize" ) # default to loading from discretize class_name = jsondict.pop("__class__") mod = importlib.import_module(module_name) cls = getattr(mod, class_name) if "_n" in jsondict: jsondict["shape_cells"] = jsondict.pop( "_n" ) # need to catch this old _n property here data = cls(**jsondict) return data def download(url, folder=".", overwrite=False, verbose=True): """ Download file(s) stored in a cloud directory. Parameters ---------- url : str or list of str url or list of urls for the file(s) being downloaded folder : str, optional Local folder where downloaded files are to be stored overwrite : bool, optional Overwrite files if they have the same name as newly downloaded files verbose : bool, optional Print progress when downloading multiple files Returns ------- os.path or list of os.path The path or a list of paths for all downloaded files """ def rename_path(downloadpath): splitfullpath = downloadpath.split(os.path.sep) # grab just the file name fname = splitfullpath[-1] fnamesplit = fname.split(".") newname = fnamesplit[0] # check if we have already re-numbered newnamesplit = newname.split("(") # add (num) to the end of the file name if len(newnamesplit) == 1: num = 1 else: num = int(newnamesplit[-1][:-1]) num += 1 newname = "{}({}).{}".format(newnamesplit[0], num, fnamesplit[-1]) return os.path.sep.join(splitfullpath[:-1] + newnamesplit[:-1] + [newname]) # ensure we are working with absolute paths and home directories dealt with folder = os.path.abspath(os.path.expanduser(folder)) # make the directory if it doesn't currently exist if not os.path.exists(folder): os.makedirs(folder) if isinstance(url, str): file_names = [url.split("/")[-1]] elif isinstance(url, list): file_names = [u.split("/")[-1] for u in url] downloadpath = [os.path.sep.join([folder, f]) for f in file_names] # check if the directory already exists for i, download in enumerate(downloadpath): if os.path.exists(download): if overwrite: if verbose: print("overwriting {}".format(download)) else: while os.path.exists(download): download = rename_path(download) if verbose: print("file already exists, new file is called {}".format(download)) downloadpath[i] = download # download files urllist = url if isinstance(url, list) else [url] for u, f in zip(urllist, downloadpath): print("Downloading {}".format(u)) urlretrieve(u, f) print(" saved to: " + f) print("Download completed!") return downloadpath if isinstance(url, list) else downloadpath[0] ================================================ FILE: discretize/utils/matrix_utils.py ================================================ """Useful functions for working with vectors and matrices.""" import numpy as np import scipy.sparse as sp from discretize.utils.code_utils import is_scalar, deprecate_function import warnings def mkvc(x, n_dims=1, **kwargs): """Coerce a vector to the specified dimensionality. This function converts a :class:`numpy.ndarray` to a vector. In general, the output vector has a dimension of 1. However, the dimensionality can be specified if the user intends to carry out a dot product with a higher order array. Parameters ---------- x : array_like An array that will be reorganized and output as a vector. The input array will be flattened on input in Fortran order. n_dims : int The dimension of the output vector. :data:`numpy.newaxis` are appended to the output array until it has this many axes. Returns ------- numpy.ndarray The output vector, with at least ``n_dims`` axes. Examples -------- Here, we reorganize a simple 2D array as a vector and demonstrate the impact of the *n_dim* argument. >>> from discretize.utils import mkvc >>> import numpy as np >>> rng = np.random.default_rng(856) >>> a = rng.random(3, 2) >>> a array([[0.33534155, 0.25334363], [0.07147884, 0.81080958], [0.85892774, 0.74357806]]) >>> v = mkvc(a) >>> v array([0.33534155, 0.07147884, 0.85892774, 0.25334363, 0.81080958, 0.74357806]) In Higher dimensions: >>> for ii in range(1, 4): ... v = mkvc(a, ii) ... print('Shape of output with n_dim =', ii, ': ', v.shape) Shape of output with n_dim = 1 : (6,) Shape of output with n_dim = 2 : (6, 1) Shape of output with n_dim = 3 : (6, 1, 1) """ if "numDims" in kwargs: raise TypeError( "The numDims keyword argument has been removed, please use n_dims. " "This will be removed in discretize 1.0.0", ) if isinstance(x, np.matrix): x = np.array(x) if hasattr(x, "tovec"): x = x.tovec() if isinstance(x, Zero): return x if not isinstance(x, np.ndarray): raise TypeError("Vector must be a numpy array") if n_dims == 1: return x.flatten(order="F") elif n_dims == 2: return x.flatten(order="F")[:, np.newaxis] elif n_dims == 3: return x.flatten(order="F")[:, np.newaxis, np.newaxis] def sdiag(v): """Generate sparse diagonal matrix from a vector. This function creates a sparse diagonal matrix whose diagonal elements are defined by the input vector *v*. For a vector of length *n*, the output matrix has shape (n,n). Parameters ---------- v : (n) numpy.ndarray or discretize.utils.Zero The vector defining the diagonal elements of the sparse matrix being constructed Returns ------- (n, n) scipy.sparse.csr_matrix or discretize.utils.Zero The sparse diagonal matrix. Examples -------- Use a 1D array of values to construct a sparse diagonal matrix. >>> from discretize.utils import sdiag >>> import numpy as np >>> v = np.array([6., 3., 1., 8., 0., 5.]) >>> M = sdiag(v) """ if isinstance(v, Zero): return Zero() return sp.spdiags(mkvc(v), 0, v.size, v.size, format="csr") def sdinv(M): """Return inverse of a sparse diagonal matrix. This function extracts the diagonal elements of the input matrix *M* and creates a sparse diagonal matrix from the reciprocal these elements. If the input matrix *M* is diagonal, the output is the inverse of *M*. Parameters ---------- M : (n, n) scipy.sparse.csr_matrix A sparse diagonal matrix Returns ------- (n, n) scipy.sparse.csr_matrix The inverse of the sparse diagonal matrix. Examples -------- >>> from discretize.utils import sdiag, sdinv >>> import numpy as np >>> v = np.array([6., 3., 1., 8., 0., 5.]) >>> M = sdiag(v) >>> Minv = sdinv(M) """ return sdiag(1.0 / M.diagonal()) def speye(n): """Generate sparse identity matrix. Parameters ---------- n : int The dimensions of the sparse identity matrix. Returns ------- (n, n) scipy.sparse.csr_matrix The sparse identity matrix. """ return sp.identity(n, format="csr") def kron3(A, B, C): r"""Compute kronecker products between 3 sparse matricies. Where :math:`\otimes` denotes the Kronecker product and *A, B* and *C* are sparse matrices, this function outputs :math:`(A \otimes B) \otimes C`. Parameters ---------- A, B, C : scipy.sparse.spmatrix Sparse matrices. Returns ------- scipy.sparse.csr_matrix Kroneker between the 3 sparse matrices. """ return sp.kron(sp.kron(A, B), C, format="csr") def spzeros(n1, n2): """Generate sparse matrix of zeros of shape=(n1, n2). Parameters ---------- n1 : int Number of rows. n2 : int Number of columns. Returns ------- (n1, n2) scipy.sparse.dia_matrix A sparse matrix of zeros. """ return sp.dia_matrix((n1, n2)) def ddx(n): r"""Create 1D difference (derivative) operator from nodes to centers. For n cells, the 1D difference (derivative) operator from nodes to centers is sparse, has shape (n, n+1) and takes the form: .. math:: \begin{bmatrix} -1 & 1 & & & \\ & -1 & 1 & & \\ & & \ddots & \ddots & \\ & & & -1 & 1 \end{bmatrix} Parameters ---------- n : int Number of cells Returns ------- (n, n + 1) scipy.sparse.csr_matrix The 1D difference operator from nodes to centers. """ return sp.spdiags((np.ones((n + 1, 1)) * [-1, 1]).T, [0, 1], n, n + 1, format="csr") def av(n): r"""Create 1D averaging operator from nodes to cell-centers. For n cells, the 1D averaging operator from nodes to centerss is sparse, has shape (n, n+1) and takes the form: .. math:: \begin{bmatrix} 1/2 & 1/2 & & & \\ & 1/2 & 1/2 & & \\ & & \ddots & \ddots & \\ & & & 1/2 & 1/2 \end{bmatrix} Parameters ---------- n : int Number of cells Returns ------- (n, n + 1) scipy.sparse.csr_matrix The 1D averaging operator from nodes to centers. """ return sp.spdiags( (0.5 * np.ones((n + 1, 1)) * [1, 1]).T, [0, 1], n, n + 1, format="csr" ) def av_extrap(n): r"""Create 1D averaging operator from cell-centers to nodes. For n cells, the 1D averaging operator from cell centers to nodes is sparse and has shape (n+1, n). Values at the outmost nodes are extrapolated from the nearest cell center value. Thus the operator takes the form: .. math:: \begin{bmatrix} 1 & & & & \\ 1/2 & 1/2 & & & \\ & 1/2 & 1/2 & & & \\ & & \ddots & \ddots & \\ & & & 1/2 & 1/2 \\ & & & & 1 \end{bmatrix} Parameters ---------- n : int Number of cells Returns ------- (n+1, n) scipy.sparse.csr_matrix The 1D averaging operator from cell-centers to nodes. """ Av = sp.spdiags( (0.5 * np.ones((n, 1)) * [1, 1]).T, [-1, 0], n + 1, n, format="csr" ) + sp.csr_matrix(([0.5, 0.5], ([0, n], [0, n - 1])), shape=(n + 1, n)) return Av def ndgrid(*args, vector=True, order="F"): """Generate gridded locations for 1D, 2D, or 3D tensors. For 1D, 2D, or 3D tensors, this function takes the unique positions defining a tensor along each of its axis and returns the gridded locations. For 2D and 3D meshes, the user may treat the unique *x*, *y* (and *z*) positions a successive positional arguments or as a single argument using a list [*x*, *y*, (*z*)]. For outputs, let *dim* be the number of dimension (1, 2 or 3) and let *n* be the total number of gridded locations. The gridded *x*, *y* (and *z*) locations can be return as a single numpy array of shape [n, ndim]. The user can also return the gridded *x*, *y* (and *z*) locations as a list of length *ndim*. The list contains entries contain the *x*, *y* (and *z*) locations as tensors. See examples. Parameters ---------- *args : (n, dim) numpy.ndarray or (dim) list of (n) numpy.ndarray Positions along each axis of the tensor. The user can define these as successive positional arguments *x*, *y*, (and *z*) or as a single argument using a list [*x*, *y*, (*z*)]. vector : bool, optional If *True*, the output is a numpy array of dimension [n, ndim]. If *False*, the gridded x, y (and z) locations are returned as separate ndarrays in a list. Default is *True*. order : {'F', 'C', 'A'} Define ordering using one of the following options: 'C' is C-like ordering, 'F' is Fortran-like ordering, 'A' is Fortran ordering if memory is contigious and C-like otherwise. Default = 'F'. See :func:`numpy.reshape` for more on this argument. Returns ------- numpy.ndarray or list of numpy.ndarray If *vector* = *True* the gridded *x*, *y*, (and *z*) locations are returned as a numpy array of shape [n, ndim]. If *vector* = *False*, the gridded *x*, *y*, (and *z*) are returned as a list of vectors. Examples -------- >>> from discretize.utils import ndgrid >>> import numpy as np >>> x = np.array([1, 2, 3]) >>> y = np.array([2, 4]) >>> ndgrid([x, y]) array([[1, 2], [2, 2], [3, 2], [1, 4], [2, 4], [3, 4]]) >>> ndgrid(x, y, order='C') array([[1, 2], [1, 4], [2, 2], [2, 4], [3, 2], [3, 4]]) >>> ndgrid(x, y, vector=False) [array([[1, 1], [2, 2], [3, 3]]), array([[2, 4], [2, 4], [2, 4]])] """ # Read the keyword arguments, and only accept a vector=True/False if not isinstance(vector, bool): raise TypeError("'vector' keyword must be a bool") # you can either pass a list [x1, x2, x3] or each seperately if isinstance(args[0], list): xin = args[0] else: xin = args # Each vector needs to be a numpy array try: if len(xin) == 1: return np.array(xin[0]) meshed = np.meshgrid(*xin, indexing="ij") except Exception: raise TypeError("All arguments must be array like") if vector: return np.column_stack([x.reshape(-1, order=order) for x in meshed]) return meshed def make_boundary_bool(shape, bdir="xyz", **kwargs): r"""Return boundary indices of a tensor grid. For a tensor grid whose shape is given (1D, 2D or 3D), this function returns a boolean index array identifying the x, y and/or z boundary locations. Parameters ---------- shape : (dim) tuple of int Defines the shape of the tensor (1D, 2D or 3D). bdir : str containing characters 'x', 'y' and/or 'z' Specify the boundaries whose indices you want returned; e.g. for a 3D tensor, you may set *dir* = 'xz' to return the indices of the x and z boundary locations. Returns ------- numpy.ndarray of bool Indices of boundary locations of the tensor for specified boundaries. The returned order matches the order the items occur in the flattened ``ndgrid`` Examples -------- Here we construct a 3x3 tensor and find the indices of the boundary locations. >>> from discretize.utils.matrix_utils import ndgrid, make_boundary_bool >>> import numpy as np Define a 3x3 tensor grid >>> x = np.array([1, 2, 3]) >>> y = np.array([2, 4, 6]) >>> tensor_grid = ndgrid(x, y) Find indices of boundary locations. >>> shape = (len(x), len(y)) >>> bool_ind = make_boundary_bool(shape) >>> tensor_grid[bool_ind] array([[1, 2], [2, 2], [3, 2], [1, 4], [3, 4], [1, 6], [2, 6], [3, 6]]) Find indices of locations of only the x boundaries, >>> bool_ind_x = make_boundary_bool(shape, 'x') >>> tensor_grid[bool_ind_x] array([[1, 2], [3, 2], [1, 4], [3, 4], [1, 6], [3, 6]]) """ old_dir = kwargs.pop("dir", None) if old_dir is not None: warnings.warn( "The `dir` keyword argument has been renamed to `bdir` to avoid shadowing the " "builtin variable `dir`. This will be removed in discretize 1.0.0", FutureWarning, stacklevel=2, ) bdir = old_dir is_b = np.zeros(shape, dtype=bool, order="F") if "x" in bdir: is_b[[0, -1]] = True if len(shape) > 1: if "y" in bdir: is_b[:, [0, -1]] = True if len(shape) > 2: if "z" in bdir: is_b[:, :, [0, -1]] = True return is_b.reshape(-1, order="F") def ind2sub(shape, inds): r"""Return subscripts of tensor grid elements from indices. This function is a wrapper for :func:`numpy.unravel_index` with a hard-coded Fortran order. Consider the :math:`n^{th}` element of a tensor grid with *N* elements. The position of this element in the tensor grid can also be defined by subscripts (i,j,k). For an array containing the indices for a set of tensor elements, this function returns the corresponding subscripts. Parameters ---------- shape : (dim) tuple of int Defines the shape of the tensor (1D, 2D or 3D). inds : array_like of int The indices of the tensor elements whose subscripts you want returned. Returns ------- (dim) tuple of numpy.ndarray Corresponding subscipts for the indices provided. The output is a tuple containing 1D integer arrays for the i, j and k subscripts, respectively. The output array will match the shape of the **inds** input. See Also -------- numpy.unravel_index """ return np.unravel_index(inds, shape, order="F") def sub2ind(shape, subs): r"""Return indices of tensor grid elements from subscripts. This function is a wrapper for :func:`numpy.ravel_multi_index` with a hard-coded Fortran order, and a column order for the ``multi_index`` Consider elements of a tensors grid whose positions are given by the subscripts (i,j,k). This function will return the corresponding indices of these elements. Each row of the input array *subs* defines the ijk for a particular tensor element. Parameters ---------- shape : (dim) tuple of int Defines the shape of the tensor (1D, 2D or 3D). subs : (N, dim) array_like of int The subscripts of the tensor grid elements. Each rows defines the position of a particular tensor element. The shape of of the array is (N, ndim). Returns ------- numpy.ndarray of int The indices of the tensor grid elements defined by *subs*. See Also -------- numpy.ravel_multi_index Examples -------- He we recreate the examples from :func:`numpy.ravel_multi_index` to illustrate the differences. The indices corresponding to each dimension are now columns in the array (instead of rows), and it assumed to use a Fortran order. >>> import numpy as np >>> from discretize.utils import sub2ind >>> arr = np.array([[3, 4], [6, 5], [6, 1]]) >>> sub2ind((7, 6), arr) array([31, 41, 13], dtype=int64) """ if len(shape) == 1: return subs subs = np.atleast_2d(subs) if subs.shape[1] != len(shape): raise ValueError( "Indexing must be done as a column vectors. e.g. [[3,6],[6,2],...]" ) inds = np.ravel_multi_index(subs.T, shape, order="F") return mkvc(inds) def get_subarray(A, ind): """Extract a subarray. For a :class:`numpy.ndarray`, the function **get_subarray** extracts a subset of the array. The portion of the original array being extracted is defined by providing the indices along each axis. Parameters ---------- A : numpy.ndarray The original numpy array. Must be 1, 2 or 3 dimensions. ind : (dim) list of numpy.ndarray A list of numpy arrays containing the indices being extracted along each dimension. The length of the list must equal the dimensions of the input array. Returns ------- numpy.ndarray The subarray extracted from the original array Examples -------- Here we construct a random 3x3 numpy array and use **get_subarray** to extract the first column. >>> from discretize.utils import get_subarray >>> import numpy as np >>> rng = np.random.default_rng(421) >>> A = rng.random((3, 3)) >>> A array([[1.07969034e-04, 9.78613931e-01, 6.62123429e-01], [8.80722877e-01, 7.61035691e-01, 7.42546796e-01], [9.09488911e-01, 7.80626334e-01, 8.67663825e-01]]) Define the indexing along the columns and rows and create the indexing list >>> ind_x = np.array([0, 1, 2]) >>> ind_y = np.array([0, 2]) >>> ind = [ind_x, ind_y] Extract the first, and third column of A >>> get_subarray(A, ind) array([[1.07969034e-04, 6.62123429e-01], [8.80722877e-01, 7.42546796e-01], [9.09488911e-01, 8.67663825e-01]]) """ if not isinstance(ind, list): raise TypeError("ind must be a list of vectors") if len(A.shape) != len(ind): raise ValueError("ind must have the same length as the dimension of A") if len(A.shape) == 2: return A[ind[0], :][:, ind[1]] elif len(A.shape) == 3: return A[ind[0], :, :][:, ind[1], :][:, :, ind[2]] else: raise Exception("get_subarray does not support dimension asked.") def inverse_3x3_block_diagonal( a11, a12, a13, a21, a22, a23, a31, a32, a33, return_matrix=True, **kwargs ): r"""Invert a set of 3x3 matricies from vectors containing their elements. Parameters ---------- a11, a12, ..., a33 : (n_blocks) numpy.ndarray Vectors which contain the corresponding element for all 3x3 matricies return_matrix : bool, optional - **True**: Returns the sparse block 3x3 matrix *M* (default). - **False:** Returns the vectors containing the elements of each matrix' inverse. Returns ------- (3 * n_blocks, 3 * n_blocks) scipy.sparse.coo_matrix or list of (n_blocks) numpy.ndarray. If *return_matrix = False*, the function will return vectors *b11, b12, b13, b21, b22, b23, b31, b32, b33*. If *return_matrix = True*, the function will return the block matrix *M*. Notes ----- The elements of a 3x3 matrix *A* are given by: .. math:: A = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{bmatrix} For a set of 3x3 matricies, the elements may be stored in a set of 9 distinct vectors :math:`\mathbf{a_{11}}`, :math:`\mathbf{a_{12}}`, ..., :math:`\mathbf{a_{33}}`. For each matrix, **inverse_3x3_block_diagonal** ouputs the vectors containing the elements of each matrix' inverse; i.e. :math:`\mathbf{b_{11}}`, :math:`\mathbf{b_{12}}`, ..., :math:`\mathbf{b_{33}}` where: .. math:: A^{-1} = B = \begin{bmatrix} b_{11} & b_{12} & b_{13} \\ b_{21} & b_{22} & b_{23} \\ b_{31} & b_{32} & b_{33} \end{bmatrix} For special applications, we may want to output the elements of the inverses of the matricies as a 3x3 block matrix of the form: .. math:: M = \begin{bmatrix} D_{11} & D_{12} & D_{13} \\ D_{21} & D_{22} & D_{23} \\ D_{31} & D_{32} & D_{33} \end{bmatrix} where :math:`D_{ij}` are diagonal matrices whose non-zero elements are defined by vector :math:`\\mathbf{b_{ij}}`. Where *n* is the number of matricies, the block matrix is sparse with dimensions (3n, 3n). Examples -------- Here, we define four 3x3 matricies and reorganize their elements into 9 vectors a11, a12, ..., a33. We then examine the outputs of the function **inverse_3x3_block_diagonal** when the argument *return_matrix* is set to both *True* and *False*. >>> from discretize.utils import inverse_3x3_block_diagonal >>> import numpy as np >>> import scipy as sp >>> import matplotlib.pyplot as plt Define four 3x3 matricies, and organize their elements into nine vectors >>> A1 = np.random.uniform(1, 10, (3, 3)) >>> A2 = np.random.uniform(1, 10, (3, 3)) >>> A3 = np.random.uniform(1, 10, (3, 3)) >>> A4 = np.random.uniform(1, 10, (3, 3)) >>> [[a11, a12, a13], [a21, a22, a23], [a31, a32, a33]] = np.stack( ... [A1, A2, A3, A4], axis=-1 ... ) Return the elements of their inverse and validate >>> b11, b12, b13, b21, b22, b23, b31, b32, b33 = inverse_3x3_block_diagonal( ... a11, a12, a13, a21, a22, a23, a31, a32, a33, return_matrix=False ... ) >>> Bs = np.stack([[b11, b12, b13],[b21, b22, b23],[b31, b32, b33]]) >>> B1, B2, B3, B4 = Bs.transpose((2, 0, 1)) >>> np.linalg.inv(A1) array([[ 0.20941584, 0.18477151, -0.22637147], [-0.06420656, -0.34949639, 0.29216461], [-0.14226339, 0.11160555, 0.0907583 ]]) >>> B1 array([[ 0.20941584, 0.18477151, -0.22637147], [-0.06420656, -0.34949639, 0.29216461], [-0.14226339, 0.11160555, 0.0907583 ]]) We can also return this as a sparse matrix with block diagonal inverse >>> M = inverse_3x3_block_diagonal( ... a11, a12, a13, a21, a22, a23, a31, a32, a33 ... ) >>> plt.spy(M) >>> plt.show() """ if "returnMatrix" in kwargs: warnings.warn( "The returnMatrix keyword argument has been deprecated, please use return_matrix. " "This will be removed in discretize 1.0.0", FutureWarning, stacklevel=2, ) return_matrix = kwargs["returnMatrix"] a11 = mkvc(a11) a12 = mkvc(a12) a13 = mkvc(a13) a21 = mkvc(a21) a22 = mkvc(a22) a23 = mkvc(a23) a31 = mkvc(a31) a32 = mkvc(a32) a33 = mkvc(a33) detA = ( a31 * a12 * a23 - a31 * a13 * a22 - a21 * a12 * a33 + a21 * a13 * a32 + a11 * a22 * a33 - a11 * a23 * a32 ) b11 = +(a22 * a33 - a23 * a32) / detA b12 = -(a12 * a33 - a13 * a32) / detA b13 = +(a12 * a23 - a13 * a22) / detA b21 = +(a31 * a23 - a21 * a33) / detA b22 = -(a31 * a13 - a11 * a33) / detA b23 = +(a21 * a13 - a11 * a23) / detA b31 = -(a31 * a22 - a21 * a32) / detA b32 = +(a31 * a12 - a11 * a32) / detA b33 = -(a21 * a12 - a11 * a22) / detA if not return_matrix: return b11, b12, b13, b21, b22, b23, b31, b32, b33 return sp.vstack( ( sp.hstack((sdiag(b11), sdiag(b12), sdiag(b13))), sp.hstack((sdiag(b21), sdiag(b22), sdiag(b23))), sp.hstack((sdiag(b31), sdiag(b32), sdiag(b33))), ) ) def inverse_2x2_block_diagonal(a11, a12, a21, a22, return_matrix=True, **kwargs): r""" Invert a set of 2x2 matricies from vectors containing their elements. Parameters ---------- a11, a12, a21, a22 : (n_blocks) numpy.ndarray All arguments a11, a12, a21, a22 are vectors which contain the corresponding element for all 2x2 matricies return_matrix : bool, optional - **True:** Returns the sparse block 2x2 matrix *M*. - **False:** Returns the vectors containing the elements of each matrix' inverse. Returns ------- (2 * n_blocks, 2 * n_blocks) scipy.sparse.coo_matrix or list of (n_blocks) numpy.ndarray If *return_matrix = False*, the function will return vectors *b11, b12, b21, b22*. If *return_matrix = True*, the function will return the block matrix *M* Notes ----- The elements of a 2x2 matrix *A* are given by: .. math:: A = \begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{bmatrix} For a set of 2x2 matricies, the elements may be stored in a set of 4 distinct vectors :math:`\mathbf{a_{11}}`, :math:`\mathbf{a_{12}}`, :math:`\mathbf{a_{21}}` and :math:`\mathbf{a_{22}}`. For each matrix, **inverse_2x2_block_diagonal** ouputs the vectors containing the elements of each matrix' inverse; i.e. :math:`\mathbf{b_{11}}`, :math:`\mathbf{b_{12}}`, :math:`\mathbf{b_{21}}` and :math:`\mathbf{b_{22}}` where: .. math:: A^{-1} = B = \begin{bmatrix} b_{11} & b_{12} \\ b_{21} & b_{22} \end{bmatrix} For special applications, we may want to output the elements of the inverses of the matricies as a 2x2 block matrix of the form: .. math:: M = \begin{bmatrix} D_{11} & D_{12} \\ D_{21} & D_{22} \end{bmatrix} where :math:`D_{ij}` are diagonal matrices whose non-zero elements are defined by vector :math:`\mathbf{b_{ij}}`. Where *n* is the number of matricies, the block matrix is sparse with dimensions (2n, 2n). Examples -------- Here, we define four 2x2 matricies and reorganize their elements into 4 vectors a11, a12, a21 and a22. We then examine the outputs of the function **inverse_2x2_block_diagonal** when the argument *return_matrix* is set to both *True* and *False*. >>> from discretize.utils import inverse_2x2_block_diagonal >>> import numpy as np >>> import matplotlib.pyplot as plt Define four 3x3 matricies, and organize their elements into four vectors >>> A1 = np.random.uniform(1, 10, (2, 2)) >>> A2 = np.random.uniform(1, 10, (2, 2)) >>> A3 = np.random.uniform(1, 10, (2, 2)) >>> A4 = np.random.uniform(1, 10, (2, 2)) >>> [[a11, a12], [a21, a22]] = np.stack([A1, A2, A3, A4], axis=-1) Return the elements of their inverse and validate >>> b11, b12, b21, b22 = inverse_2x2_block_diagonal( ... a11, a12, a21, a22, return_matrix=False ... ) >>> Bs = np.stack([[b11, b12],[b21, b22]]) >>> B1, B2, B3, B4 = Bs.transpose((2, 0, 1)) >>> np.linalg.inv(A1) array([[ 0.34507439, -0.4831833 ], [-0.24286626, 0.57531461]]) >>> B1 array([[ 0.34507439, -0.4831833 ], [-0.24286626, 0.57531461]]) Plot the sparse block matrix containing elements of the inverses >>> M = inverse_2x2_block_diagonal( ... a11, a12, a21, a22 ... ) >>> plt.spy(M) >>> plt.show() """ if "returnMatrix" in kwargs: warnings.warn( "The returnMatrix keyword argument has been deprecated, please use return_matrix. " "This will be removed in discretize 1.0.0", FutureWarning, stacklevel=2, ) return_matrix = kwargs["returnMatrix"] a11 = mkvc(a11) a12 = mkvc(a12) a21 = mkvc(a21) a22 = mkvc(a22) # compute inverse of the determinant. detAinv = 1.0 / (a11 * a22 - a21 * a12) b11 = +detAinv * a22 b12 = -detAinv * a12 b21 = -detAinv * a21 b22 = +detAinv * a11 if not return_matrix: return b11, b12, b21, b22 return sp.vstack( (sp.hstack((sdiag(b11), sdiag(b12))), sp.hstack((sdiag(b21), sdiag(b22)))) ) def invert_blocks(A): """Invert a set of 2x2 or 3x3 matricies. This is a shortcut function that will only invert 2x2 and 3x3 matrices. The function is broadcast over the last two dimensions of A. Parameters ---------- A : (..., N, N) numpy.ndarray the block of matrices to invert, N must be either 2 or 3. Returns ------- (..., N, N) numpy.ndarray the block of inverted matrices See Also -------- numpy.linalg.inv : Similar to this function, but is not specialized to 2x2 or 3x3 inverse_2x2_block_diagonal : use when each element of the blocks is separated inverse_3x3_block_diagonal : use when each element of the blocks is separated Examples -------- >>> from discretize.utils import invert_blocks >>> import numpy as np >>> x = np.ones((1000, 3, 3)) >>> x[..., 1, 1] = 0 >>> x[..., 1, 2] = 0 >>> x[..., 2, 1] = 0 >>> As = np.einsum('...ij,...jk', x, x.transpose(0, 2, 1)) >>> Ainvs = invert_blocks(As) >>> As[0] @ Ainvs[0] array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) """ if A.shape[-1] != A.shape[-2]: raise ValueError(f"Last two dimensions are not equal, got {A.shape}") if A.shape[-1] == 2: a11 = A[..., 0, 0] a12 = A[..., 0, 1] a21 = A[..., 1, 0] a22 = A[..., 1, 1] detA = a11 * a22 - a21 * a12 B = np.empty_like(A) B[..., 0, 0] = a22 / detA B[..., 0, 1] = -a12 / detA B[..., 1, 0] = -a21 / detA B[..., 1, 1] = a11 / detA elif A.shape[-1] == 3: a11 = A[..., 0, 0] a12 = A[..., 0, 1] a13 = A[..., 0, 2] a21 = A[..., 1, 0] a22 = A[..., 1, 1] a23 = A[..., 1, 2] a31 = A[..., 2, 0] a32 = A[..., 2, 1] a33 = A[..., 2, 2] B = np.empty_like(A) B[..., 0, 0] = a22 * a33 - a23 * a32 B[..., 0, 1] = a13 * a32 - a12 * a33 B[..., 0, 2] = a12 * a23 - a13 * a22 B[..., 1, 0] = a31 * a23 - a21 * a33 B[..., 1, 1] = a11 * a33 - a31 * a13 B[..., 1, 2] = a21 * a13 - a11 * a23 B[..., 2, 0] = a21 * a32 - a31 * a22 B[..., 2, 1] = a31 * a12 - a11 * a32 B[..., 2, 2] = a11 * a22 - a21 * a12 detA = a11 * B[..., 0, 0] + a21 * B[..., 0, 1] + a31 * B[..., 0, 2] B /= detA[..., None, None] else: raise NotImplementedError("Only supports 2x2 and 3x3 blocks") return B class TensorType(object): r"""Class for determining property tensor type. For a given *mesh*, the **TensorType** class examines the :class:`numpy.ndarray` *tensor* to determine whether *tensor* defines a scalar, isotropic, diagonal anisotropic or full tensor anisotropic constitutive relationship for each cell on the mesh. The general theory behind this functionality is explained below. Parameters ---------- mesh : discretize.base.BaseTensorMesh An instance of any of the mesh classes support in discretize; i.e. *TensorMesh*, *CylindricalMesh*, *TreeMesh* or *CurvilinearMesh*. tensor : numpy.ndarray or a float The shape of the input argument *tensor* must fall into one of these classifications: - *Scalar:* A float is entered. - *Isotropic:* A 1D numpy.ndarray with a property value for every cell. - *Anisotropic:* A (*nCell*, *dim*) numpy.ndarray of shape where each row defines the diagonal-anisotropic property parameters for each cell. *nParam* = 2 for 2D meshes and *nParam* = 3 for 3D meshes. - *Tensor:* A (*nCell*, *nParam*) numpy.ndarray where each row defines the full anisotropic property parameters for each cell. *nParam* = 3 for 2D meshes and *nParam* = 6 for 3D meshes. Notes ----- The relationship between a quantity and its response to external stimuli (e.g. Ohm's law) can be defined by a scalar quantity: .. math:: \vec{j} = \sigma \vec{e} Or in the case of anisotropy, the relationship is defined generally by a symmetric tensor: .. math:: \vec{j} = \Sigma \vec{e} \;\;\; where \;\;\; \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ \sigma_{xy} & \sigma_{yy} & \sigma_{yz} \\ \sigma_{xz} & \sigma_{yz} & \sigma_{zz} \end{bmatrix} In 3D, the tensor is defined by 6 independent element (3 independent elements in 2D). When using the input argument *tensor* to define the consitutive relationship for every cell in the *mesh*, there are 4 classifications recognized by discretize: - **Scalar:** :math:`\vec{j} = \sigma \vec{e}`, where :math:`\sigma` a constant. Thus the input argument *tensor* is a float. - **Isotropic:** :math:`\vec{j} = \sigma \vec{e}`, where :math:`\sigma` varies spatially. Thus the input argument *tensor* is a 1D array that provides a :math:`\sigma` value for every cell in the mesh. - **Anisotropic:** :math:`\vec{j} = \Sigma \vec{e}`, where the off-diagonal elements are zero. That is, :math:`\Sigma` is diagonal. In this case, the input argument *tensor* defining the physical properties in each cell is a :class:`numpy.ndarray` of shape (*nCells*, *dim*). - **Tensor:** :math:`\vec{j} = \Sigma \vec{e}`, where off-diagonal elements are non-zero and :math:`\Sigma` is a full tensor. In this case, the input argument *tensor* defining the physical properties in each cell is a :class:`numpy.ndarray` of shape (*nCells*, *nParam*). In 2D, *nParam* = 3 and in 3D, *nParam* = 6. """ def __init__(self, mesh, tensor): if tensor is None: # default is ones self._tt = -1 self._tts = "none" elif is_scalar(tensor): self._tt = 0 self._tts = "scalar" elif tensor.size == mesh.nC: self._tt = 1 self._tts = "isotropic" elif (mesh.dim == 2 and tensor.size == mesh.nC * 2) or ( mesh.dim == 3 and tensor.size == mesh.nC * 3 ): self._tt = 2 self._tts = "anisotropic" elif (mesh.dim == 2 and tensor.size == mesh.nC * 3) or ( mesh.dim == 3 and tensor.size == mesh.nC * 6 ): self._tt = 3 self._tts = "tensor" else: raise Exception("Unexpected shape of tensor: {}".format(tensor.shape)) def __str__(self): """Represent tensor type as a string.""" return "TensorType[{0:d}]: {1!s}".format(self._tt, self._tts) def __eq__(self, v): """Compare tensor type equal to a value.""" return self._tt == v def __le__(self, v): """Compare tensor type less than or equal to a value.""" return self._tt <= v def __ge__(self, v): """Compare tensor type greater than or equal to a value.""" return self._tt >= v def __lt__(self, v): """Compare tensor type less than a value.""" return self._tt < v def __gt__(self, v): """Compare tensor type greater than a value.""" return self._tt > v def make_property_tensor(mesh, tensor): r"""Construct the physical property tensor. For a given *mesh*, the input parameter *tensor* is a :class:`numpy.ndarray` defining the constitutive relationship (e.g. Ohm's law) between two discrete vector quantities :math:`\boldsymbol{j}` and :math:`\boldsymbol{e}` living at cell centers. The function **make_property_tensor** constructs the property tensor :math:`\boldsymbol{M}` for the entire mesh such that: >>> j = M @ e where the Cartesian components of the discrete vector for are organized according to: >>> e = np.r_[ex, ey, ez] >>> j = np.r_[jx, jy, jz] Parameters ---------- mesh : discretize.base.BaseMesh A mesh tensor : numpy.ndarray or a float - *Scalar:* A float is entered. - *Isotropic:* A 1D numpy.ndarray with a property value for every cell. - *Anisotropic:* A (*nCell*, *dim*) numpy.ndarray where each row defines the diagonal-anisotropic property parameters for each cell. *nParam* = 2 for 2D meshes and *nParam* = 3 for 3D meshes. - *Tensor:* A (*nCell*, *nParam*) numpy.ndarray where each row defines the full anisotropic property parameters for each cell. *nParam* = 3 for 2D meshes and *nParam* = 6 for 3D meshes. Returns ------- (dim * n_cells, dim * n_cells) scipy.sparse.coo_matrix The property tensor. Notes ----- The relationship between a quantity and its response to external stimuli (e.g. Ohm's law) in each cell can be defined by a scalar function :math:`\sigma` in the isotropic case, or by a tensor :math:`\Sigma` in the anisotropic case, i.e.: .. math:: \vec{j} = \sigma \vec{e} \;\;\;\;\;\; \textrm{or} \;\;\;\;\;\; \vec{j} = \Sigma \vec{e} where .. math:: \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ \sigma_{xy} & \sigma_{yy} & \sigma_{yz} \\ \sigma_{xz} & \sigma_{yz} & \sigma_{zz} \end{bmatrix} Examples -------- For the 4 classifications allowable (scalar, isotropic, anistropic and tensor), we construct and compare the property tensor on a small 2D mesh. For this example, note the following: - The dimensions for all property tensors are the same - All property tensors, except in the case of full anisotropy are diagonal sparse matrices - For the scalar case, the non-zero elements are equal - For the isotropic case, the non-zero elements repreat in order for the x, y (and z) components - For the anisotropic case (diagonal anisotropy), the non-zero elements do not repeat - For the tensor caes (full anisotropy), there are off-diagonal components >>> from discretize.utils import make_property_tensor >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl >>> rng = np.random.default_rng(421) Define a 2D tensor mesh >>> h = [1., 1., 1.] >>> mesh = TensorMesh([h, h], origin='00') Define a physical property for all cases (2D) >>> sigma_scalar = 4. >>> sigma_isotropic = rng.integers(1, 10, mesh.nC) >>> sigma_anisotropic = rng.integers(1, 10, (mesh.nC, 2)) >>> sigma_tensor = rng.integers(1, 10, (mesh.nC, 3)) Construct the property tensor in each case >>> M_scalar = make_property_tensor(mesh, sigma_scalar) >>> M_isotropic = make_property_tensor(mesh, sigma_isotropic) >>> M_anisotropic = make_property_tensor(mesh, sigma_anisotropic) >>> M_tensor = make_property_tensor(mesh, sigma_tensor) Plot the property tensors. >>> M_list = [M_scalar, M_isotropic, M_anisotropic, M_tensor] >>> case_list = ['Scalar', 'Isotropic', 'Anisotropic', 'Full Tensor'] >>> ax1 = 4*[None] >>> fig = plt.figure(figsize=(15, 4)) >>> for ii in range(0, 4): ... ax1[ii] = fig.add_axes([0.05+0.22*ii, 0.05, 0.18, 0.9]) ... ax1[ii].imshow( ... M_list[ii].todense(), interpolation='none', cmap='binary', vmax=10. ... ) ... ax1[ii].set_title(case_list[ii], fontsize=24) >>> ax2 = fig.add_axes([0.92, 0.15, 0.01, 0.7]) >>> norm = mpl.colors.Normalize(vmin=0., vmax=10.) >>> cbar = mpl.colorbar.ColorbarBase( ... ax2, norm=norm, orientation="vertical", cmap=mpl.cm.binary ... ) >>> plt.show() """ if tensor is None: # default is ones tensor = np.ones(mesh.nC) if is_scalar(tensor): tensor = tensor * np.ones(mesh.nC) propType = TensorType(mesh, tensor) if propType == 1: # Isotropic! Sigma = sp.kron(sp.identity(mesh.dim), sdiag(mkvc(tensor))) elif propType == 2: # Diagonal tensor Sigma = sdiag(mkvc(tensor)) elif mesh.dim == 2 and tensor.size == mesh.nC * 3: # Fully anisotropic, 2D tensor = tensor.reshape((mesh.nC, 3), order="F") row1 = sp.hstack((sdiag(tensor[:, 0]), sdiag(tensor[:, 2]))) row2 = sp.hstack((sdiag(tensor[:, 2]), sdiag(tensor[:, 1]))) Sigma = sp.vstack((row1, row2)) elif mesh.dim == 3 and tensor.size == mesh.nC * 6: # Fully anisotropic, 3D tensor = tensor.reshape((mesh.nC, 6), order="F") row1 = sp.hstack( (sdiag(tensor[:, 0]), sdiag(tensor[:, 3]), sdiag(tensor[:, 4])) ) row2 = sp.hstack( (sdiag(tensor[:, 3]), sdiag(tensor[:, 1]), sdiag(tensor[:, 5])) ) row3 = sp.hstack( (sdiag(tensor[:, 4]), sdiag(tensor[:, 5]), sdiag(tensor[:, 2])) ) Sigma = sp.vstack((row1, row2, row3)) else: raise Exception("Unexpected shape of tensor") return Sigma def inverse_property_tensor(mesh, tensor, return_matrix=False, **kwargs): r"""Construct the inverse of the physical property tensor. For a given *mesh*, the input parameter *tensor* is a :class:`numpy.ndarray` defining the constitutive relationship (e.g. Ohm's law) between two discrete vector quantities :math:`\boldsymbol{j}` and :math:`\boldsymbol{e}` living at cell centers. Where :math:`\boldsymbol{M}` is the physical property tensor, **inverse_property_tensor** explicitly constructs the inverse of the physical property tensor :math:`\boldsymbol{M^{-1}}` for all cells such that: >>> e = Mi @ j where the Cartesian components of the discrete vectors are organized according to: >>> j = np.r_[jx, jy, jz] >>> e = np.r_[ex, ey, ez] Parameters ---------- mesh : discretize.base.BaseMesh A mesh tensor : numpy.ndarray or float - *Scalar:* A float is entered. - *Isotropic:* A 1D numpy.ndarray with a property value for every cell. - *Anisotropic:* A (*nCell*, *dim*) numpy.ndarray where each row defines the diagonal-anisotropic property parameters for each cell. *nParam* = 2 for 2D meshes and *nParam* = 3 for 3D meshes. - *Tensor:* A (*nCell*, *nParam*) numpy.ndarray where each row defines the full anisotropic property parameters for each cell. *nParam* = 3 for 2D meshes and *nParam* = 6 for 3D meshes. return_matrix : bool, optional - *True:* the function returns the inverse of the property tensor. - *False:* the function returns the non-zero elements of the inverse of the property tensor in a numpy.ndarray in the same order as the input argument *tensor*. Returns ------- numpy.ndarray or scipy.sparse.coo_matrix - If *return_matrix* = *False*, the function outputs the parameters defining the inverse of the property tensor in a numpy.ndarray with the same dimensions as the input argument *tensor* - If *return_natrix* = *True*, the function outputs the inverse of the property tensor as a *scipy.sparse.coo_matrix*. Notes ----- The relationship between a quantity and its response to external stimuli (e.g. Ohm's law) in each cell can be defined by a scalar function :math:`\sigma` in the isotropic case, or by a tensor :math:`\Sigma` in the anisotropic case, i.e.: .. math:: \vec{j} = \sigma \vec{e} \;\;\;\;\;\; \textrm{or} \;\;\;\;\;\; \vec{j} = \Sigma \vec{e} where .. math:: \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ \sigma_{xy} & \sigma_{yy} & \sigma_{yz} \\ \sigma_{xz} & \sigma_{yz} & \sigma_{zz} \end{bmatrix} Examples -------- For the 4 classifications allowable (scalar, isotropic, anistropic and tensor), we construct the property tensor on a small 2D mesh. We then construct the inverse of the property tensor and compare. >>> from discretize.utils import make_property_tensor, inverse_property_tensor >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl >>> rng = np.random.default_rng(421) Define a 2D tensor mesh >>> h = [1., 1., 1.] >>> mesh = TensorMesh([h, h], origin='00') Define a physical property for all cases (2D) >>> sigma_scalar = 4. >>> sigma_isotropic = rng.integers(1, 10, mesh.nC) >>> sigma_anisotropic = rng.integers(1, 10, (mesh.nC, 2)) >>> sigma_tensor = rng.integers(1, 10, (mesh.nC, 3)) Construct the property tensor in each case >>> M_scalar = make_property_tensor(mesh, sigma_scalar) >>> M_isotropic = make_property_tensor(mesh, sigma_isotropic) >>> M_anisotropic = make_property_tensor(mesh, sigma_anisotropic) >>> M_tensor = make_property_tensor(mesh, sigma_tensor) Construct the inverse property tensor in each case >>> Minv_scalar = inverse_property_tensor(mesh, sigma_scalar, return_matrix=True) >>> Minv_isotropic = inverse_property_tensor(mesh, sigma_isotropic, return_matrix=True) >>> Minv_anisotropic = inverse_property_tensor(mesh, sigma_anisotropic, return_matrix=True) >>> Minv_tensor = inverse_property_tensor(mesh, sigma_tensor, return_matrix=True) Plot the property tensors. >>> M_list = [M_scalar, M_isotropic, M_anisotropic, M_tensor] >>> Minv_list = [Minv_scalar, Minv_isotropic, Minv_anisotropic, Minv_tensor] >>> case_list = ['Scalar', 'Isotropic', 'Anisotropic', 'Full Tensor'] >>> fig1 = plt.figure(figsize=(15, 4)) >>> ax1 = 4*[None] >>> for ii in range(0, 4): ... ax1[ii] = fig1.add_axes([0.05+0.22*ii, 0.05, 0.18, 0.9]) ... ax1[ii].imshow( ... M_list[ii].todense(), interpolation='none', cmap='binary', vmax=10. ... ) ... ax1[ii].set_title('$M$ (' + case_list[ii] + ')', fontsize=24) >>> cax1 = fig1.add_axes([0.92, 0.15, 0.01, 0.7]) >>> norm1 = mpl.colors.Normalize(vmin=0., vmax=10.) >>> cbar1 = mpl.colorbar.ColorbarBase( ... cax1, norm=norm1, orientation="vertical", cmap=mpl.cm.binary ... ) >>> plt.show() Plot the inverse property tensors. >>> fig2 = plt.figure(figsize=(15, 4)) >>> ax2 = 4*[None] >>> for ii in range(0, 4): ... ax2[ii] = fig2.add_axes([0.05+0.22*ii, 0.05, 0.18, 0.9]) ... ax2[ii].imshow( ... Minv_list[ii].todense(), interpolation='none', cmap='binary', vmax=1. ... ) ... ax2[ii].set_title('$M^{-1}$ (' + case_list[ii] + ')', fontsize=24) >>> cax2 = fig2.add_axes([0.92, 0.15, 0.01, 0.7]) >>> norm2 = mpl.colors.Normalize(vmin=0., vmax=1.) >>> cbar2 = mpl.colorbar.ColorbarBase( ... cax2, norm=norm2, orientation="vertical", cmap=mpl.cm.binary ... ) >>> plt.show() """ if "returnMatrix" in kwargs: warnings.warn( "The returnMatrix keyword argument has been deprecated, please use return_matrix. " "This will be removed in discretize 1.0.0", FutureWarning, stacklevel=2, ) return_matrix = kwargs["returnMatrix"] propType = TensorType(mesh, tensor) if is_scalar(tensor): T = 1.0 / tensor elif propType < 3: # Isotropic or Diagonal T = 1.0 / mkvc(tensor) # ensure it is a vector. elif mesh.dim == 2 and tensor.size == mesh.nC * 3: # Fully anisotropic, 2D tensor = tensor.reshape((mesh.nC, 3), order="F") B = inverse_2x2_block_diagonal( tensor[:, 0], tensor[:, 2], tensor[:, 2], tensor[:, 1], return_matrix=False ) b11, b12, b21, b22 = B T = np.r_[b11, b22, b12] elif mesh.dim == 3 and tensor.size == mesh.nC * 6: # Fully anisotropic, 3D tensor = tensor.reshape((mesh.nC, 6), order="F") B = inverse_3x3_block_diagonal( tensor[:, 0], tensor[:, 3], tensor[:, 4], tensor[:, 3], tensor[:, 1], tensor[:, 5], tensor[:, 4], tensor[:, 5], tensor[:, 2], return_matrix=False, ) b11, b12, b13, b21, b22, b23, b31, b32, b33 = B T = np.r_[b11, b22, b33, b12, b13, b23] else: raise Exception("Unexpected shape of tensor") if return_matrix: return make_property_tensor(mesh, T) return T def cross2d(x, y): """Compute the cross product of two vectors. This function will calculate the cross product as if the third component of each of these vectors was zero. The returned direction is perpendicular to both inputs, making it be solely in the third dimension. Parameters ---------- x, y : array_like The vectors for the cross product. Returns ------- x_cross_y : numpy.ndarray The cross product of x and y. """ x = np.asarray(x) y = np.asarray(y) # np.cross(x, y) is deprecated for 2D input return x[..., 0] * y[..., 1] - x[..., 1] * y[..., 0] class Zero(object): """Carries out arithmetic operations between 0 and arbitrary quantities. This class was designed to manage basic arithmetic operations between 0 and :class:`numpy.ndarray` of any shape. It is a short circuiting evaluation that will return the expected values. Examples -------- >>> import numpy as np >>> from discretize.utils import Zero >>> Z = Zero() >>> Z Zero >>> x = np.arange(5) >>> x + Z array([0, 1, 2, 3, 4]) >>> Z - x array([ 0, -1, -2, -3, -4]) >>> Z * x Zero >>> Z @ x Zero >>> Z[0] Zero """ __numpy_ufunc__ = True __array_ufunc__ = None def __repr__(self): """Represent zeros a string.""" return "Zero" def __bool__(self): """Return False for zero matrix.""" return False def __add__(self, v): """Add a value to zero.""" return v def __radd__(self, v): """Add zero to a value.""" return v def __iadd__(self, v): """Add zero to a value inplace.""" return v def __sub__(self, v): """Subtract a value from zero.""" return -v def __rsub__(self, v): """Subtract zero from a value.""" return v def __isub__(self, v): """Subtract zero from a value inplace.""" return v def __mul__(self, v): """Multiply zero by a value.""" return self def __rmul__(self, v): """Multiply a value by zero.""" return self def __matmul__(self, v): """Multiply zero by a matrix.""" return self def __rmatmul__(self, v): """Multiply a matrix by zero.""" return self def __div__(self, v): """Divide zero by a value.""" return self def __truediv__(self, v): """Divide zero by a value.""" return self def __rdiv__(self, v): """Try to divide a value by zero.""" raise ZeroDivisionError("Cannot divide by zero.") def __rtruediv__(self, v): """Try to divide a value by zero.""" raise ZeroDivisionError("Cannot divide by zero.") def __rfloordiv__(self, v): """Try to divide a value by zero.""" raise ZeroDivisionError("Cannot divide by zero.") def __pos__(self): """Return zero.""" return self def __neg__(self): """Negate zero.""" return self def __lt__(self, v): """Compare less than zero.""" return 0 < v def __le__(self, v): """Compare less than or equal to zero.""" return 0 <= v def __eq__(self, v): """Compare equal to zero.""" return v == 0 def __ne__(self, v): """Compare not equal to zero.""" return not (0 == v) def __ge__(self, v): """Compare greater than or equal to zero.""" return 0 >= v def __gt__(self, v): """Compare greater than zero.""" return 0 > v def transpose(self): """Return the transpose of the *Zero* class, i.e. itself.""" return self def __getitem__(self, key): """Get an element of the *Zero* class, i.e. itself.""" return self @property def ndim(self): """Return the dimension of *Zero* class, i.e. *None*.""" return None @property def shape(self): """Return the shape *Zero* class, i.e. *None*.""" return _inftup(None) @property def T(self): """Return the *Zero* class as an operator.""" return self class Identity(object): """Carries out arithmetic operations involving the identity. This class was designed to manage basic arithmetic operations between the identity matrix and :class:`numpy.ndarray` of any shape. It is a short circuiting evaluation that will return the expected values. Parameters ---------- positive : bool, optional Whether it is a positive (or negative) Identity matrix Examples -------- >>> import numpy as np >>> from discretize.utils import Identity, Zero >>> Z = Zero() >>> I = Identity() >>> x = np.arange(5) >>> x + I array([1, 2, 3, 4, 5]) >>> I - x array([ 1, 0, -1, -2, -3]) >>> I * x array([0, 1, 2, 3, 4]) >>> I @ x array([0, 1, 2, 3, 4]) >>> I @ Z Zero """ __numpy_ufunc__ = True __array_ufunc__ = None _positive = True def __init__(self, positive=True): self._positive = positive def __repr__(self): """Represent 1 (or -1 if not positive).""" if self._positive: return "I" else: return "-I" def __bool__(self): """Return True for identity matrix.""" return True def __pos__(self): """Return positive 1 (or -1 if not positive).""" return self def __neg__(self): """Negate 1 (or -1 if not positive).""" return Identity(not self._positive) def __add__(self, v): """Add 1 (or -1 if not positive) to a value.""" if sp.issparse(v): return v + speye(v.shape[0]) if self._positive else v - speye(v.shape[0]) return v + 1 if self._positive else v - 1 def __radd__(self, v): """Add 1 (or -1 if not positive) to a value.""" return self.__add__(v) def __sub__(self, v): """Subtract a value from 1 (or -1 if not positive).""" return self + -v def __rsub__(self, v): """Subtract 1 (or -1 if not positive) from a value.""" return -self + v def __mul__(self, v): """Multiply 1 (or -1 if not positive) by a value.""" return v if self._positive else -v def __rmul__(self, v): """Multiply 1 (or -1 if not positive) by a value.""" return v if self._positive else -v def __matmul__(self, v): """Multiply 1 (or -1 if not positive) by a matrix.""" return v if self._positive else -v def __rmatmul__(self, v): """Multiply a matrix by 1 (or -1 if not positive).""" return v if self._positive else -v def __div__(self, v): """Divide 1 (or -1 if not positive) by a value.""" if sp.issparse(v): raise NotImplementedError("Sparse arrays not divisibile.") return 1 / v if self._positive else -1 / v def __truediv__(self, v): """Divide 1 (or -1 if not positive) by a value.""" if sp.issparse(v): raise NotImplementedError("Sparse arrays not divisibile.") return 1.0 / v if self._positive else -1.0 / v def __rdiv__(self, v): """Divide a value by 1 (or -1 if not positive).""" return v if self._positive else -v def __rtruediv__(self, v): """Divide a value by 1 (or -1 if not positive).""" return v if self._positive else -v def __floordiv__(self, v): """Flooring division of 1 (or -1 if not positive) by a value.""" return 1 // v if self._positive else -1 // v def __rfloordiv__(self, v): """Flooring division of a value by 1 (or -1 if not positive).""" return v // 1 if self._positive else v // -1 def __lt__(self, v): """Compare less than 1 (or -1 if not positive).""" return 1 < v if self._positive else -1 < v def __le__(self, v): """Compare less than or equal to 1 (or -1 if not positive).""" return 1 <= v if self._positive else -1 <= v def __eq__(self, v): """Compare equal to 1 (or -1 if not positive).""" return v == 1 if self._positive else v == -1 def __ne__(self, v): """Compare not equal to 1 (or -1 if not positive).""" return (not (1 == v)) if self._positive else (not (-1 == v)) def __ge__(self, v): """Compare greater than or equal to 1 (or -1 if not positive).""" return 1 >= v if self._positive else -1 >= v def __gt__(self, v): """Compare greater than 1 (or -1 if not positive).""" return 1 > v if self._positive else -1 > v @property def ndim(self): """Return the dimension of *Identity* class, i.e. *None*.""" return None @property def shape(self): """Return the shape of *Identity* class, i.e. *None*.""" return _inftup(None) @property def T(self): """Return the *Identity* class as an operator.""" return self def transpose(self): """Return the transpose of the *Identity* class, i.e. itself.""" return self class _inftup(tuple): """An infinitely long tuple of a value repeated infinitely.""" def __init__(self, val=None): self._val = val def __getitem__(self, key): if isinstance(key, slice): return _inftup(self._val) return self._val def __len__(self): return 0 def __repr__(self): return f"({self._val}, {self._val}, ...)" ################################################ # DEPRECATED FUNCTIONS ################################################ sdInv = deprecate_function(sdinv, "sdInv", removal_version="1.0.0", error=True) getSubArray = deprecate_function( get_subarray, "getSubArray", removal_version="1.0.0", error=True ) inv3X3BlockDiagonal = deprecate_function( inverse_3x3_block_diagonal, "inv3X3BlockDiagonal", removal_version="1.0.0", error=True, ) inv2X2BlockDiagonal = deprecate_function( inverse_2x2_block_diagonal, "inv2X2BlockDiagonal", removal_version="1.0.0", error=True, ) makePropertyTensor = deprecate_function( make_property_tensor, "makePropertyTensor", removal_version="1.0.0", error=True, ) invPropertyTensor = deprecate_function( inverse_property_tensor, "invPropertyTensor", removal_version="1.0.0", error=True, ) ================================================ FILE: discretize/utils/matutils.py ================================================ from discretize.utils.matrix_utils import * # NOQA F401,F403 raise ImportError( "Importing from discretize.matutils is deprecated behavoir. Please import " "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", ) ================================================ FILE: discretize/utils/mesh_utils.py ================================================ """Useful tools for working with meshes.""" import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp from discretize.utils.code_utils import is_scalar from scipy.spatial import cKDTree, Delaunay from scipy import interpolate import discretize from discretize.utils.code_utils import deprecate_function import warnings num_types = [int, float] def random_model( shape, random_seed=None, anisotropy=None, its=100, bounds=None, seed=None ): """Create random tensor model. Creates a random tensor model by convolving a kernel function with a uniformly distributed model. The user specifies the number of cells along the x, (y and z) directions with the input argument *shape* and the function outputs a tensor model with the same shape. Afterwards, the user may use the :py:func:`~discretize.utils.mkvc` function to convert the tensor to a vector which can be plotting on a corresponding tensor mesh. Parameters ---------- shape : (dim) tuple of int shape of the model. random_seed : numpy.random.Generator, int, optional pick which model to produce, prints the seed if you don't choose anisotropy : numpy.ndarray, optional this is the kernel that is convolved with the model its : int, optional number of smoothing iterations bounds : list, optional Lower and upper bounds on the model. Has the form [lower_bound, upper_bound]. Returns ------- numpy.ndarray A random generated model whose shape was specified by the input parameter *shape* Examples -------- Here, we generate a random model for a 2D tensor mesh and plot. >>> from discretize import TensorMesh >>> from discretize.utils import random_model, mkvc >>> import matplotlib as mpl >>> import matplotlib.pyplot as plt >>> h = [(1., 50)] >>> vmin, vmax = 0., 1. >>> mesh = TensorMesh([h, h]) >>> model = random_model(mesh.shape_cells, random_seed=4, bounds=[vmin, vmax]) >>> fig = plt.figure(figsize=(5, 4)) >>> ax = plt.subplot(111) >>> im, = mesh.plot_image(model, grid=False, ax=ax, clim=[vmin, vmax]) >>> cbar = plt.colorbar(im) >>> ax.set_title('Random Tensor Model') >>> plt.show() """ if bounds is None: bounds = [0, 1] if seed is not None: warnings.warn( "Deprecated in version 0.11.0. The `seed` keyword argument has been renamed to `random_seed` " "for consistency across the package. Please update your code to use the new keyword argument.", FutureWarning, stacklevel=2, ) random_seed = seed rng = np.random.default_rng(random_seed) if random_seed is None: print("Using a seed of: ", rng.bit_generator.seed_seq) if type(shape) in num_types: shape = (shape,) # make it a tuple for consistency mr = rng.random(shape) if anisotropy is None: if len(shape) == 1: smth = np.array([1, 10.0, 1], dtype=float) elif len(shape) == 2: smth = np.array([[1, 7, 1], [2, 10, 2], [1, 7, 1]], dtype=float) elif len(shape) == 3: kernal = np.array([1, 4, 1], dtype=float).reshape((1, 3)) smth = np.array( sp.kron(sp.kron(kernal, kernal.T).todense()[:], kernal).todense() ).reshape((3, 3, 3)) else: if len(anisotropy.shape) != len(shape): raise ValueError("Anisotropy must be the same shape.") smth = np.array(anisotropy, dtype=float) smth = smth / smth.sum() # normalize mi = mr for _i in range(its): mi = ndi.convolve(mi, smth) # scale the model to live between the bounds. mi = (mi - mi.min()) / (mi.max() - mi.min()) # scaled between 0 and 1 mi = mi * (bounds[1] - bounds[0]) + bounds[0] return mi def unpack_widths(value): """Unpack a condensed representation of cell widths or time steps. For a list of numbers, if the same value is repeat or expanded by a constant factor, it may be represented in a condensed form using list of floats and/or tuples. **unpack_widths** takes a list of floats and/or tuples in condensed form, e.g.: [ float, (cellSize, numCell), (cellSize, numCell, factor) ] and expands the representation to a list containing all widths in order. That is: [ w1, w2, w3, ..., wn ] Parameters ---------- value : list of float and/or tuple The list of floats and/or tuples that are to be unpacked Returns ------- numpy.ndarray The unpacked list with all widths in order Examples -------- Time stepping for time-domain codes can be represented in condensed form, e.g.: >>> from discretize.utils import unpack_widths >>> dt = [ (1e-5, 10), (1e-4, 4), 1e-3 ] The above means to take 10 steps at a step width of 1e-5 s and then 4 more at 1e-4 s, and then one step of 1e-3 s. When unpacked, the output is of length 15 and is given by: >>> unpack_widths(dt) array([1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-04, 1.e-04, 1.e-04, 1.e-04, 1.e-03]) Each axis of a tensor mesh can also be defined as a condensed list of floats and/or tuples. When a third number is defined in any tuple, the width value is successively expanded by that factor, e.g.: >>> dt = [ 6., 8., (10.0, 3), (8.0, 4, 2.) ] >>> unpack_widths(dt) array([ 6., 8., 10., 10., 10., 16., 32., 64., 128.]) """ if type(value) is not list: raise Exception("unpack_widths must be a list of scalars and tuples.") proposed = [] for v in value: if is_scalar(v): proposed += [float(v)] elif type(v) is tuple and len(v) == 2: proposed += [float(v[0])] * int(v[1]) elif type(v) is tuple and len(v) == 3: start = float(v[0]) num = int(v[1]) factor = float(v[2]) pad = ((np.ones(num) * np.abs(factor)) ** (np.arange(num) + 1)) * start if factor < 0: pad = pad[::-1] proposed += pad.tolist() else: raise Exception( "unpack_widths must contain only scalars and len(2) or len(3) tuples." ) return np.array(proposed) def closest_points_index(mesh, pts, grid_loc="CC", **kwargs): """Find the indicies for the nearest grid location for a set of points. Parameters ---------- mesh : discretize.base.BaseMesh An instance of *discretize.base.BaseMesh* pts : (n, dim) numpy.ndarray Points to query. grid_loc : {'CC', 'N', 'Fx', 'Fy', 'Fz', 'Ex', 'Ex', 'Ey', 'Ez'} Specifies the grid on which points are being moved to. Returns ------- (n ) numpy.ndarray of int Vector of length *n* containing the indicies for the closest respective cell center, node, face or edge. Examples -------- Here we define a set of random (x, y) locations and find the closest cell centers and nodes on a mesh. >>> from discretize import TensorMesh >>> from discretize.utils import closest_points_index >>> import numpy as np >>> import matplotlib.pyplot as plt >>> h = 2*np.ones(5) >>> mesh = TensorMesh([h, h], x0='00') Define some random locations, grid cell centers and grid nodes, >>> xy_random = np.random.uniform(0, 10, size=(4,2)) >>> xy_centers = mesh.cell_centers >>> xy_nodes = mesh.nodes Find indicies of closest cell centers and nodes, >>> ind_centers = closest_points_index(mesh, xy_random, 'CC') >>> ind_nodes = closest_points_index(mesh, xy_random, 'N') Plot closest cell centers and nodes >>> fig = plt.figure(figsize=(5, 5)) >>> ax = fig.add_axes([0.1, 0.1, 0.8, 0.8]) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(xy_random[:, 0], xy_random[:, 1], 50, 'k') >>> ax.scatter(xy_centers[ind_centers, 0], xy_centers[ind_centers, 1], 50, 'r') >>> ax.scatter(xy_nodes[ind_nodes, 0], xy_nodes[ind_nodes, 1], 50, 'b') >>> plt.show() """ if "gridLoc" in kwargs: raise TypeError( "The gridLoc keyword argument has been removed, please use grid_loc. " "This message will be removed in discretize 1.0.0", ) warnings.warn( "The closest_points_index utilty function has been moved to be a method of " "a class object. Please access it as mesh.closest_points_index(). This will " "be removed in a future version of discretize", DeprecationWarning, stacklevel=2, ) return mesh.closest_points_index(pts, grid_loc=grid_loc, discard=True) def extract_core_mesh(xyzlim, mesh, mesh_type="tensor"): """Extract the core mesh from a global mesh. Parameters ---------- xyzlim : (dim, 2) numpy.ndarray 2D array defining the x, y and z cutoffs for the core mesh region. Each row contains the minimum and maximum limit for the x, y and z axis, respectively. mesh : discretize.TensorMesh The mesh mesh_type : str, optional Unused currently Returns ------- tuple: (**active_index**, **core_mesh**) **active_index** is a boolean array that maps from the global the mesh to core mesh. **core_mesh** is a *discretize.base.BaseMesh* object representing the core mesh. Examples -------- Here, we define a 2D tensor mesh that has both a core region and padding. We use the function **extract_core_mesh** to return a mesh which contains only the core region. >>> from discretize.utils import extract_core_mesh >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl >>> mpl.rcParams.update({"font.size": 14}) Form a mesh of a uniform cube >>> h = [(1., 5, -1.5), (1., 20), (1., 5, 1.5)] >>> mesh = TensorMesh([h, h], origin='CC') Plot original mesh >>> fig = plt.figure(figsize=(7, 7)) >>> ax = fig.add_subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.set_title('Original Tensor Mesh') >>> plt.show() Set the limits for the cutoff of the core mesh (dim, 2) >>> xlim = np.c_[-10., 10] >>> ylim = np.c_[-10., 10] >>> core_limits = np.r_[xlim, ylim] Extract indices of core mesh cells and the core mesh, then plot >>> core_ind, core_mesh = extract_core_mesh(core_limits, mesh) >>> fig = plt.figure(figsize=(4, 4)) >>> ax = fig.add_subplot(111) >>> core_mesh.plot_grid(ax=ax) >>> ax.set_title('Core Mesh') >>> plt.show() """ if not isinstance(mesh, discretize.TensorMesh): raise Exception("Only implemented for class TensorMesh") if mesh.dim == 1: xyzlim = xyzlim.flatten() xmin, xmax = xyzlim[0], xyzlim[1] xind = np.logical_and(mesh.cell_centers_x > xmin, mesh.cell_centers_x < xmax) xc = mesh.cell_centers_x[xind] hx = mesh.h[0][xind] origin = [xc[0] - hx[0] * 0.5] meshCore = discretize.TensorMesh([hx], origin=origin) actind = (mesh.cell_centers > xmin) & (mesh.cell_centers < xmax) elif mesh.dim == 2: xmin, xmax = xyzlim[0, 0], xyzlim[0, 1] ymin, ymax = xyzlim[1, 0], xyzlim[1, 1] xind = np.logical_and(mesh.cell_centers_x > xmin, mesh.cell_centers_x < xmax) yind = np.logical_and(mesh.cell_centers_y > ymin, mesh.cell_centers_y < ymax) xc = mesh.cell_centers_x[xind] yc = mesh.cell_centers_y[yind] hx = mesh.h[0][xind] hy = mesh.h[1][yind] origin = [xc[0] - hx[0] * 0.5, yc[0] - hy[0] * 0.5] meshCore = discretize.TensorMesh([hx, hy], origin=origin) actind = ( (mesh.cell_centers[:, 0] > xmin) & (mesh.cell_centers[:, 0] < xmax) & (mesh.cell_centers[:, 1] > ymin) & (mesh.cell_centers[:, 1] < ymax) ) elif mesh.dim == 3: xmin, xmax = xyzlim[0, 0], xyzlim[0, 1] ymin, ymax = xyzlim[1, 0], xyzlim[1, 1] zmin, zmax = xyzlim[2, 0], xyzlim[2, 1] xind = np.logical_and(mesh.cell_centers_x > xmin, mesh.cell_centers_x < xmax) yind = np.logical_and(mesh.cell_centers_y > ymin, mesh.cell_centers_y < ymax) zind = np.logical_and(mesh.cell_centers_z > zmin, mesh.cell_centers_z < zmax) xc = mesh.cell_centers_x[xind] yc = mesh.cell_centers_y[yind] zc = mesh.cell_centers_z[zind] hx = mesh.h[0][xind] hy = mesh.h[1][yind] hz = mesh.h[2][zind] origin = [xc[0] - hx[0] * 0.5, yc[0] - hy[0] * 0.5, zc[0] - hz[0] * 0.5] meshCore = discretize.TensorMesh([hx, hy, hz], origin=origin) actind = ( (mesh.cell_centers[:, 0] > xmin) & (mesh.cell_centers[:, 0] < xmax) & (mesh.cell_centers[:, 1] > ymin) & (mesh.cell_centers[:, 1] < ymax) & (mesh.cell_centers[:, 2] > zmin) & (mesh.cell_centers[:, 2] < zmax) ) else: raise Exception("Not implemented!") return actind, meshCore def mesh_builder_xyz( xyz, h, padding_distance=None, base_mesh=None, depth_core=None, expansion_factor=1.3, mesh_type="tensor", tree_diagonal_balance=None, ): """Generate a tensor or tree mesh using a cloud of points. For a cloud of (x,y[,z]) locations and specified minimum cell widths (hx,hy,[hz]), this function creates a tensor or a tree mesh. The lateral extent of the core region is determine by the cloud of points. Other properties of the mesh can be defined automatically or by the user. If *base_mesh* is an instance of :class:`~discretize.TensorMesh` or :class:`~discretize.TreeMesh`, the core cells will be centered on the underlying mesh to reduce interpolation errors. Parameters ---------- xyz : (n, dim) numpy.ndarray Location points h : (dim ) list Cell size(s) for the core mesh padding_distance : list, optional Padding distances [[W,E], [N,S], [Down,Up]], default is no padding. base_mesh : discretize.TensorMesh or discretize.TreeMesh, optional discretize mesh used to center the core mesh depth_core : float, optional Depth of core mesh below xyz expansion_factor : float. optional Expansion factor for padding cells. Ignored if *mesh_type* = *tree* mesh_type : {'tensor', 'tree'} Specify output mesh type tree_diagonal_balance : bool, optional Whether to diagonally balance the tree mesh, `None` will use the `TreeMesh` default behavoir. Returns ------- discretize.TensorMesh or discretize.TreeMesh Mesh of type specified by *mesh_type* Examples -------- >>> import discretize >>> import matplotlib.pyplot as plt >>> import numpy as np >>> rng = np.random.default_rng(87142) >>> xy_loc = rng.standard_normal((8,2)) >>> mesh = discretize.utils.mesh_builder_xyz( ... xy_loc, [0.1, 0.1], depth_core=0.5, ... padding_distance=[[1,2], [1,0]], ... mesh_type='tensor', ... ) >>> axs = plt.subplot() >>> mesh.plot_image(mesh.cell_volumes, grid=True, ax=axs) >>> axs.scatter(xy_loc[:,0], xy_loc[:,1], 15, c='w', zorder=3) >>> axs.set_aspect('equal') >>> plt.show() """ if mesh_type.lower() not in ["tensor", "tree"]: raise ValueError("Revise mesh_type. Only TENSOR | TREE mesh are implemented") if padding_distance is None: padding_distance = [[0, 0], [0, 0], [0, 0]] # Get extent of points limits = [] center = [] nC = [] for dim in range(xyz.shape[1]): max_min = np.r_[xyz[:, dim].max(), xyz[:, dim].min()] limits += [max_min] center += [np.mean(max_min)] nC += [int((max_min[0] - max_min[1]) / h[dim])] if depth_core is not None: nC[-1] += int(depth_core / h[-1]) limits[-1][1] -= depth_core if mesh_type.lower() == "tensor": # Figure out padding cells from distance def expand(dx, pad): length = 0 nc = 0 while length < pad: nc += 1 length = np.sum(dx * expansion_factor ** (np.asarray(range(nc)) + 1)) return nc # Define h along each dimension h_dim = [] nC_origin = [] for dim in range(xyz.shape[1]): h_dim += [ [ ( h[dim], expand(h[dim], padding_distance[dim][0]), -expansion_factor, ), (h[dim], nC[dim]), ( h[dim], expand(h[dim], padding_distance[dim][1]), expansion_factor, ), ] ] nC_origin += [h_dim[-1][0][1]] # Create mesh mesh = discretize.TensorMesh(h_dim) elif mesh_type.lower() == "tree": # Figure out full extent required from input h_dim = [] nC_origin = [] for ii, _cc in enumerate(nC): extent = limits[ii][0] - limits[ii][1] + np.sum(padding_distance[ii]) # Number of cells at the small octree level maxLevel = int(np.log2(extent / h[ii])) + 1 h_dim += [np.ones(2**maxLevel) * h[ii]] # Define the mesh and origin mesh = discretize.TreeMesh(h_dim, diagonal_balance=tree_diagonal_balance) for ii, _cc in enumerate(nC): core = limits[ii][0] - limits[ii][1] nC_origin += [int(np.ceil((mesh.h[ii].sum() - core) / h[ii] / 2))] # Set origin origin = [] for ii, hi in enumerate(mesh.h): origin += [limits[ii][1] - np.sum(hi[: nC_origin[ii]])] mesh.origin = np.hstack(origin) # Shift mesh if global mesh is used based on closest to centroid axis = ["x", "y", "z"] if base_mesh is not None: for dim in range(base_mesh.dim): cc_base = getattr( base_mesh, "cell_centers_{orientation}".format(orientation=axis[dim]), ) cc_local = getattr( mesh, "cell_centers_{orientation}".format(orientation=axis[dim]) ) shift = ( cc_base[np.max([np.searchsorted(cc_base, center[dim]) - 1, 0])] - cc_local[np.max([np.searchsorted(cc_local, center[dim]) - 1, 0])] ) origin[dim] += shift mesh.origin = np.hstack(origin) return mesh def refine_tree_xyz( mesh, xyz, method="radial", octree_levels=(1, 1, 1), octree_levels_padding=None, finalize=False, min_level=0, max_distance=np.inf, ): """Refine region within a :class:`~discretize.TreeMesh`. This function refines the specified region of a tree mesh using one of several methods. These are summarized below: **radial:** refines based on radial distances from a set of xy[z] locations. Consider a tree mesh whose smallest cell size has a width of *h* . And *octree_levels = [nc1, nc2, nc3, ...]* . Within a distance of *nc1 x h* from any of the points supplied, the smallest cell size is used. Within a distance of *nc2 x (2h)* , the cells will have a width of *2h* . Within a distance of *nc3 x (4h)* , the cells will have a width of *4h* . Etc... **surface:** refines downward from a triangulated surface. Consider a tree mesh whose smallest cell size has a width of *h*. And *octree_levels = [nc1, nc2, nc3, ...]* . Within a downward distance of *nc1 x h* from the topography (*xy[z]* ) supplied, the smallest cell size is used. The topography is triangulated if the points supplied are coarser than the cell size. No refinement is done above the topography. Within a vertical distance of *nc2 x (2h)* , the cells will have a width of *2h* . Within a vertical distance of *nc3 x (4h)* , the cells will have a width of *4h* . Etc... **box:** refines inside the convex hull defined by the xy[z] locations. Consider a tree mesh whose smallest cell size has a width of *h*. And *octree_levels = [nc1, nc2, nc3, ...]* . Within the convex hull defined by *xyz* , the smallest cell size is used. Within a distance of *nc2 x (2h)* from that convex hull, the cells will have a width of *2h* . Within a distance of *nc3 x (4h)* , the cells will have a width of *4h* . Etc... .. deprecated:: 0.9.0 `refine_tree_xyz` will be removed in a future version of discretize. It is replaced by `discretize.TreeMesh.refine_surface`, `discretize.TreeMesh.refine_bounding_box`, and `discretize.TreeMesh.refine_points`, to separate the calling convetions, and improve the individual documentation. Those methods are more explicit about which levels of the TreeMesh you are refining, and provide more flexibility for padding cells in each dimension. Parameters ---------- mesh : discretize.TreeMesh The tree mesh object to be refined xyz : numpy.ndarray 2D array of points (n, dim) method : {'radial', 'surface', 'box'} Method used to refine the mesh based on xyz locations. - *radial:* Based on radial distance xy[z] and cell centers - *surface:* Refines downward from a triangulated surface - *box:* Inside limits defined by outer xy[z] locations octree_levels : list of int, optional Minimum number of cells around points in each *k* octree level starting from the smallest cells size; i.e. *[nc(k), nc(k-1), ...]* . Note that you *can* set entries to 0; e.g. you don't want to discretize using the smallest cell size. octree_levels_padding : list of int, optional Padding cells added to extend the region of refinement at each level. Used for *method = surface* and *box*. Has the form *[nc(k), nc(k-1), ...]* finalize : bool, optional Finalize the tree mesh. min_level : int, optional Sets the largest cell size allowed in the mesh. The default (*0*), allows the largest cell size to be used. max_distance : float Maximum refinement distance from xy[z] locations. Used if *method* = "surface" to reduce interpolation distance Returns ------- discretize.TreeMesh The refined tree mesh See Also -------- discretize.TreeMesh.refine_surface Recommended to use instead of this function for the `surface` option. discretize.TreeMesh.refine_bounding_box Recommended to use instead of this function for the `box` option. discretize.TreeMesh.refine_points Recommended to use instead of this function for the `radial` option. Examples -------- Here we use the **refine_tree_xyz** function refine a tree mesh based on topography as well as a cluster of points. >>> from discretize import TreeMesh >>> from discretize.utils import mkvc, refine_tree_xyz >>> import matplotlib.pyplot as plt >>> import numpy as np >>> dx = 5 # minimum cell width (base mesh cell width) in x >>> dy = 5 # minimum cell width (base mesh cell width) in y >>> x_length = 300.0 # domain width in x >>> y_length = 300.0 # domain width in y Compute number of base mesh cells required in x and y >>> nbcx = 2 ** int(np.round(np.log(x_length / dx) / np.log(2.0))) >>> nbcy = 2 ** int(np.round(np.log(y_length / dy) / np.log(2.0))) Define the base mesh >>> hx = [(dx, nbcx)] >>> hy = [(dy, nbcy)] >>> mesh = TreeMesh([hx, hy], x0="CC") Refine surface topography >>> xx = mesh.nodes_x >>> yy = -3 * np.exp((xx ** 2) / 100 ** 2) + 50.0 >>> pts = np.c_[mkvc(xx), mkvc(yy)] >>> mesh = refine_tree_xyz( ... mesh, pts, octree_levels=[2, 4], method="surface", finalize=False ... ) Refine mesh near points >>> xx = np.array([-10.0, 10.0, 10.0, -10.0]) >>> yy = np.array([-40.0, -40.0, -60.0, -60.0]) >>> pts = np.c_[mkvc(xx), mkvc(yy)] >>> mesh = refine_tree_xyz( ... mesh, pts, octree_levels=[4, 2], method="radial", finalize=True ... ) Plot the mesh >>> fig = plt.figure(figsize=(6, 6)) >>> ax = fig.add_subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.set_xbound(mesh.x0[0], mesh.x0[0] + np.sum(mesh.h[0])) >>> ax.set_ybound(mesh.x0[1], mesh.x0[1] + np.sum(mesh.h[1])) >>> ax.set_title("QuadTree Mesh") >>> plt.show() """ if octree_levels_padding is not None: if len(octree_levels_padding) != len(octree_levels): raise ValueError( "'octree_levels_padding' must be the length %i" % len(octree_levels) ) else: octree_levels_padding = np.zeros_like(octree_levels) octree_levels = np.asarray(octree_levels) octree_levels_padding = np.asarray(octree_levels_padding) # levels = mesh.max_level - np.arange(len(octree_levels)) # non_zeros = octree_levels != 0 # levels = levels[non_zeros] # Trigger different refine methods if method.lower() == "radial": # padding = octree_levels[non_zeros] # mesh.refine_points(xyz, levels, padding, finalize=finalize,) warnings.warn( "The radial option is deprecated as of `0.9.0` please update your code to " "use the `TreeMesh.refine_points` functionality. It will be removed in a " "future version of discretize.", DeprecationWarning, stacklevel=2, ) # Compute the outer limits of each octree level rMax = np.cumsum( mesh.h[0].min() * octree_levels * 2 ** np.arange(len(octree_levels)) ) rs = np.ones(xyz.shape[0]) level = np.ones(xyz.shape[0], dtype=np.int32) for ii, _nC in enumerate(octree_levels): # skip "zero" sized balls if rMax[ii] > 0: mesh.refine_ball( xyz, rs * rMax[ii], level * (mesh.max_level - ii), finalize=False ) if finalize: mesh.finalize() elif method.lower() == "surface": warnings.warn( "The surface option is deprecated as of `0.9.0` please update your code to " "use the `TreeMesh.refine_surface` functionality. It will be removed in a " "future version of discretize.", DeprecationWarning, stacklevel=2, ) # padding = np.zeros((len(octree_levels), mesh.dim)) # padding[:, -1] = np.maximum(octree_levels - 1, 0) # padding[:, :-1] = octree_levels_padding[:, None] # padding = padding[non_zeros] # mesh.refine_surface(xyz, levels, padding, finalize=finalize, pad_down=True, pad_up=False) # Compute centroid centroid = np.mean(xyz, axis=0) if mesh.dim == 2: rOut = np.abs(centroid[0] - xyz).max() hz = mesh.h[1].min() else: # Largest outer point distance rOut = np.linalg.norm( np.r_[ np.abs(centroid[0] - xyz[:, 0]).max(), np.abs(centroid[1] - xyz[:, 1]).max(), ] ) hz = mesh.h[2].min() # Compute maximum depth of refinement zmax = np.cumsum(hz * octree_levels * 2 ** np.arange(len(octree_levels))) # Compute maximum horizontal padding offset padWidth = np.cumsum( mesh.h[0].min() * octree_levels_padding * 2 ** np.arange(len(octree_levels_padding)) ) # Increment the vertical offset zOffset = 0 xyPad = -1 depth = zmax[-1] # Cycle through the Tree levels backward for ii in range(len(octree_levels) - 1, -1, -1): dx = mesh.h[0].min() * 2**ii if mesh.dim == 3: dy = mesh.h[1].min() * 2**ii dz = mesh.h[2].min() * 2**ii else: dz = mesh.h[1].min() * 2**ii # Increase the horizontal extent of the surface if xyPad != padWidth[ii]: xyPad = padWidth[ii] # Calculate expansion for padding XY cells expansion_factor = (rOut + xyPad) / rOut xLoc = (xyz - centroid) * expansion_factor + centroid if mesh.dim == 3: # Create a new triangulated surface tri2D = Delaunay(xLoc[:, :2]) F = interpolate.LinearNDInterpolator(tri2D, xLoc[:, 2]) else: F = interpolate.interp1d( xLoc[:, 0], xLoc[:, 1], fill_value="extrapolate" ) limx = np.r_[xLoc[:, 0].max(), xLoc[:, 0].min()] nCx = int(np.ceil((limx[0] - limx[1]) / dx)) if mesh.dim == 3: limy = np.r_[xLoc[:, 1].max(), xLoc[:, 1].min()] nCy = int(np.ceil((limy[0] - limy[1]) / dy)) # Create a grid at the octree level in xy CCx, CCy = np.meshgrid( np.linspace(limx[1], limx[0], nCx), np.linspace(limy[1], limy[0], nCy), ) xy = np.c_[CCx.reshape(-1), CCy.reshape(-1)] # Only keep points within triangulation indexTri = tri2D.find_simplex(xy) else: xy = np.linspace(limx[1], limx[0], nCx) indexTri = np.ones_like(xy, dtype="bool") # Interpolate the elevation linearly z = F(xy[indexTri != -1]) newLoc = np.c_[xy[indexTri != -1], z] # Only keep points within max_distance tree = cKDTree(xyz) r, ind = tree.query(newLoc) # Apply vertical padding for current octree level dim = mesh.dim - 1 zOffset = 0 while zOffset < depth: indIn = r < (max_distance + padWidth[ii]) nnz = int(np.sum(indIn)) if nnz > 0: mesh.insert_cells( np.c_[newLoc[indIn, :dim], newLoc[indIn, -1] - zOffset], np.ones(nnz) * mesh.max_level - ii, finalize=False, ) zOffset += dz depth -= dz * octree_levels[ii] if finalize: mesh.finalize() elif method.lower() == "box": warnings.warn( "The box option is deprecated as of `0.9.0` please update your code to " "use the `TreeMesh.refine_bounding_box` functionality. It will be removed in a " "future version of discretize.", DeprecationWarning, stacklevel=2, ) # padding = np.zeros((len(octree_levels), mesh.dim)) # padding[:, -1] = np.maximum(octree_levels - 1, 0) # padding[:, :-1] = octree_levels_padding[:, None] # padding = padding[non_zeros] # mesh.refine_bounding_box(xyz, levels, padding, finalize=finalize) # Define the data extent [bottom SW, top NE] bsw = np.min(xyz, axis=0) tne = np.max(xyz, axis=0) hs = np.asarray([h.min() for h in mesh.h]) hx = hs[0] hz = hs[-1] # Pre-calculate outer extent of each level # x_pad padWidth = np.cumsum( hx * octree_levels_padding * 2 ** np.arange(len(octree_levels)) ) if mesh.dim == 3: # y_pad hy = hs[1] padWidth = np.c_[ padWidth, np.cumsum( hy * octree_levels_padding * 2 ** np.arange(len(octree_levels)) ), ] # Pre-calculate max depth of each level padWidth = np.c_[ padWidth, np.cumsum( hz * np.maximum(octree_levels - 1, 0) * 2 ** np.arange(len(octree_levels)) ), ] levels = [] BSW = [] TNE = [] for ii, octZ in enumerate(octree_levels): if octZ > 0: levels.append(mesh.max_level - ii) BSW.append(bsw - padWidth[ii]) TNE.append(tne + padWidth[ii]) mesh.refine_box(BSW, TNE, levels, finalize=finalize) else: raise NotImplementedError( "Only method= 'radial', 'surface'" " or 'box' have been implemented" ) return mesh def active_from_xyz(mesh, xyz, grid_reference="CC", method="linear"): """Return boolean array indicating which cells are below surface. For a set of locations defining a surface, **active_from_xyz** outputs a boolean array indicating which mesh cells like below the surface points. This method uses SciPy's interpolation routine to interpolate between location points defining the surface. Nearest neighbour interpolation is used for cells outside the convex hull of the surface points. Parameters ---------- mesh : discretize.TensorMesh or discretize.TreeMesh or discretize.CylindricalMesh Mesh object. If *mesh* is a cylindrical mesh, it must be symmetric xyz : (N, dim) numpy.ndarray Points defining the surface topography. grid_reference : {'CC', 'N'} Define where the cell is defined relative to surface. Choose between {'CC','N'} - If 'CC' is used, cells are active if their centers are below the surface. - If 'N' is used, cells are active if they lie entirely below the surface. method : {'linear', 'nearest'} Interpolation method for locations between the xyz points. Returns ------- (n_cells) numpy.ndarray of bool 1D mask array of *bool* for the active cells below xyz. Examples -------- Here we define the active cells below a parabola. We demonstrate the differences that appear when using the 'CC' and 'N' options for *reference_grid*. >>> import matplotlib.pyplot as plt >>> import numpy as np >>> from discretize import TensorMesh >>> from discretize.utils import active_from_xyz Determine active cells for a given mesh and topography >>> mesh = TensorMesh([5, 5]) >>> topo_func = lambda x: -3*(x-0.2)*(x-0.8)+.5 >>> topo_points = np.linspace(0, 1) >>> topo_vals = topo_func(topo_points) >>> active_cc = active_from_xyz(mesh, np.c_[topo_points, topo_vals], grid_reference='CC') >>> active_n = active_from_xyz(mesh, np.c_[topo_points, topo_vals], grid_reference='N') Plot visual representation >>> ax = plt.subplot(121) >>> mesh.plot_image(active_cc, ax=ax) >>> mesh.plot_grid(centers=True, ax=ax) >>> ax.plot(np.linspace(0,1), topo_func(np.linspace(0,1)), color='C3') >>> ax.set_title("CC") >>> ax = plt.subplot(122) >>> mesh.plot_image(active_n, ax=ax) >>> mesh.plot_grid(nodes=True, ax=ax) >>> ax.plot(np.linspace(0,1), topo_func(np.linspace(0,1)), color='C3') >>> ax.set_title("N") >>> plt.show() """ try: if not mesh.is_symmetric: raise NotImplementedError( "Unsymmetric CylindricalMesh is not yet supported" ) except AttributeError: pass if grid_reference not in ["N", "CC"]: raise ValueError( "Value of grid_reference must be 'N' (nodal) or 'CC' (cell center)" ) dim = mesh.dim - 1 if mesh.dim == 3: if xyz.shape[1] != 3: raise ValueError("xyz locations of shape (*, 3) required for 3D mesh") if method == "linear": tri2D = Delaunay(xyz[:, :2]) z_interpolate = interpolate.LinearNDInterpolator(tri2D, xyz[:, 2]) else: z_interpolate = interpolate.NearestNDInterpolator(xyz[:, :2], xyz[:, 2]) elif mesh.dim == 2: if xyz.shape[1] != 2: raise ValueError("xyz locations of shape (*, 2) required for 2D mesh") z_interpolate = interpolate.interp1d( xyz[:, 0], xyz[:, 1], bounds_error=False, fill_value=np.nan, kind=method ) else: if xyz.ndim != 1: raise ValueError("xyz locations of shape (*, ) required for 1D mesh") if grid_reference == "CC": # this should work for all 4 mesh types... locations = mesh.cell_centers if mesh.dim == 1: active = np.zeros(mesh.nC, dtype="bool") active[np.searchsorted(mesh.cell_centers_x, xyz).max() :] = True return active elif grid_reference == "N": try: # try for Cyl, Tensor, and Tree operations if mesh.dim == 3: locations = np.vstack( [ mesh.cell_centers + (np.c_[-1, 1, 1][:, None] * mesh.h_gridded / 2.0).squeeze(), mesh.cell_centers + (np.c_[-1, -1, 1][:, None] * mesh.h_gridded / 2.0).squeeze(), mesh.cell_centers + (np.c_[1, 1, 1][:, None] * mesh.h_gridded / 2.0).squeeze(), mesh.cell_centers + (np.c_[1, -1, 1][:, None] * mesh.h_gridded / 2.0).squeeze(), ] ) elif mesh.dim == 2: locations = np.vstack( [ mesh.cell_centers + (np.c_[-1, 1][:, None] * mesh.h_gridded / 2.0).squeeze(), mesh.cell_centers + (np.c_[1, 1][:, None] * mesh.h_gridded / 2.0).squeeze(), ] ) else: active = np.zeros(mesh.nC, dtype="bool") active[np.searchsorted(mesh.nodes_x, xyz).max() :] = True return active except AttributeError: # Try for Curvilinear Mesh gridN = mesh.gridN.reshape((*mesh.vnN, mesh.dim), order="F") if mesh.dim == 3: locations = np.vstack( [ gridN[:-1, 1:, 1:].reshape((-1, mesh.dim), order="F"), gridN[:-1, :-1, 1:].reshape((-1, mesh.dim), order="F"), gridN[1:, 1:, 1:].reshape((-1, mesh.dim), order="F"), gridN[1:, :-1, 1:].reshape((-1, mesh.dim), order="F"), ] ) elif mesh.dim == 2: locations = np.vstack( [ gridN[:-1, 1:].reshape((-1, mesh.dim), order="F"), gridN[1:, 1:].reshape((-1, mesh.dim), order="F"), ] ) # Interpolate z values on CC or N z_xyz = z_interpolate(locations[:, :-1]).squeeze() # Apply nearest neighbour if in extrapolation ind_nan = np.isnan(z_xyz) if any(ind_nan): tree = cKDTree(xyz) _, ind = tree.query(locations[ind_nan, :]) z_xyz[ind_nan] = xyz[ind, dim] # Create an active bool of all True active = np.all( (locations[:, dim] < z_xyz).reshape((mesh.nC, -1), order="F"), axis=1 ) return active.ravel() def example_simplex_mesh(rect_shape): """Create a simple tetrahedral mesh on a unit cube in 2D or 3D. Returns the nodes and connectivity of a triangulated domain on the [0, 1] cube. This is not necessarily a good triangulation, just a complete one. This is mostly used for testing purposes. In 2D, this discretizes each rectangle into two triangles. In 3D, each cube is broken into 6 tetrahedrons. Parameters ---------- rect_shape : (dim) array_like of int For each dimension, create n+1 nodes along that axis. Returns ------- points : (n_points, dim) numpy.ndarray array of created nodes simplics : (n_cells, dim + 1) numpy.ndarray connectivity of nodes for each cell. Examples -------- >>> from discretize import SimplexMesh >>> from discretize.utils import example_simplex_mesh >>> from matplotlib import pyplot as plt >>> nodes, simplices = example_simplex_mesh((5, 6)) >>> mesh = SimplexMesh(nodes, simplices) >>> mesh.plot_grid() >>> plt.show() """ if len(rect_shape) == 2: n1, n2 = rect_shape xs, ys = np.mgrid[0 : 1 : (n1 + 1) * 1j, 0 : 1 : (n2 + 1) * 1j] points = np.c_[xs.reshape(-1), ys.reshape(-1)] node_inds = np.arange((n1 + 1) * (n2 + 1)).reshape((n1 + 1, n2 + 1)) left_triangs = np.c_[ node_inds[:-1, :-1].reshape(-1), # i00 node_inds[1:, :-1].reshape(-1), # i10 node_inds[:-1, 1:].reshape(-1), # i01 ] right_triangs = np.c_[ node_inds[1:, 1:].reshape(-1), # i11 node_inds[1:, :-1].reshape(-1), # i10 node_inds[:-1, 1:].reshape(-1), # i01 ] simplices = np.r_[left_triangs, right_triangs] if len(rect_shape) == 3: n1, n2, n3 = rect_shape xs, ys, zs = np.mgrid[ 0 : 1 : (n1 + 1) * 1j, 0 : 1 : (n2 + 1) * 1j, 0 : 1 : (n3 + 1) * 1j ] points = np.c_[xs.reshape(-1), ys.reshape(-1), zs.reshape(-1)] node_inds = np.arange((n1 + 1) * (n2 + 1) * (n3 + 1)).reshape( (n1 + 1, n2 + 1, n3 + 1) ) a_triangs = np.c_[ node_inds[1:, :-1, :-1].reshape(-1), # i100 node_inds[:-1, :-1, 1:].reshape(-1), # i001 node_inds[:-1, 1:, :-1].reshape(-1), # i010 node_inds[:-1, :-1, :-1].reshape(-1), # i000 ] b_triangs = np.c_[ node_inds[:-1, 1:, 1:].reshape(-1), # i011 node_inds[1:, :-1, :-1].reshape(-1), # i100 node_inds[:-1, :-1, 1:].reshape(-1), # i001 node_inds[:-1, 1:, :-1].reshape(-1), # i010 ] c_triangs = np.c_[ node_inds[1:, 1:, :-1].reshape(-1), # i110 node_inds[:-1, 1:, 1:].reshape(-1), # i011 node_inds[1:, :-1, :-1].reshape(-1), # i100 node_inds[:-1, 1:, :-1].reshape(-1), # i010 ] d_triangs = np.c_[ node_inds[1:, :-1, 1:].reshape(-1), # i101 node_inds[:-1, 1:, 1:].reshape(-1), # i011 node_inds[1:, :-1, :-1].reshape(-1), # i100 node_inds[:-1, :-1, 1:].reshape(-1), # i001 ] e_triangs = np.c_[ node_inds[1:, :-1, 1:].reshape(-1), # i101 node_inds[1:, 1:, :-1].reshape(-1), # i110 node_inds[:-1, 1:, 1:].reshape(-1), # i011 node_inds[1:, 1:, 1:].reshape(-1), # i111 ] f_triangs = np.c_[ node_inds[1:, :-1, 1:].reshape(-1), # i101 node_inds[1:, 1:, :-1].reshape(-1), # i110 node_inds[:-1, 1:, 1:].reshape(-1), # i011 node_inds[1:, :-1, :-1].reshape(-1), # i100 ] simplices = np.r_[ a_triangs, b_triangs, c_triangs, d_triangs, e_triangs, f_triangs ] return points, simplices meshTensor = deprecate_function( unpack_widths, "meshTensor", removal_version="1.0.0", error=True ) closestPoints = deprecate_function( closest_points_index, "closestPoints", removal_version="1.0.0", error=True ) ExtractCoreMesh = deprecate_function( extract_core_mesh, "ExtractCoreMesh", removal_version="1.0.0", error=True ) closest_points = deprecate_function( closest_points_index, "closest_points", removal_version="1.0.0", error=True ) ================================================ FILE: discretize/utils/meshutils.py ================================================ from discretize.utils.mesh_utils import * # NOQA F401,F403 raise ImportError( "Importing from discretize.meshutils is deprecated behavoir. Please import " "from discretize.utils. This message will be removed in version 1.0.0 of discretize.", ) ================================================ FILE: discretize/utils/meson.build ================================================ python_sources = [ '__init__.py', 'code_utils.py', 'coordinate_utils.py', 'curvilinear_utils.py', 'interpolation_utils.py', 'io_utils.py', 'matrix_utils.py', 'mesh_utils.py', 'codeutils.py', 'coordutils.py', 'curvutils.py', 'interputils.py', 'matutils.py', 'meshutils.py', ] py.install_sources( python_sources, subdir: 'discretize/utils' ) ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -j auto SPHINXBUILD = sphinx-build BUILDDIR = _build # Internal variables. ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others .PHONY: all api help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " api to build the api docs" @echo " html to make standalone HTML files" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" all: html clean: rm -rf $(BUILDDIR)/* rm -rf api/generated rm -rf examples/ rm -rf tutorials/ rm -rf sg_execution_times.rst html-noplot: $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output or in $(BUILDDIR)/linkcheck/output.txt." linkcheck-noplot: $(SPHINXBUILD) -D plot_gallery=0 -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ================================================ FILE: docs/_static/css/custom.css ================================================ @import url(https://fonts.googleapis.com/css?family=Raleway); .bd-header .navbar-header-items__center { margin: auto!important; } body{ font-family: "Roboto", sans-serif; } h1{ font-family: "Raleway", Helvetica, Arial, sans-serif; font-weight: bold; } h2{ font-family: "Raleway", Helvetica, Arial, sans-serif; font-weight: bold; } h3{ font-family: "Raleway", Helvetica, Arial, sans-serif; font-weight: bold; } .column > h3{ font-family: "Raleway", Helvetica, Arial, sans-serif; } /* Dark theme tweaking Matplotlib images are in png and inverted while other output types are assumed to be normal images. */ html[data-theme=dark] img:not(.only-dark):not(.dark-light) { filter: invert(0.82) brightness(0.8) contrast(1.2); } html[data-theme=dark] img[src*='discretize-logo.png']:not(.only-dark):not(.dark-light){ filter: invert(0.82) brightness(0.8) contrast(1.2) hue-rotate(180deg); } html[data-theme=dark] img[src*='discretize-logo.png'] { filter: invert(0.82) brightness(0.8) contrast(1.2) hue-rotate(180deg); } html[data-theme=dark] .MathJax_SVG * { fill: var(--pst-color-text-base); } html[data-theme=dark] { --pst-color-text-muted: #a6a6a6; } html[data-theme=light] { --pst-color-text-muted: rgb(51, 51, 51); } html[data-theme=dark] .navbar-nav>.active>.nav-link { color: #FFFFFF!important; } html[data-theme=light] .navbar-nav>.active>.nav-link { color: #000000!important; } html[data-theme=dark] .bd-header { background: #213a1b!important; } html[data-theme=light] .bd-header { background: #acd6af!important; } ================================================ FILE: docs/_static/versions.json ================================================ [ { "name": "main", "version": "dev", "url": "https://discretize.simpeg.xyz/en/main/" }, { "name": "0.12.0 (stable)", "version": "v0.12.0", "url": "https://discretize.simpeg.xyz/en/v0.12.0/", "preferred": true }, { "name": "0.11.3", "version": "v0.11.3", "url": "https://discretize.simpeg.xyz/en/v0.11.3/" }, { "name": "0.11.2", "version": "v0.11.2", "url": "https://discretize.simpeg.xyz/en/v0.11.2/" }, { "name": "0.11.1", "version": "v0.11.1", "url": "https://discretize.simpeg.xyz/en/v0.11.1/" }, { "name": "0.11.0", "version": "v0.11.0", "url": "https://discretize.simpeg.xyz/en/v0.11.0/" }, { "name": "0.10.0", "version": "v0.10.0", "url": "https://discretize.simpeg.xyz/en/v0.10.0/" } ] ================================================ FILE: docs/_templates/autosummary/attribute.rst ================================================ :orphan: {{ fullname | escape | underline}} .. currentmodule:: {{ module }} .. auto{{ objtype }}:: {{ objname }} ================================================ FILE: docs/_templates/autosummary/base.rst ================================================ {% if objtype == 'property' %} :orphan: {% endif %} {{ fullname | escape | underline}} .. currentmodule:: {{ module }} .. auto{{ objtype }}:: {{ objname }} ================================================ FILE: docs/_templates/autosummary/class.rst ================================================ {{ fullname }} {{ underline }} .. currentmodule:: {{ module }} .. inheritance-diagram:: {{ objname }} :parts: 1 .. autoclass:: {{ objname }} {% block methods %} .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages. .. autosummary:: :toctree: {% for item in all_methods %} {%- if not item.startswith('_') or item in ['__call__', '__mul__', '__getitem__', '__len__'] %} {{ name }}.{{ item }} {%- endif -%} {%- endfor %} {% for item in inherited_members %} {%- if item in ['__call__', '__mul__', '__getitem__', '__len__'] %} {{ name }}.{{ item }} {%- endif -%} {%- endfor %} {% endblock %} {% block attributes %} {% if attributes %} .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages. .. autosummary:: :toctree: {% for item in all_attributes %} {%- if not item.startswith('_') %} {{ name }}.{{ item }} {%- endif -%} {%- endfor %} {% endif %} {% endblock %} .. minigallery:: {{ fullname }} :add-heading: Galleries and Tutorials using ``{{ fullname }}`` :heading-level: - ================================================ FILE: docs/_templates/autosummary/function.rst ================================================ {{ fullname | escape | underline }} .. currentmodule:: {{ module }} .. autofunction:: {{ objname }} .. minigallery:: {{ fullname }} :add-heading: Galleries and Tutorials using ``{{ fullname }}`` :heading-level: - ================================================ FILE: docs/_templates/autosummary/method.rst ================================================ :orphan: {{ fullname | escape | underline}} .. currentmodule:: {{ module }} .. auto{{ objtype }}:: {{ objname }} ================================================ FILE: docs/api/discretize.base.rst ================================================ .. automodule:: discretize.base ================================================ FILE: docs/api/discretize.mixins.rst ================================================ .. automodule:: discretize.mixins ================================================ FILE: docs/api/discretize.operators.rst ================================================ .. automodule:: discretize.operators ================================================ FILE: docs/api/discretize.rst ================================================ .. automodule:: discretize ================================================ FILE: docs/api/discretize.tests.rst ================================================ .. automodule:: discretize.tests ================================================ FILE: docs/api/discretize.utils.rst ================================================ .. automodule:: discretize.utils ================================================ FILE: docs/api/index.rst ================================================ .. _api: ============= API Reference ============= Meshes ====== Meshes supported by the ``discretize`` package. The :class:`~discretize.tree_mesh.TreeCell` class is an additional class used to define cells comprising instances of the :class:`~discretize.TreeMesh` class. .. toctree:: :maxdepth: 3 discretize Mesh Building Blocks ==================== Base classes for ``discretize`` meshes, classes for constructing discrete operators, and mixins for interfacing with external libraries. .. toctree:: :maxdepth: 2 discretize.base discretize.operators discretize.mixins Utilities ========= Classes and functions for performing useful operations. .. toctree:: :maxdepth: 3 discretize.utils Testing ======= Classes and functions for testing the ``discretize`` package. .. toctree:: :maxdepth: 2 discretize.tests ================================================ FILE: docs/conf.py ================================================ # -*- coding: utf-8 -*- # # discretize documentation build configuration file, created by # sphinx-quickstart on Fri Aug 30 18:42:44 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys from pathlib import Path from datetime import datetime from packaging.version import parse import discretize import shutil from importlib.metadata import version # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # sys.path.append(os.path.pardir) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "numpydoc", "sphinx.ext.autosummary", "sphinx.ext.coverage", "sphinx.ext.doctest", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", "sphinx.ext.inheritance_diagram", "sphinx.ext.graphviz", "matplotlib.sphinxext.plot_directive", "sphinx_gallery.gen_gallery", ] # Autosummary pages will be generated by sphinx-autogen instead of sphinx-build autosummary_generate = True numpydoc_attributes_as_param_list = False numpydoc_show_inherited_class_members = { "discretize.base.BaseMesh": False, "discretize.base.BaseRegularMesh": False, "discretize.base.BaseRectangularMesh": False, "discretize.base.BaseTensorMesh": False, "discretize.operators.DiffOperators": False, "discretize.operators.InnerProducts": False, "discretize.mixins.TensorMeshIO": False, "discretize.mixins.TreeMeshIO": False, "discretize.mixins.InterfaceMPL": False, "discretize.mixins.InterfaceVTK": False, "discretize.mixins.InterfaceOMF": False, "discretize.mixins.Slicer": False, "discretize.tests.OrderTest": False, } # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source file names. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "discretize" copyright = "2013 - {}, SimPEG Developers, http://simpeg.xyz".format( datetime.now().year ) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = version("discretize") discretize_version = parse(release) # The short X.Y version. version = discretize_version.public if discretize_version.is_devrelease: branch = "main" else: branch = f"v{version}" # The short X.Y version. # version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] linkcheck_ignore = [ r"https://github.com/simpeg/*", ] linkcheck_retries = 3 linkcheck_timeout = 500 # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # source code links link_github = True # You can build old with link_github = False if link_github: import inspect extensions.append("sphinx.ext.linkcode") def linkcode_resolve(domain, info): """ Determine the URL corresponding to Python object """ if domain != "py": return None modname = info["module"] fullname = info["fullname"] submod = sys.modules.get(modname) if submod is None: return None obj = submod for part in fullname.split("."): try: obj = getattr(obj, part) except AttributeError: return None if inspect.isfunction(obj): obj = inspect.unwrap(obj) try: fn = inspect.getsourcefile(obj) except TypeError: fn = None if not fn or fn.endswith("__init__.py"): try: fn = inspect.getsourcefile(sys.modules[obj.__module__]) except (TypeError, AttributeError, KeyError): fn = None if not fn: return None try: source, lineno = inspect.getsourcelines(obj) except (OSError, TypeError): lineno = None if lineno: linespec = f"#L{lineno:d}-L{lineno + len(source) - 1:d}" else: linespec = "" try: fn = os.path.relpath(fn, start=os.path.dirname(discretize.__file__)) except ValueError: return None return f"https://github.com/simpeg/discretize/blob/{branch}/discretize/{fn}{linespec}" else: extensions.append("sphinx.ext.viewcode") # Make numpydoc to generate plots for example sections numpydoc_use_plots = True plot_pre_code = """ import numpy as np np.random.seed(0) """ plot_include_source = True plot_formats = [("png", 100), "pdf"] import math phi = (math.sqrt(5) + 1) / 2 plot_rcparams = { "font.size": 8, "axes.titlesize": 8, "axes.labelsize": 8, "xtick.labelsize": 8, "ytick.labelsize": 8, "legend.fontsize": 8, "figure.figsize": (3 * phi, 3), "figure.subplot.bottom": 0.2, "figure.subplot.left": 0.2, "figure.subplot.right": 0.9, "figure.subplot.top": 0.85, "figure.subplot.wspace": 0.4, "text.usetex": False, } # -- Options for HTML output --------------------------------------------------- external_links = [ dict(name="SimPEG", url="https://simpeg.xyz"), dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), ] # Use Pydata Sphinx theme html_theme = "pydata_sphinx_theme" # If false, no module index is generated. html_use_modindex = True html_theme_options = { "external_links": external_links, "icon_links": [ { "name": "GitHub", "url": "https://github.com/simpeg/discretize", "icon": "fab fa-github", }, { "name": "Mattermost", "url": "https://mattermost.softwareunderground.org/simpeg", "icon": "fas fa-comment", }, { "name": "Discourse", "url": "https://simpeg.discourse.group/", "icon": "fab fa-discourse", }, { "name": "Youtube", "url": "https://www.youtube.com/c/geoscixyz", "icon": "fab fa-youtube", }, ], "use_edit_page_button": False, "collapse_navigation": True, "navbar_align": "left", # make elements closer to logo on the left "navbar_end": ["version-switcher", "theme-switcher", "navbar-icon-links"], # Configure version switcher (remember to add it to the "navbar_end") "switcher": { "version_match": "dev" if branch == "main" else branch, "json_url": "https://discretize.simpeg.xyz/en/main/_static/versions.json", }, "show_version_warning_banner": True, } html_logo = "images/discretize-logo.png" html_static_path = ["_static"] html_css_files = [ "css/custom.css", ] html_context = { "github_user": "simpeg", "github_repo": "discretize", "github_version": "main", "doc_path": "docs", } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = os.path.sep.join([".", "images", "discretize-block.ico"]) # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "discretize" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "discretize.tex", "discretize documentation", "SimPEG Developers", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "simpeg", "discretize Documentation", ["SimPEG Developers"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # Intersphinx intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable", None), "scipy": ("https://docs.scipy.org/doc/scipy", None), "matplotlib": ("https://matplotlib.org/stable", None), "pyvista": ("https://docs.pyvista.org", None), "omf": ("https://omf.readthedocs.io/en/stable", None), } numpydoc_xref_param_type = True # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "discretize", "discretize documentation", "SimPEG Developers", "discretize", "Finite volume methods for python.", "Miscellaneous", ), ] # -- pyvista configuration --------------------------------------------------- import pyvista # Manage errors pyvista.set_error_output_file("errors.txt") # Ensure that offscreen rendering is used for docs generation pyvista.OFF_SCREEN = True # Not necessary - simply an insurance policy # Preferred plotting style for documentation pyvista.set_plot_theme("document") pyvista.global_theme.window_size = [1024, 768] pyvista.global_theme.font.size = 22 pyvista.global_theme.font.label_size = 22 pyvista.global_theme.font.title_size = 22 pyvista.global_theme.return_cpos = False pyvista.set_jupyter_backend(None) # Save figures in specified directory pyvista.FIGURE_PATH = os.path.join(os.path.abspath("./images/"), "auto-generated/") if not os.path.exists(pyvista.FIGURE_PATH): os.makedirs(pyvista.FIGURE_PATH) # necessary when building the sphinx gallery pyvista.BUILDING_GALLERY = True os.environ["PYVISTA_BUILDING_GALLERY"] = "true" # Sphinx Gallery sphinx_gallery_conf = { # path to your examples scripts "examples_dirs": [ "../examples", "../tutorials/mesh_generation", "../tutorials/operators", "../tutorials/inner_products", "../tutorials/pde", ], "gallery_dirs": [ "examples", "tutorials/mesh_generation", "tutorials/operators", "tutorials/inner_products", "tutorials/pde", ], "within_subsection_order": "FileNameSortKey", "filename_pattern": "\\.py", "backreferences_dir": "api/generated/backreferences", "doc_module": "discretize", "image_scrapers": ("pyvista", "matplotlib"), } # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' graphviz_dot = shutil.which("dot") # this must be png, because links on SVG are broken graphviz_output_format = "png" autodoc_member_order = "bysource" nitpick_ignore = [ ("py:class", "discretize.CurvilinearMesh.Array"), ("py:class", "discretize.mixins.vtk_mod.InterfaceTensorread_vtk"), ("py:class", "callable"), ] ================================================ FILE: docs/content/additional_resources.rst ================================================ .. _additional_resources: Additional Resources ==================== An enormous amount of information (including tutorials and examples) can be found on the official websites of the packages * `Python Website `_ * `Numpy Website `_ * `SciPy Website `_ * `Matplotlib `_ Python for scientific computing ------------------------------- * `Learn Python `_ Links to commonly used packages, Matlab to Python comparison * `Python Wiki `_ Lists packages and resources for scientific computing in Python Numpy and Matlab ---------------- * `NumPy for Matlab Users `_ * `Python vs Matlab `_ Lessons in Python ----------------- * `Software Carpentry `_ * `Introduction to NumPy and Matplotlib `_ Editing Python -------------- There are numerous ways to edit and test Python (see `PythonWiki `_ for an overview) and in our group at least the following options are being used: * `Sublime `_ * `Jupyter `_ ================================================ FILE: docs/content/big_picture.rst ================================================ Why discretize? =============== Inverse problems are common across the geosciences: imaging in geophysics, history matching, parameter estimation, and many of these require constrained optimization using partial differential equations (PDEs) where the derivative of mesh variables are sought. Finite difference, finite element and finite volume techniques allow subdivision of continuous differential equations into discrete domains. The knowledge and appropriate application of these methods is fundamental to simulating physical processes. Many inverse problems in the geosciences are solved using stochastic techniques or external finite difference based tools (e.g. PEST); these are robust to local minima and the programmatic implementation, respectively, however these methods do not scale to millions of parameters to be estimated. This sort of scale is necessary for solving many of the inverse problems in geophysics and increasingly hydrogeology (e.g. electromagnetics, gravity, and fluid flow problems). In the context of the inverse problem, when the physical properties, the domain, and the boundary conditions are not necessarily known, the simplicity and efficiency in mesh generation are important criteria. Complex mesh geometries, such as body fitted grids, commonly used when the domain is explicitly given, are less appropriate. Additionally, when considering the inverse problem, it is important that operators and their derivatives are accessible to interrogation and extension. The goal of this work is to provide a high level background to finite volume techniques abstracted across four mesh types: 1) tensor product mesh :class:`discretize.TensorMesh` 2) cylindrically symmetric mesh :class:`discretize.CylMesh` 3) curvilinear mesh :class:`discretize.CurvilinearMesh` 4) octree and quadtree meshes :class:`discretize.TreeMesh` :code:`discretize` contributes an overview of finite volume techniques in the context of geoscience inverse problems, which are treated in a consistent way across various mesh types, highlighting similarities and differences. .. include:: ../../CITATION.rst Authors ------- .. include:: ../../AUTHORS.rst License ------- .. include:: ../../LICENSE ================================================ FILE: docs/content/finite_volume.rst ================================================ .. _api_FiniteVolume: Finite Volume ************* Any numerical implementation requires the discretization of continuous functions into discrete approximations. These approximations are typically organized in a mesh, which defines boundaries, locations, and connectivity. Of specific interest to geophysical simulations, we require that averaging, interpolation and differential operators be defined for any mesh. In SimPEG, we have implemented a staggered mimetic finite volume approach (`Hyman and Shashkov, 1999 `_). This approach requires the definitions of variables at either cell-centers, nodes, faces, or edges as seen in the figure below. .. image:: ../images/finitevolrealestate.png :width: 400 px :alt: FiniteVolume :align: center ================================================ FILE: docs/content/getting_started.rst ================================================ .. _getting_started: =============== Getting Started =============== Here you'll find instructions on getting up and running with ``discretize``. .. toctree:: :maxdepth: 2 big_picture installing additional_resources ================================================ FILE: docs/content/inner_products.rst ================================================ Inner Products ************** By using the weak formulation of many of the PDEs in geophysical applications, we can rapidly develop discretizations. Much of this work, however, needs a good understanding of how to approximate inner products on our discretized meshes. We will define the inner product as: .. math:: \left(a,b\right) = \int_\Omega{a \cdot b}{\partial v} where a and b are either scalars or vectors. .. note:: The InnerProducts class is a base class providing inner product matrices for meshes and cannot run on its own. Example problem for DC resistivity ---------------------------------- We will start with the formulation of the Direct Current (DC) resistivity problem in geophysics. .. math:: \frac{1}{\sigma}\vec{j} = \nabla \phi \\ \nabla\cdot \vec{j} = q In the following discretization, :math:`\sigma` and :math:`\phi` will be discretized on the cell-centers and the flux, :math:`\vec{j}`, will be on the faces. We will use the weak formulation to discretize the DC resistivity equation. We can define in weak form by integrating with a general face function :math:`\vec{f}`: .. math:: \int_{\Omega}{\sigma^{-1}\vec{j} \cdot \vec{f}} = \int_{\Omega}{\nabla \phi \cdot \vec{f}} Here we can integrate the right side by parts, .. math:: \nabla\cdot(\phi\vec{f})=\nabla\phi\cdot\vec{f} + \phi\nabla\cdot\vec{f} and rearrange it, and apply the Divergence theorem. .. math:: \int_{\Omega}{\sigma^{-1}\vec{j} \cdot \vec{f}} = - \int_{\Omega}{(\phi \nabla \cdot \vec{f})} + \int_{\partial \Omega}{ \phi \vec{f} \cdot \mathbf{n}} We can then discretize for every cell: .. math:: v_{\text{cell}} \sigma^{-1} (\mathbf{J}_x \mathbf{F}_x +\mathbf{J}_y \mathbf{F}_y + \mathbf{J}_z \mathbf{F}_z ) = -\phi^{\top} v_{\text{cell}} \mathbf{D}_{\text{cell}} \mathbf{F} + \text{BC} .. note:: We have discretized the dot product above, but remember that we do not really have a single vector :math:`\mathbf{J}`, but approximations of :math:`\vec{j}` on each face of our cell. In 2D that means 2 approximations of :math:`\mathbf{J}_x` and 2 approximations of :math:`\mathbf{J}_y`. In 3D we also have 2 approximations of :math:`\mathbf{J}_z`. Regardless of how we choose to approximate this dot product, we can represent this in vector form (again this is for every cell), and will generalize for the case of anisotropic (tensor) sigma. .. math:: \mathbf{F}_c^{\top} (\sqrt{v_{\text{cell}}} \Sigma^{-1} \sqrt{v_{\text{cell}}}) \mathbf{J}_c = -\phi^{\top} v_{\text{cell}} \mathbf{D}_{\text{cell}} \mathbf{F}) + \text{BC} We multiply by square-root of volume on each side of the tensor conductivity to keep symmetry in the system. Here :math:`\mathbf{J}_c` is the Cartesian :math:`\mathbf{J}` (on the faces that we choose to use in our approximation) and must be calculated differently depending on the mesh: .. math:: \mathbf{J}_c = \mathbf{Q}_{(i)}\mathbf{J}_\text{TENSOR} \\ \mathbf{J}_c = \mathbf{N}_{(i)}^{-1}\mathbf{Q}_{(i)}\mathbf{J}_\text{Curv} Here the :math:`i` index refers to where we choose to approximate this integral, as discussed in the note above. We will approximate this integral by taking the fluxes clustered around every node of the cell, there are 8 combinations in 3D, and 4 in 2D. We will use a projection matrix :math:`\mathbf{Q}_{(i)}` to pick the appropriate fluxes. So, now that we have 8 approximations of this integral, we will just take the average. For the TensorMesh, this looks like: .. math:: \mathbf{F}^{\top} {1\over 8} \left(\sum_{i=1}^8 \mathbf{Q}_{(i)}^{\top} \sqrt{v_{\text{cell}}} \Sigma^{-1} \sqrt{v_{\text{cell}}} \mathbf{Q}_{(i)} \right) \mathbf{J} = -\mathbf{F}^{\top} \mathbf{D}_{\text{cell}}^{\top} v_{\text{cell}} \phi + \text{BC} Or, when generalizing to the entire mesh and dropping our general face function: .. math:: \mathbf{M}^f_{\Sigma^{-1}} \mathbf{J} = - \mathbf{D}^{\top} \text{diag}(\mathbf{v}) \phi + \text{BC} By defining the faceInnerProduct (8 combinations of fluxes in 3D, 4 in 2D, 2 in 1D) to be: .. math:: \mathbf{M}^f_{\Sigma^{-1}} = \sum_{i=1}^{2^d} \mathbf{P}_{(i)}^{\top} \Sigma^{-1} \mathbf{P}_{(i)} Where :math:`d` is the dimension of the mesh. The :math:`\mathbf{M}^f` is returned when given the input of :math:`\Sigma^{-1}`. Here each :math:`\mathbf{P} ~ \in ~ \mathbb{R}^{(d*nC, nF)}` is a combination of the projection, volume, and any normalization to Cartesian coordinates (where the dot product is well defined): .. math:: \mathbf{P}_{(i)} = \sqrt{ \frac{1}{2^d} \mathbf{I}^d \otimes \text{diag}(\mathbf{v})} \overbrace{\mathbf{N}_{(i)}^{-1}}^{\text{Curv only}} \mathbf{Q}_{(i)} .. note:: This is actually completed for each cell in the mesh at the same time, and the full matrices are returned. If ``returnP=True`` is requested in any of these methods the projection matrices are returned as a list ordered by nodes around which the fluxes were approximated:: # In 3D P = [P000, P100, P010, P110, P001, P101, P011, P111] # In 2D P = [P00, P10, P01, P11] # In 1D P = [P0, P1] The derivation for ``edgeInnerProducts`` is exactly the same, however, when we approximate the integral using the fields around each node, the projection matrices look a bit different because we have 12 edges in 3D instead of just 6 faces. The interface to the code is exactly the same. Defining Tensor Properties -------------------------- **For 3D:** Depending on the number of columns (either 1, 3, or 6) of mu, the material property is interpreted as follows: .. math:: \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 & 0 \\ 0 & \mu_{1} & 0 \\ 0 & 0 & \mu_{1} \end{matrix}\right] \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 & 0 \\ 0 & \mu_{2} & 0 \\ 0 & 0 & \mu_{3} \end{matrix}\right] \vec{\mu} = \left[\begin{matrix} \mu_{1} & \mu_{4} & \mu_{5} \\ \mu_{4} & \mu_{2} & \mu_{6} \\ \mu_{5} & \mu_{6} & \mu_{3} \end{matrix}\right] **For 2D:** Depending on the number of columns (either 1, 2, or 3) of mu, the material property is interpreted as follows: .. math:: \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 \\ 0 & \mu_{1} \end{matrix}\right] \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 \\ 0 & \mu_{2} \end{matrix}\right] \vec{\mu} = \left[\begin{matrix} \mu_{1} & \mu_{3} \\ \mu_{3} & \mu_{2} \end{matrix}\right] Structure of Matrices --------------------- Both the isotropic, and anisotropic material properties result in a diagonal mass matrix. Which is nice and easy to invert if necessary, however, in the fully anisotropic case which is not aligned with the grid, the matrix is not diagonal. This can be seen for a 3D mesh in the figure below. .. plot:: import discretize import numpy as np import matplotlib.pyplot as plt mesh = discretize.TensorMesh([10,50,3]) m1 = np.random.rand(mesh.nC) m2 = np.random.rand(mesh.nC,3) m3 = np.random.rand(mesh.nC,6) M = list(range(3)) M[0] = mesh.get_face_inner_product(m1) M[1] = mesh.get_face_inner_product(m2) M[2] = mesh.get_face_inner_product(m3) plt.figure(figsize=(13,5)) for i, lab in enumerate(['Isotropic','Anisotropic','Tensor']): plt.subplot(131 + i) plt.spy(M[i],ms=0.5,color='k') plt.tick_params(axis='both',which='both',labeltop='off',labelleft='off') plt.title(lab + ' Material Property') plt.show() Taking Derivatives ------------------ We will take the derivative of the fully anisotropic tensor for a 3D mesh, the other cases are easier and will not be discussed here. Let us start with one part of the sum which makes up :math:`\mathbf{M}^f_\Sigma` and take the derivative when this is multiplied by some vector :math:`\mathbf{v}`: .. math:: \mathbf{P}^\top \boldsymbol{\Sigma} \mathbf{Pv} Here we will let :math:`\mathbf{Pv} = \mathbf{y}` and :math:`\mathbf{y}` will have the form: .. math:: \mathbf{y} = \mathbf{Pv} = \left[ \begin{matrix} \mathbf{y}_1\\ \mathbf{y}_2\\ \mathbf{y}_3\\ \end{matrix} \right] .. math:: \mathbf{P}^\top\Sigma\mathbf{y} = \mathbf{P}^\top \left[\begin{matrix} \boldsymbol{\sigma}_{1} & \boldsymbol{\sigma}_{4} & \boldsymbol{\sigma}_{5} \\ \boldsymbol{\sigma}_{4} & \boldsymbol{\sigma}_{2} & \boldsymbol{\sigma}_{6} \\ \boldsymbol{\sigma}_{5} & \boldsymbol{\sigma}_{6} & \boldsymbol{\sigma}_{3} \end{matrix}\right] \left[ \begin{matrix} \mathbf{y}_1\\ \mathbf{y}_2\\ \mathbf{y}_3\\ \end{matrix} \right] = \mathbf{P}^\top \left[ \begin{matrix} \boldsymbol{\sigma}_{1}\circ \mathbf{y}_1 + \boldsymbol{\sigma}_{4}\circ \mathbf{y}_2 + \boldsymbol{\sigma}_{5}\circ \mathbf{y}_3\\ \boldsymbol{\sigma}_{4}\circ \mathbf{y}_1 + \boldsymbol{\sigma}_{2}\circ \mathbf{y}_2 + \boldsymbol{\sigma}_{6}\circ \mathbf{y}_3\\ \boldsymbol{\sigma}_{5}\circ \mathbf{y}_1 + \boldsymbol{\sigma}_{6}\circ \mathbf{y}_2 + \boldsymbol{\sigma}_{3}\circ \mathbf{y}_3\\ \end{matrix} \right] Now it is easy to take the derivative with respect to any one of the parameters, for example, :math:`\frac{\partial}{\partial\boldsymbol{\sigma}_1}` .. math:: \frac{\partial}{\partial \boldsymbol{\sigma}_1}\left(\mathbf{P}^\top\Sigma\mathbf{y}\right) = \mathbf{P}^\top \left[ \begin{matrix} \text{diag}(\mathbf{y}_1)\\ 0\\ 0\\ \end{matrix} \right] Whereas :math:`\frac{\partial}{\partial\boldsymbol{\sigma}_4}`, for example, is: .. math:: \frac{\partial}{\partial \boldsymbol{\sigma}_4}\left(\mathbf{P}^\top\Sigma\mathbf{y}\right) = \mathbf{P}^\top \left[ \begin{matrix} \text{diag}(\mathbf{y}_2)\\ \text{diag}(\mathbf{y}_1)\\ 0\\ \end{matrix} \right] These are computed for each of the 8 projections, horizontally concatenated, and returned. ================================================ FILE: docs/content/installing.rst ================================================ .. _api_installing: Installing ********** Which Python? ============= Currently, ``discretize`` is tested on python 3.10 through 3.12. We recommend that you use the latest version of Python available on `Anaconda `_. Installing Python ----------------- Python is available on all major operating systems, but if you are getting started with python it is best to use a package manager such as `Anaconda `_. You can download the package manager and use it to install the dependencies above. .. note:: When using Continuum Anaconda, make sure to run:: conda update conda conda update anaconda .. _discretize_dependencies: Dependencies ============ ``discretize``'s runtime requirements are: - `numpy `_ 1.22.4 (or greater) - `scipy `_ 1.8 (or greater) Additional functionality is provided when the following optional packages are installed: - `matplotlib `_ - `pyvista `_ - `vtk `_ - `omf `_ We also recommend installing: - `pymatsolver `_ 0.1.2 (or greater) Installing discretize ===================== ``discretize`` is available on conda-forge and using the ``conda`` (or ``mamba``) package manager is our recommended way of installing `discretize``:: conda install -c conda-forge discretize ``discretize`` is also available on pypi:: pip install discretize There are currently pre-built wheels for windows available on pypi, but other operating systems will require a build. Installing from Source ---------------------- .. attention:: Install ``discretize`` from the source code only if you need to run the development version. Otherwise it's usually better to install it from ``conda-forge`` or ``pypi``. As ``discretize`` contains several compiled extensions and is not a pure python pacakge, installing ``discretize`` from the source code requires a C/C++ compiler capable of using a C++ 17 standard. ``discretize`` uses a ``pyproject.toml`` file to define the build and install steps. As such there is no ``setup.py`` file to run. You must use ``pip`` to install ``discretize``. As long as you have an available compiler you should be able to install ``discretize`` from the source as:: pip install . .. note:: For Windows users, make sure you are using compilers that are compatible with your python installation. If you have gnu compilers on your system, ``meson`` will default to using those, and you might have to force ``meson`` to use the ``msvc`` compilers by appending:: --config-settings=setup-args="--vsenv" to the ``pip`` installation command. Editable Installs ^^^^^^^^^^^^^^^^^ If you are an active developer of ``discretize``, and find yourself modifying the code often, you might want to install it from source, in an editable installation. ``discretize`` uses ``meson-python`` to build the external modules and install the package. As such, there are a few extra steps to take: 1. Make sure you have the runtime dependencies installed in your environment (see :ref:`discretize_dependencies` listed above). However, you **must** install ``numpy >=2.0`` when *building* ``discretize``. 2. You must also install packages needed to build ``discretize`` into your environment. You can do so with ``pip``:: pip install meson-python meson ninja cython setuptools_scm Or with ``conda``:: conda install -c conda-forge meson-python meson ninja cython setuptools_scm This will allow you to use the build backend required by `discretize`. 3. Finally, you should then be able to perform an editable install using the source code:: pip install --no-build-isolation --editable . This builds and installs the local directory to your active python environment in an "editable" mode; when source code is changed, you will be able to make use of it immediately. It also builds against the packages installed in your environment instead of creating and isolated environment to build a wheel for the package, which is why we needed to install the build requirements into the environment. Testing your installation ========================= Head over to the :ref:`sphx_glr_examples` and download and run any of the notebooks or python scripts. ================================================ FILE: docs/content/theory.rst ================================================ .. _theory: ====== Theory ====== This section is a resource for the fundamental finite volume theory used behind `discretize`. .. toctree:: :maxdepth: 2 finite_volume inner_products ================================================ FILE: docs/content/user_guide.rst ================================================ .. _user_guide: ========== User Guide ========== We've included some tutorials and gallery examples that will walk you through using discretize to solve your PDE. For more details on any of the functions, check out the API documentation. Tutorials --------- .. toctree:: :maxdepth: 2 ../tutorials/mesh_generation/index ../tutorials/operators/index ../tutorials/inner_products/index ../tutorials/pde/index Examples -------- .. toctree:: :maxdepth: 2 ../examples/index ================================================ FILE: docs/index.rst ================================================ .. include:: ../README.rst .. toctree:: :maxdepth: 3 :hidden: :titlesonly: content/getting_started content/user_guide api/index content/theory release/index ================================================ FILE: docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SimPEG.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SimPEG.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end ================================================ FILE: docs/release/0.10.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.10.0_notes: =================================== ``discretize`` 0.10.0 Release Notes =================================== October 27, 2023 This minor release changes ``discretize`` to use a ``pyproject.toml`` file with a ``meson-python`` build backend, and does away with the old `setup.py` style. This should allow the package to more reliably be built from the source code, properly setting the build-requirements. It also adds more functionality for integrating properties that are defined on cell faces and edges for finite volume formulations (previously we only supported integrating with properties that were defined on cell centers). Build system ------------ ``discretize`` now uses a ``pyproject.toml`` file with a ``meson-python`` backend to build the compiled external modules (used for the ``TreeMesh``, ``SimplexMesh``, and interpolation functions. Moving away from a ``setup.py`` file allows us to reliably control the build environment separate from the install environment, as the build requirements are not the same as the runtime requirements. We will also begin distributing many more pre-compiled wheels on pypi for Windows, MacOS, and Linux systems from python 3.8 to 3.12. Our goal is to provide a pre-compiled wheel for every system that scipy provides wheels for. Tensor Mesh ----------- You can now directly index a ``TensorMesh`` and return a ``TensorCell`` object. This functionality mimics what is currently available in ``TreeMesh``. ``TensorMesh`` also has a ``cell_nodes`` property that list the indices of each node of every cell (again similar to the ``TreeMesh``). Face Properties --------------- All meshes now have new functionality to integrate properties that are defined on cell faces and cell meshes within the finite volume formulation. Style updates ------------- The pre-commit config files for discretize have been updated to more recent versions of ``black`` and ``flake8``. Contributors ============ * @jcapriot * @santisoler * @dccowan * @munahaf Pull requests ============= * `#327 `__: Add black, flake8 and flake8 plugins to environment file * `#328 `__: Use any Python 3 in pre-commit * `#329 `__: Simplex stashing * `#325 `__: Add new TensorCell class * `#330 `__: Configure pyvista for doc builds * `#331 `__: Add a noexcept clause to the wrapper function * `#326 `__: Face props mass matrices * `#335 `__: Pin flake8 * `#333 `__: Add cell_nodes property to TensorMesh * `#339 `__: Update a test expression to fix a logical short circuit * `#340 `__: Add export config for git archives * `#338 `__: Pyproject.toml * `#342 `__: CIbuildwheel * `#342 `__: 0.10.0 Release Notes ================================================ FILE: docs/release/0.11.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.11.0_notes: =================================== ``discretize`` 0.11.0 Release Notes =================================== October 24, 2024 This minor release contains many bugfixes and updates related to new package builds. Numpy 2 ------- `discretize` now fully supports `numpy` 2! It is both built against and tested against `numpy` 2.0. It still has a minimum required runtime of `numpy` 1.22 though, as building against numpy 2.0 emits ABI compatible calls for older numpy versions. Of note to developers, we now require `numpy` 2.0 for building as it makes use of the `numpy-config` tool to locate the `numpy` include directory. Python versions --------------- `discretize` has bumped its minimum supported `python` version to 3.10, and is tested against versions 3.10-3.13. In the future we intended to stay in line with the minimum `python` version supported by the most recent `numpy` release. Random Generators ----------------- `discretize` and its testing utilities now make use of ``numpy.random.RandomGenerator`` to make draws from random number generators instead of the deprecated ``numpy.random.rand`` functions. These functions now support a new keyword argument `random_seed` : * :func:``discretize.tests.setup_mesh`` (only used when ``"random" in mesh_type``) * :func:``discretize.tests.check_derivative`` (only used when ``dx=None``) * :func:``discretize.tests.assert_isadjoint`` * :func:``discretize.tests.OrderTest.orderTest`` (only used when ``"random" in mesh_type``) * :func:``discretize.utils.random_model`` Maintainers of downstream packages should explicitly set seeded generators when using these methods for testing purposess to ensure reproducibility. Cell Bounds ----------- :class:``discretize.TensorMesh`` and :class:``discretize.TreeMesh`` now have a ``cell_bounds`` property that returns the ``(x_min, x_max, y_min, y_max, z_min, z_max)`` of every cell in the mesh at once. Also now the :class:``discretize.tree_mesh.TreeCell`` has a corresponding ``bounds`` property. ``TreeMesh`` updates -------------------- You can now query a :class:``discretize.TreeMesh`` for cells contained in the same geometric primitives that are supported for refining. In addition there is a new :func:``discretize.TreeMesh.refine_plane`` method for refining along a plane. Contributors ============ * @jcapriot * @santisoler * @prisae * @xiaolongw1223 * @lheagy * @omid-b Pull requests ============= * `#347 `__: Replace deprecated Numpy's `product` by `prod` * `#351 `__: Replace Slack links for Mattermost links * `#353 `__: Fix typo in tutorials * `#354 `__: Update year in LICENSE * `#356 `__: Expose TreeMesh geometric intersections used for refine functions. * `#358 `__: Replace hanging CurviMesh in docstring for CurvilinearMesh * `#360 `__: Update use of `numpy`'s random number generators. * `#364 `__: Fix slicer re #363 * `#366 `__: Add `TensorMesh.cell_bounds` property * `#367 `__: Add `TreeCell.bounds` and `TreeMesh.cell_bounds` methods * `#368 `__: Set minimum to Python 3.10 (and general CI Maintenance) * `#371 `__: Add version switcher to discretize docs * `#372 `__: Deploy docs to a new folder named after their tagged version * `#373 `__: display dev doc banner * `#374 `__: Bump pydata_sphinx_theme version to 0.15.4 * `#375 `__: Fix caching of internal projection matrices * `#376 `__: Fix macos-latest build * `#379 `__: Numpy2.0 updates * `#380 `__: Create build_distributions.yml * `#381 `__: 0.11.0 Release Notes ================================================ FILE: docs/release/0.11.1-notes.rst ================================================ .. currentmodule:: discretize .. _0.11.1_notes: =================================== ``discretize`` 0.11.1 Release Notes =================================== November 5, 2024 This is a bugfix release for issues found in the previous release, and adds some warning messages for users. Updates ======= Warning Messages ---------------- * Added a warning to the `TreeMesh` informing of incoming future behavoir changes regarding diagonal balancing. This will default to ``True`` in `discretize` 1.0. * Added warning messages to test functions using uncontrolled randomized input that appear when run under `pytest` or `nosetest`, alerting the user to non-repeatable tests. * Changed the default for ``plotIt`` argument to ``False`` for testing functions. Fixed Bugs ---------- * `TreeMesh.point2index` now refers to the correction function. Contributors ============ * @jcapriot Pull requests ============= * Outstanding bugfixes. by @jcapriot in `#382 `__. * Warn for non-repeatable random tests in a testing environment by @jcapriot in `#384 `__. * Staging for 0.11.1 by @jcapriot in `#385 `__. ================================================ FILE: docs/release/0.11.2-notes.rst ================================================ .. currentmodule:: discretize .. _0.11.2_notes: =================================== ``discretize`` 0.11.2 Release Notes =================================== January 28, 2025 This is a bugfix release for discretize with some minor updates. Updates ======= Fixed Bugs ---------- * ``is_scalar`` will now return true for any numpy array broadcastable as a scalar (any array with ``arr.size == 1``). * Explicitly set diagonal balancing on internal plotting and reading routines that build a ``TreeMesh``. * Fixes formatting across line breaks in new warnings. * the ``Zero`` and ``Identity`` classes now return expected truthiness values: ``bool(Zero()) == False`` and ``bool(Identity()) == True``. Contributors ============ * @jcapriot * @santisoler * @prisae Pull requests ============= * improve scalar test to handle arbitrary dimensional ndarrays by @jcapriot in `#388 `__. * Set no diagonal balance when reading UBC tree meshes by @santisoler in `#386 `__ * Fix small typos in diagonal_balance warning by @santisoler in `#387 `__ * Implement truthiness for Zero and Identity by @jcapriot in `#389 `__ * Fix formatting new warning by @prisae in `#390 `__ * v0.11.2 staging @jcapriot in `#391 `__ ================================================ FILE: docs/release/0.11.3-notes.rst ================================================ .. currentmodule:: discretize .. _0.11.3_notes: =================================== ``discretize`` 0.11.3 Release Notes =================================== June 6, 2025 This is a bugfix release for discretize. Updates ======= Fixed Bugs ---------- * Updates for Cython import deprecations. * Allows safely generating string representations of non-finalized `TreeMesh`. * Adds error guards when attempting to access a property or method that requires a finalized `TreeMesh`. Contributors ============ * @jcapriot * @santisoler Pull requests ============= * Python3.13 by @jcapriot in `#377 `__ * Allow `TreeMesh.__repr__` to run when non finalized by @santisoler in `#393 `__ * Add safeguards to TreeMesh properties by @santisoler in `#394 `__ * Switches to libc math import by @jcapriot in `#396 `__ * 0.11.3 staging by @jcapriot in `#3967 `__ ================================================ FILE: docs/release/0.12.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.12.0_notes: =================================== ``discretize`` 0.12.0 Release Notes =================================== October 8, 2025 This minor release contains many bugfixes and updates related to new package builds. Python versions --------------- `discretize` has bumped its minimum supported `python` version to 3.11, and is tested against versions 3.11-3.14. Minimum scipy versions have also been bumped to 1.12. Users on older python versions should continue to use `discretize` 0.11.x. We have also added support for (and tests against) free-threaded python builds for python 3.13 and later, which should be available through the normal python distribution channels (pypi, conda-forge). ``TreeMesh`` updates -------------------- Tree meshes now support a `refine_image` method that allows users to refine a mesh based on an image (2D or 3D numpy array). See :func:``discretize.TreeMesh.refine_image`` for more details. ``TensorMesh`` updates ---------------------- A new :func:``discretize.TensorMesh.point2index`` method has been added to convert a point location to the corresponding cell index in a tensor mesh, similar to the existing :func:``discretize.TreeMesh.point2index`` method. Contributors ============ * @jcapriot Pull requests ============= * bump sphinx and pydata-sphinx to more recent versions by @jcapriot in `#402 `__ * Small cleanups to the external TreeMesh code, no functionality changes by @jcapriot in `#400 `__ * Add point2index functionality for `tensor_mesh` by @jcapriot in `#401 `__ * Updates for python free threading by @jcapriot in `#403 `__ * Updates for cibuildwheel by @jcapriot in `#405 `__ * Add functionality to refine a `TreeMesh` using an "image" by @jcapriot in `#406 `__ ================================================ FILE: docs/release/0.4.12-notes.rst ================================================ .. currentmodule:: discretize .. _0.4.12_notes: =================================== ``discretize`` 0.4.12 Release Notes =================================== June 6, 2020 This patch release is for a few small bugs and code speed improvements. There is now a fast function for return active cell indexes for ``TensorMesh``-s, ``TreeMesh``-s, and symmetric ``CylMesh``-s below a topography surface defined by scattered points, ``discretize.utils.active_from_xyz``. There is also a bug fix of `#197 `__ for the ``discretize.utils.refine_tree_xyz`` helper function on QuadTree meshes when attempting to extend the padding vertically using ``method=surface``. There was also a bug when reading in a 2D UBC mesh that would only manifest if the UBC mesh's origin wasn't at the surface (`#194 `__). We have updated links as we are now using discourse over google groups as a means for users to ask for general help. A few links to the SimPEG documentation needed to be updated for the re-organization of the examples folders. We are removing ``pymatsolver`` from the list of explicit dependancies for ``discretize``. It is **highly** recommended, but it isn't actually required to run anything within the ``discretize`` package. Contributors ============ * @domfournier/@fourndo * @jcapriot * @lheagy * @prisae Bug Fixes ========= * `#194 `__: z-dimension in write/readUBC * `#197 `__: Amount of cells not changing when changing Octree levels in utils.refine_tree_xyz Pull requests ============= * `#193 `__: Remove pymatsolver * `#198 `__: Refine tree xyz * `#201 `__: Update README.rst * `#203 `__: consolidating tests on travis * `#205 `__: Update requirements * `#206 `__: Bug fix for 2D mesh read in. * `#207 `__: update release notes ================================================ FILE: docs/release/0.4.13-notes.rst ================================================ .. currentmodule:: discretize .. _0.4.13_notes: =================================== ``discretize`` 0.4.13 Release Notes =================================== June 22, 2020 This release contains some bug fixes: First, we have squashed a few bugs related to serializing the TreeMesh to a json object. ``tree_mesh.save`` should now work. We have also added the ability to write out a 2D ``TreeMesh`` to a UBC-GIF-like format. There is no official QuadTree mesh from UBC-GIF however, so this just mimics the 3D format for a 2D mesh. There was also a small bug in reading in a 2D UBC TensorMesh model when the values were split over multiple lines. We have altered the logic here so as to be independant of the structure of that file, as the values should always appear in a specific order. Contributors ============ * @dccowan * @jcapriot * @prisae Pull requests ============= * `#204 `__: Remove restriction of sphinx < 2 * `#208 `__: Create IO for 2D Tree mesh in UBC-like format * `#209 `__: small bug fix for 2D UBC TensorMesh model readin ================================================ FILE: docs/release/0.4.14-notes.rst ================================================ .. currentmodule:: discretize .. _0.4.14_notes: =================================== ``discretize`` 0.4.14 Release Notes =================================== July 4, 2020 This release is renaming a few of the options to the plotting routines to more pep8 friendly names. The older name will still function, but throw a deprecation warning * ``vType`` → ``v_type`` * ``pcolorOpts`` → ``pcolor_opts`` * ``streamOpts`` → ``stream_opts`` * ``gridOpts`` → ``grid_opts`` * ``showIt`` → ``show_it`` * ``annotationColor`` → ``annotation_color`` A small release adding the ability to specify the ``norm`` option for the ``pcolor_opts`` dictionary argument to ``TreeMesh.plotImage``. Contributors ============ * @jcapriot Pull requests ============= * `#211 `__: Add ability to handle "norm" keyword in pcolor_opts on the TreeMesh ================================================ FILE: docs/release/0.4.15-notes.rst ================================================ .. currentmodule:: discretize .. _0.4.15_notes: =================================== ``discretize`` 0.4.15 Release Notes =================================== July 23, 2020 This is a minor release to fix a small error in the deprecated vType argument Contributors ============ * @jcapriot Pull requests ============= * `#213 `__: Fix bug in deprecated vType ================================================ FILE: docs/release/0.5.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.5.0_notes: =================================== ``discretize`` 0.5.0 Release Notes =================================== September 2, 2020 This minor release has a few small bug fixes as well as a new volume averaging operator. The Volume Averaging operator has been implemented for arbitrary `TensorMesh`, `TreeMesh`, and combinations of them. It is defined as being a mass conserving operation. More details can be found in its documentation, :func:`discretize.utils.volume_average`. There are also some updates for the new deprecations in ``matplotlib`` to hopefully throw less deprecation warnings internally. There are still a few left which are on our radar to fix in the next patch. We are also dropping support for python 3.5 which will reach end-of-life within a few weeks. Contributors ============ * @jcapriot * @prisae * @bluetyson Pull requests ============= * `#212 `__: Volume average * `#216 `__: Update 2_tensor_mesh.py * `#217 `__: Fix Slicer matplotlib-warning. * `#220 `__: 0.5.0 release notes and requirements update ================================================ FILE: docs/release/0.5.1-notes.rst ================================================ .. currentmodule:: discretize .. _0.5.1_notes: =================================== ``discretize`` 0.5.1 Release Notes =================================== September 4, 2020 A small patch to fix a bug in the volume averaging operator when using meshes with different shaped bases, to address `#222 `__. There are also a few deprecated syntax updates for comparisons with literals. `#221 `__. Contributors ============ * @jcapriot Pull requests ============= * `#223 `__: Syntax and bug fixes ================================================ FILE: docs/release/0.6.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.6.0_notes: =================================== ``discretize`` 0.6.0 Release Notes =================================== November 16, 2020 This minor release is intended to bring consistent pep8 style naming across all of discretize's classes and functions. There are two major types of renaming, aliases and deprecations. We have chosen to move to more descriptive property names for classes, generally. For example, ``mesh.area`` is deprecated and now is ``mesh.face_area``. Also properties like ``mesh.vnC`` are now officially ``mesh.shape_cells`` due to the more descriptive name, but can also be accessed as ``mesh.vnC`` to speed up some code writing for users. We have included a full list of aliases and deprecations below. In PR `#227 `__ we have detailed our reasonings behind individual name choices. The other big change that will likely cause previous code to break is that all of these ``mesh.shape_*`` type properties are now explicitly ``tuple``-s, making them immutable. These properties could previously be modified which would result in undefined and unsafe behavoir. A side effect of this, is that any code that relied on these properties being ``numpy.ndarray``-s, will break. This is intentional. There's a few internal changes as well, to reorganize the file structure. importing items in ``discretize.utils`` from their individual module files is not recommended and might result in future broken code. Please only import these items from the ``discretize.utils`` module. We have also separated the ``matplotlib`` plotting code into a separate module: ``discretize.utils.mixins.mpl_mod``. At the same time we have further improved the plotting speed of ``discretize.TreeMesh`` and ``discretize.CurvilinearMesh``. This also allows all of these functions to have a unified calling convention. Finally, we have removed assert errors in favor of throwing the proper exception when checking inputs. We have removed all references to ``__future__`` and ``six`` to clean up and drop python 2 compatibility. .. note:: Testing now uses Azure CI Changes ======= This is a full list of the aliases and deprecations for this ``discretize`` release. Aliases ------- On ``discretize.base.BaseMesh``: * ``origin=x0`` * ``n_cells=nC`` * ``n_nodes=nN`` * ``n_edges=nE`` * ``n_faces=nF`` * ``n_edges_x=nEx``, ``n_edges_y=nEy``, ``n_edges_z=nEz`` * ``n_faces_x=nFx``, ``n_faces_y=nFy``, ``n_faces_z=nFz`` * ``n_edges_per_direction=vnE``, ``n_faces_per_direction=vnF`` On ``discretize.base.BaseRectangularMesh``: * ``shape_cells=vnC`` * ``shape_nodes=vnN`` * ``shape_edges_x=vnEx``, ``shape_edges_y=vnEy``, ``shape_edges_z=vnEz`` * ``shape_faces_x=vnFx``, ``shape_faces_y=vnFy``, ``shape_faces_z=vnFz`` On ``discretize.base.BaseTensorMesh``: * ``cell_centers=gridCC`` * ``nodes=gridN`` * ``edges_x=gridEx``, ``edges_y=gridEy``, ``edges_z=gridEz`` * ``faces_x=gridFx``, ``faces_y=gridFy``, ``faces_z=gridFz`` On ``discretize.operators.DiffOperators``: * ``average_face_to_cell=aveF2CC``, ``average_face_to_cell_vector=aveF2CCV`` * ``average_face_x_to_cell=aveFx2CC``, ``average_face_y_to_cell=aveFy2CC``, ``average_face_z_to_cell=aveFz2CC`` * ``average_cell_to_face=aveCC2F``, ``average_cell_vector_to_face=aveCCV2F`` * ``average_edge_to_cell=aveE2CC``, ``average_edge_to_cell_vector=aveE2CCV`` * ``average_edge_x_to_cell=aveEx2CC``, ``average_edge_y_to_cell=aveEy2CC``, ``average_edge_z_to_cell=aveEz2CC`` * ``average_node_to_cell=aveN2CC``, ``average_node_to_edge=aveN2E``, ``average_node_to_face=aveN2F`` On ``TreeMesh``: * Similar to above, all ``n_hanging_XXX=nhX``, and ``n_total_XXX=ntX``, for each ``node=N``, ``face_x,y,z=Fx,y,z``, and ``edge_x,y,z=Ex,y,z`` * Also, similar to above: ``hanging_XXX=gridhX``, for each hanging ``node=N``, ``face_x,y,z=Fx,y,z`` and ``edge_x,y,z=Ex,y,z`` These aliases are consistent across all meshes that inherit from these classes: ``discretize.TensorMesh``, ``discretize.CurvilinearMesh``, ``discretize.CylindricalMesh``, and ``discretize.TreeMesh``. Deprecations ------------ These deprecations will give ``FutureWarning`` when called indicating that they will be removed in version 1.0.0 of ``discretize``. Base Classes ************ On ``discretize.base.BaseMesh``: * ``normals`` → ``face_normals`` * ``tangents`` → ``edge_tangents`` * ``projectEdgeVector`` → ``project_edge_vector`` * ``projectFaceVector`` → ``project_face_vector`` On ``discretize.base.BaseRectangularMesh``: * ``r`` → ``reshape`` * ``nCx`` → ``shape_cells[0]``, ``nCy`` → ``shape_cells[1]``, ``nCz`` → ``shape_cells[2]`` * ``nNx`` → ``shape_nodes[0]``, ``nNy`` → ``shape_nodes[1]``, ``nNz`` → ``shape_nodes[2]`` * ``hx``→``h[0]``, ``hy`` → ``h[1]``, ``hz``→ ``h[2]`` On ``discretize.base.BaseTensorMesh``: * ``vectorNx`` → ``nodes_x``, ``vectorNy`` → ``nodes_y``, ``vectorNz`` → ``nodes_z`` * ``vectorCCx`` → ``cell_centers_x``, ``vectorCCy`` → ``cell_centers_y``, ``vectorCCz`` → ``cell_centers_z`` * ``getInterpolationMat`` → ``get_interpolation_matrix`` * ``isInside`` → ``is_inside`` * ``getTensor`` → ``get_tensor`` On ``discretize.base.MeshIO``: * ``readUBC`` → ``read_UBC`` * ``readModelUBC`` → ``read_model_UBC`` * ``writeUBC`` → ``write_UBC`` * ``writeModelUBC`` → ``write_model_UBC`` On ``discretize.operators.DiffOperators``: * ``cellGrad`` → ``cell_gradient`` * ``cellGradBC`` → ``cell_gradient_BC`` * ``cellGradx`` → ``cell_gradient_x``, ``cellGrady`` → ``cell_gradient_y``, ``cellGradz`` → ``cell_gradient_z`` * ``nodalGrad`` → ``nodal_gradient`` * ``nodalLaplacian`` → ``nodal_laplacian`` * ``faceDiv`` → ``face_divergence`` * ``faceDivx`` → ``face_x_divergence``, ``faceDivy`` → ``face_y_divergence``, ``faceDivz`` →``face_z_divergence`` * ``edgeCurl`` → ``edge_curl`` * ``setCellGradBC`` → ``set_cell_gradient_BC`` * ``getBCProjWF`` → ``get_BC_projections`` * ``getBCProjWF_simple`` → ``get_BC_projections_simple`` On ``discretize.operators.InnerProducts``: * ``getFaceInnerProduct`` → ``get_face_inner_product`` * ``getEdgeInnerProduct`` → ``get_edge_inner_product`` * ``getFaceInnerProductDeriv`` → ``get_face_inner_product_deriv`` * ``getEdgeInnerProductDeriv`` → ``get_edge_inner_product_deriv`` Main Meshes *********** ``CylMesh`` → ``CylindricalMesh`` On ``discretize.TensorMesh``, ``discretize.CylindricalMesh``, ``discretize.TreeMesh``, ``discretize.CurvilinearMesh``: * ``vol`` → ``cell_volumes`` * ``area`` → ``face_areas`` * ``edge`` → ``edge_lengths`` On ``discretize.TensorMesh``, ``discretize.CylindricalMesh``, ``discretize.TreeMesh``: * ``areaFx`` → ``face_x_areas``, ``areaFy`` → ``face_y_areas``, ``areaFz`` →``face_z_areas`` * ``edgeEx`` → ``edge_x_lengths``, ``edgeEy`` → ``edge_y_lengths``, ``edgeEz`` →``edge_z_lengths`` On ``discretize.TensorMesh``, ``discretize.TreeMesh``: * ``faceBoundaryInd`` → ``face_boundary_indices`` * ``cellBoundaryInd`` → ``cell_boundary_indices`` On ``discretize.CurvilinearMesh``: * The ``nodes`` property is now ``node_list`` to avoid the name clash with the ``nodes`` location property On ``discretize.CylindricalMesh``: * ``isSymmetric`` → ``is_symmetric`` * ``cartesianOrigin`` → ``cartesian_origin`` * ``getInterpolationMatCartMesh`` → ``get_interpolation_matrix_cartesian_mesh`` * ``cartesianGrid`` → ``cartesian_grid`` On ``discretize.TreeMesh``: * ``maxLevel`` → ``max_used_level`` * ``permuteCC`` → ``permute_cells`` * ``permuteF`` → ``permute_faces`` * ``permuteE`` → ``permute_edges`` And for plotting with ``matplotlib``: * ``plotGrid`` → ``plot_grid`` * ``plotImage`` → ``plot_image`` * ``plotSlice`` → ``plot_slice`` Utilities deprecations ********************** Deprecations inside ``discretize.utils``: * ``isScalar`` → ``is_scalar`` * ``asArray_N_x_Dim`` → ``as_array_n_by_dim`` * ``sdInv`` → ``sdinv`` * ``getSubArray`` → ``get_subarray`` * ``inv3X3BlockDiagonal`` → ``inverse_3x3_block_diagonal`` * ``inv2X2BlockDiagonal`` → ``inverse_2x2_block_diagonal`` * ``makePropertyTensor`` → ``make_property_tensor`` * ``invPropertyTensor`` → ``inverse_property_tensor`` * ``exampleLrmGrid`` → ``example_curvilinear_grid`` * ``meshTensor`` → ``unpack_widths`` * ``closestPoints`` → ``closest_points_index`` * ``ExtractCoreMesh`` → ``extract_core_mesh`` * ``volTetra`` → ``volume_tetrahedron`` * ``indexCube`` → ``index_cube`` * ``faceInfo`` → ``face_info`` * ``interpmat`` → ``interpolation_matrix`` * ``rotationMatrixFromNormals`` → ``rotate_points_from_normals`` * ``rotatePointsFromNormals`` → ``rotation_matrix_from_normals`` Contributors ============ * @jcapriot With reviews from: * @prisae * @lheagy Also, input on function names were given by many of the ``discretize`` developers. Pull requests ============= * `#227 `__: Restructure. ================================================ FILE: docs/release/0.6.1-notes.rst ================================================ .. currentmodule:: discretize .. _0.6.1_notes: =================================== ``discretize`` 0.6.1 Release Notes =================================== November 17, 2020 This patch release adds deprecation for 3 of the hidden properties on the TreeMesh. These properties are now publicly accessible. * ``_aveCC2FxStencil`` → ``average_cell_to_total_face_x`` * ``_aveCC2FyStencil`` → ``average_cell_to_total_face_y`` * ``_aveCC2FzStencil`` → ``average_cell_to_total_face_z`` In addition, we have promoted the cell gradient stencils to publicly accessible as: * ``_cellGradStencil`` → ``stencil_cell_gradient`` * ``_cellGradxStencil`` → ``stencil_cell_gradient_x`` * ``_cellGradyStencil`` → ``stencil_cell_gradient_y`` * ``_cellGradzStencil`` → ``stencil_cell_gradient_z`` We continue to discourage against accessing hidden variables as there is no guarantee that their name will stay the same. Contributors ============ * @jcapriot Pull requests ============= * `#229 `__: Quick Patch for stencil matrices. ================================================ FILE: docs/release/0.6.2-notes.rst ================================================ .. currentmodule:: discretize .. _0.6.2_notes: =================================== ``discretize`` 0.6.2 Release Notes =================================== November 26, 2020 This patch release changes ``FutureWarning`` to ``DeprecationWarning`` to temporarily hide them from end users. Our current plan is to switch them back to ``FutureWarning`` on the next minor release. We encourage developers to update their code prior to this. Contributors ============ * @jcapriot Pull requests ============= * `#231 `__: Deprecate warnings ================================================ FILE: docs/release/0.6.3-notes.rst ================================================ .. currentmodule:: discretize .. _0.6.3_notes: =================================== ``discretize`` 0.6.3 Release Notes =================================== March 1, 2021 This patch contains a bug-fix related to plotting on a :class:`TreeMesh`. It also contains a few additions which caused a minor functionality change in :func:`utils.refine_tree_xyz`. The first convenience is the addition of a `slice_loc` parameter to the :func:`mixins.mpl_mod.InterfaceMPL.plot_slice`. This allows for slightly more intuitive use over the old `ind` keyword, by allowing the user to directly specify the location parameter of the slice. The second addition, which caused the minor functionality change in `refine_tree_xyz`, is the addition of two specialized refine functions for the `TreeMesh`. We can now directly refine all cells that intersect a box (or list of boxes) with :func:`TreeMesh.refine_box`, and similarly we can refine all cells that intersect a ball (or list of balls) with :func:`TreeMesh.refine_ball`. Contributors ============ * @jcapriot * @ckohnke * @Rockpointgeo * @thast * @domfournier Pull requests ============= * `#200 `__: Refine tree xyz box * `#235 `__: add xyzslice keyword to mesh.plot_slice * `#236 `__: Correct the range_y in Tree plot_image * `#237 `__: Tree refine c ================================================ FILE: docs/release/0.7.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.7.0_notes: =================================== ``discretize`` 0.7.0 Release Notes =================================== April 30, 2021 This Minor release has several new features, including support for boundary conditions, quiver vector plotting for :func:`TreeMesh.plot_slice`, and a rather large change in removing the ``properties`` requirement from ``discretize``. Removal of ``properties`` ------------------------- Starting with the largest change, we have made the decision to remove the properties backend of `discretize`. Our main motivation for this was to give ourselves more control over the documentation of classes. Our goal was to recreate the functionality without any adverse effects to our users, and have taken many steps to hopefully address any possible issues with the conversion. That being said, please raise an issue on the github page if you run into any unexpected issues. The next step here is to finalize the documentation.. Boundary Conditions ------------------- We have added an improved support for implementing boundary conditions for the finite volume formulation that underlies ``discretize`` on :class:`TreeMesh`, :class:`TensorMesh` , and :class:`CurvilinearMesh`. In general, ``discretize`` often makes use of the following two identities for the weak form of the finite volume: .. math:: \int_\Omega \nabla u \cdot \vec{v} dV = -\int_\Omega u \nabla \cdot \vec{v} dV + \int_{\partial\Omega} u \vec{v}\cdot \hat{n} dA .. math:: \int_\Omega (\nabla \times \vec{w}) \cdot \vec{v} dV = \int_\Omega \vec{w} \cdot (\nabla \times \vec{v}) dV - \int_{\partial\omega} (\vec{w} \times \hat{n}) \cdot \vec{v} dA Previously we focused on approximating the volume integrals in the above equations, however to implement boundary conditions, we use the boundary face integrals! As part of this, meshes now have a few new properties: * ``boundary_faces`` * ``boundary_face_outward_normals`` * ``boundary_edges`` * ``boundary_nodes`` * ``average_node_to_face`` * ``average_edge_to_face_vector`` * ``project_edge_to_boundary_edge`` * ``project_face_to_boundary_face`` * ``project_node_to_boundary_node`` Together these are used in a few items that are correspond to the mass matrices, :func:`operators.InnerProducts.get_edge_inner_product` and :func:`operators.InnerProducts.get_face_inner_product`. These relate to the item that they are operating on, and return the necessary matrix to integrate that quantity on the default boundaries of the meshes. * ``boundary_face_scalar_integral`` * ``boundary_node_vector_integral`` * ``boundary_edge_vector_integral`` You can investigate the source code of these functions to see how they are built if you need to design your own customized boundary. All together we have also implemented two helper operators that can be used to reproduce common types of boundary conditions for a few PDE's. These are based on Robin type conditions that can flexibly support multiple types of boundary conditions depending on the discrete value's locations. * :func:`operators.DiffOperators.cell_gradient_weak_form_robin` * :func:`operators.DiffOperators.edge_divergence_weak_form_robin` Future Work =========== With the removal of ``properties`` we will be updating the documentation to be more explicit, but in the meantime this will make some items look less clear. We will also be pushing out some tutorials on how to use the boundary conditions to solve the boundary value PDE's. In the meantime though, you can look at the boundary condition test codes where we form a few PDE's for convergence tests. Contributors ============ * @jcapriot With reviews from: * @prisae * @domfournier/@fourndo * @lheagy Pull requests ============= * `#232 `__: Exorcise Properties * `#234 `__: Boundary conditions * `#240 `__: Tree quiver plot * `#241 `__: Update azure-pipelines.yml ================================================ FILE: docs/release/0.7.1-notes.rst ================================================ .. currentmodule:: discretize .. _0.7.1_notes: =================================== ``discretize`` 0.7.1 Release Notes =================================== October 7, 2021 This patch release is a big step, but with minimal functional changes. There are a few small bug fixes, but the largest news is the updated documentation! New Documentation ----------------- The documentation for every module, class, function, etc. has all been unified to numpy styled documentation. Many functions and methods now have small examples within their docstrings that show simple usage. We have also update the layout of the documentation to match the new ``pydata`` community layout. Bug Fixes --------- We previously re-intoduced a small bug in the ``mixins.mpl.plot_3d_slicer`` functionality that would caused the colorscales to not match for a homogenous model. This has be re-fixed. We also now additionally return the quiver plot object when vector plotting using the ```plot_slice`` on a ``TreeMesh`` Contributors ============ * @jcapriot * @dcowan * @lheagy * @prisae Pull requests ============= * `#253 `__: Numpy docstrings api * `#256 `__: Update mpl_mod.py * `#258 `__: Numpy docstrings api review * `#262 `__: Fix wrong colour for fullspaces - again / * `#264 `__: patch for fullspace slicer colorscales ================================================ FILE: docs/release/0.7.2-notes.rst ================================================ .. currentmodule:: discretize .. _0.7.2_notes: =================================== ``discretize`` 0.7.2 Release Notes =================================== December 6, 2021 This patch release is for a minor improvements to a few functions Searching --------- The function ``closest_points_index`` now exists on the `base.BaseMesh`, with a default implementation based on a KDTree lookup. This allows for much faster repeated calls. The previous function one off function will now point to this method on the mesh. Negative Levels --------------- To referring to levels on ``TreeMesh`` refine functions, you can now pass negative integers to refer to the maximum refine level (similar to negative indexing on arrays) Contributors ============ * @jcapriot * @ngodber Pull requests ============= * `#251 `__: refactor closest_points_index to use cKDTree. * `#267 `__: TreeMesh negative levels * `#270 `__: ensure the grid locations are (N x dim) happens in 1D ================================================ FILE: docs/release/0.7.3-notes.rst ================================================ .. currentmodule:: discretize .. _0.7.3_notes: =================================== ``discretize`` 0.7.3 Release Notes =================================== February 28, 2022 This patch release fixes a few minor bugs related to the edge curl operator in 2D for ```TensorMesh`` which was not properly indexed as edges. There is also minor additions to the functionality of cylindrical meshes, allowing the user to set an origin point for the rotational component. Finally, there is an added dot product test to simplify future development and testing. Contributors ============ * @jcapriot * @prisae Pull requests ============= * `#268 `__: ENH: Actually use origin[1] for cylindrical mesh * `#271 `__: update 2D edge curl operator to properly go from 2D edges to Z-faces * `#272 `__: Add dot product test. ================================================ FILE: docs/release/0.7.4-notes.rst ================================================ .. currentmodule:: discretize .. _0.7.4_notes: =================================== ``discretize`` 0.7.4 Release Notes =================================== April 20, 2022 This bug fix patch addressed an issue accessing the face indices of a ``TreeCell``. We are also testing a new versioning system that is based off of git tags. If you are a developer ``setuptools_scm`` is now a requirement along with an installation of ``git`` that is discoverable from the command line. Contributors ============ * @jcapriot * @prisae * @thibaut-kobold Pull requests ============= * `#273 `__: Move from bumpversion to setuptools_scm * `#276 `__: fix faces property of TreeCell ================================================ FILE: docs/release/0.8.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.8.0_notes: =================================== ``discretize`` 0.8.0 Release Notes =================================== May 12, 2022 This minor release introduces a new mesh type to discretize, :class:`SimplexMesh`! ``SimplexMesh`` --------------- ``discretize`` now has support for triangular (2D) and tetrahedral (3D) meshes. These meshes support most of the operations that you would expect of a discretize mesh that is used to solve PDEs with the finite volume method. You have access to :func:`SimplexMesh.face_divergence`, :func:`SimplexMesh.nodal_gradient`, and :func:`SimplexMesh.edge_curl` operators, along with the expected inner product operators and their derivatives: :func:`SimplexMesh.get_edge_inner_product`, :func:`SimplexMesh.get_face_inner_product`, :func:`SimplexMesh.get_edge_inner_product_deriv`, and :func:`SimplexMesh.get_face_inner_product_deriv`. They contain the expected average operators that move between nodes, cell centers, faces, and edges. The interpolation operator can interpolate scalar values from nodes and cell centers, and interpolate vector quantities from edges and faces. The mesh also has operators to handle the boundary conditions in manners similar to the previous implementation. The basic format of input into a :class:`SimplexMesh` is an array of node locations, and an array of the simplex indices, enabling for simple interaction with many different mesh generation libraries. There are readers for unstructured VTK files which contain all of either triangular or tetrahedral elements. ``TreeMesh`` ------------ Interpolation on :class:`TreeMesh` is now linear between cells of the same level for the cell centered, edge, and face interpolators. Future Warnings =============== All of the previous Deprecation Warnings from the refactor of ``discretize`` to pep8 friendly names in v0.6.0 have been changed to Future Warnings in preparation for the 1.0.0 release of discretize. Contributors ============ * @jcapriot Pull requests ============= * `#263 `__: Unstructured Triangular/Tetrahedral meshes * `#277 `__: Updating TreeMesh Interpolation * `#278 `__: 0.8.0 Release ================================================ FILE: docs/release/0.8.1-notes.rst ================================================ .. currentmodule:: discretize .. _0.8.1_notes: =================================== ``discretize`` 0.8.1 Release Notes =================================== August 16, 2022 This patch release, in addition to some small bug fixes and runtime improvements implements, also implements some missing functionality for cylindrical meshes. ``CylindricalMesh`` ------------------- The big news is that 3D cylindrical meshes can now be output to a vtk format, which represents the wedges and arced cells as rational bezier curves in an unstructured vtk mesh. There is now full interpolation functionality when using 3D meshes, that includes appropriate interpolation that wraps around the angular component across the cylindrical mesh. The 3D nodal gradient operator is also implemented now. On the backend, several internal matrices should now build much quicker than before. Startup ------------ Since ``matplotlib``, ``vtk`` and ``omf`` libraries have been made optional, they are now only imported when the corresponding functionality is actually used. These means a reasonable improvement in the speed on the first import of ``discretize``. Bug Fixes --------- - 3D ``TreeMesh`` now generates the correct list of nodes in each cell - A missed internal deprecation for `zerosOutside` has been cleaned up - We've added python 3.10 to the testing sweet. - Bumped the minimum version of python to 3.7 to match the tests. Contributors ============ * @jcapriot * @prisae * @lheagy Pull requests ============= * `#280 `__: Nodal_Gradient for the CylindricalMesh * `#281 `__: Cyl vtk * `#283 `__: add python 3.10 to testing suite * `#284 `__: Improve load time * `#285 `__: zeros_outside * `#286 `__: Cell node tree * `#288 `__: Allow ``np.int_`` * `#289 `__: 0.8.1 Release ================================================ FILE: docs/release/0.8.2-notes.rst ================================================ .. currentmodule:: discretize .. _0.8.2_notes: =================================== ``discretize`` 0.8.2 Release Notes =================================== August 17, 2022 This patch release fixes a small bug in importing the ``importlib.util`` library Bug Fixes --------- - ``importlib.util`` import should now work Contributors ============ * @jcapriot Pull requests ============= * `#291 `__: Importlib import fix ================================================ FILE: docs/release/0.8.3-notes.rst ================================================ .. currentmodule:: discretize .. _0.8.3_notes: =================================== ``discretize`` 0.8.3 Release Notes =================================== January 27, 2023 This is a patch release with several bugfixes, as well as additions to the ``TreeMesh``. ``TreeMesh`` improvements ------------------------- We have added several methods to the :class:`discretize.TreeMesh` that provide specialize geometric refinement entities including: - Refine along a line segment path, :meth:`~discretize.TreeMesh.refine_line` - Refine on triangle intersection, :meth:`~discretize.TreeMesh.refine_triangle` - Refine on tetrahedron interseciton, :meth:`~discretize.TreeMesh.refine_tetrahedron` - Refine on vertical triangular prism, :meth:`~discretize.TreeMesh.refine_vertical_trianglular_prism` These enabled more accurate functions for refining along surfaces. Therefore we have deprecated the :meth:`discretize.utils.refine_tree_xyz` which was essentially three different functions, in favor of separate and distinct methods on the ``TreeMesh`` itself which all allow for padding at different levels.: - Refine on surface, :meth:`~discretize.TreeMesh.refine_surface` - Refine on bounding box of points, :meth:`~discretize.TreeMesh.refine_bounding_box` - Refine around points, :meth:`~discretize.TreeMesh.refine_points` We have also added optional diagonal balancing to the `TreeMesh` which will ensure only a single level change across diagonally adjacent cells. This can be set at the instantiation of the object, or on a each separate call to a refinement function. We plan to make this option `True` by default in a future version, but this will never change how any mesh is read in from a file, ensuring only newly created meshes are affected. Style Updates ------------- All new PR's for ``discretize`` are now checked for style consistency by ``black`` and ``flake8`` before running the testing sweet. Bug Fixes --------- - Interpolation on the :class:`~discretize.SimplexMesh` now respects the `zeros_outside` keyword argument - Documentation inheritance for the functions inherited from :class:`~discretize.operators.DiffOperators` and :class:`~discretize.operators.InnerProducts` have been fixed by making these two classes subclass :class:`~discretize.base.BaseMesh`. Contributors ============ * @jcapriot Pull requests ============= * `#292 `__: Dark mode theme for documentation * `#294 `__: Testing environment updates * `#295 `__: Diagonal tree balance * `#296 `__: More tree refine functions * `#297 `__: Add new refine surface, bounding box, and point refine methods. * `#298 `__: implement zeros outside for interpolation function * `#299 `__: Build maintenance * `#300 `__: Add style testing * `#301 `__: Documentation updates * `#302 `__: 0.8.3 release ================================================ FILE: docs/release/0.9.0-notes.rst ================================================ .. currentmodule:: discretize .. _0.9.0_notes: =================================== ``discretize`` 0.9.0 Release Notes =================================== May 31, 2023 This is a minor release with several pieces of additional functionality, and some small bug fixes. It also stages the updates for the `1.0.0` release by changing the ``FutureWarnings`` to actual errors that describe to the user of how to update their code. All of these errors and messages will be removed on `1.0.0`. ``CylindricalMesh`` ------------------- Cylindrical meshes have now become more flexible. They can be created without discretizing the full azimuthal space (think a slice of pizza, or a half-cylinder), and they can be created without starting the radial dimension at 0 (think a ring). Cylindrical meshes now also support averaging edges to faces, and thus now allow for boundary conditions to be imposed on PDEs involving curls. Boundary Conditions ------------------- The curvilinear and simplex meshes now better support boundary conditions with improved (and corrected) handling of the inner products in the boundary condition surface integral. There is also now a detailed example on how to solve a nodal gradient problem with a dirichlet boundary condition. Tests ----- Discretize tests now better support a ``pytest`` environment with new assertion tests. Previous tests now use these internally. Thhe new tests are more flexible and should give better messages when they fail. ``TreeMesh`` ------------ The external ``c++`` code now uses a ``try_emplace`` function for determining if an item already exists when constructing the mesh. This should speedup mesh refinement. Bug Fixes --------- - Scrolling through the ``Slicer`` object within modern notebook enviroments should now be working correctly. Contributors ============ * @jcapriot * @prisae Pull requests ============= * `#308