diff --git a/.ci/azure/deploy.yml b/.ci/azure/deploy.yml index c7911e66a..23e8d851c 100644 --- a/.ci/azure/deploy.yml +++ b/.ci/azure/deploy.yml @@ -1,24 +1,18 @@ jobs: - job: - displayName: "Deploy Docs and source" + displayName: "Deploy Docs" pool: vmImage: ubuntu-latest + steps: # No need to checkout the repo here! - checkout: none - # Just download all of the items already built - - task: DownloadPipelineArtifact@2 - inputs: - buildType: 'current' - artifactName: 'wheels' - targetPath: 'dist' - - - task: DownloadPipelineArtifact@2 - inputs: - buildType: 'current' - artifactName: 'source_dist' - targetPath: 'dist' + - bash: | + echo $IS_TAG + echo $IS_MAIN + echo $BRANCH_NAME + displayName: Report branch parameters - task: DownloadPipelineArtifact@2 inputs: @@ -27,8 +21,8 @@ jobs: targetPath: 'html' - bash: | - ls -l dist ls -l html + displayName: Report downloaded cache contents. - bash: | git config --global user.name ${GH_NAME} @@ -39,25 +33,34 @@ jobs: GH_NAME: $(gh.name) GH_EMAIL: $(gh.email) + # upload documentation to discretize-docs gh-pages on tags - bash: | - twine upload --skip-existing dist/* - displayName: Deploy source and wheels + git clone -q --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/simpeg/discretize-docs.git + displayName: Checkout doc repository env: - TWINE_USERNAME: $(twine.username) - TWINE_PASSWORD: $(twine.password) + GH_TOKEN: $(gh.token) - # upload documentation to discretize-docs gh-pages on tags - bash: | - git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/discretize-docs.git cd discretize-docs - git gc --prune=now - git remote prune origin - rm -rf en/main/* - cp -r html/* en/main/ + rm -rf "en/$BRANCH_NAME" + mv ../html "en/$BRANCH_NAME" touch .nojekyll - git add . + displayName: Set Doc Folder + + - bash: | + # Update latest symlink + cd discretize-docs + rm -f en/latest + ln -s "en/$BRANCH_NAME" en/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) \ No newline at end of file + GH_TOKEN: $(gh.token) diff --git a/.ci/azure/docs.yml b/.ci/azure/docs.yml index 6fe0dac83..51b3fcb6f 100644 --- a/.ci/azure/docs.yml +++ b/.ci/azure/docs.yml @@ -1,5 +1,5 @@ jobs: -- job: +- job: BuildDocs displayName: "Build Documentation" pool: vmImage: ubuntu-latest @@ -33,4 +33,4 @@ jobs: inputs: targetPath: 'docs/_build/html' artifact: 'html_docs' - parallel: true \ No newline at end of file + parallel: true diff --git a/.ci/azure/setup_env.sh b/.ci/azure/setup_env.sh index 388feef43..e012bb0ca 100755 --- a/.ci/azure/setup_env.sh +++ b/.ci/azure/setup_env.sh @@ -7,7 +7,6 @@ do_doc=$(echo "${DOC_BUILD:-false}" | tr '[:upper:]' '[:lower:]') if ${is_azure} then - conda update --yes -n base conda if ${do_doc} then .ci/setup_headless_display.sh diff --git a/.ci/azure/setup_miniconda_macos.sh b/.ci/azure/setup_miniconda_macos.sh new file mode 100755 index 000000000..31ca98131 --- /dev/null +++ b/.ci/azure/setup_miniconda_macos.sh @@ -0,0 +1,15 @@ +#!/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" diff --git a/.ci/azure/test.yml b/.ci/azure/test.yml index 26187e26a..3952f84bd 100644 --- a/.ci/azure/test.yml +++ b/.ci/azure/test.yml @@ -35,15 +35,16 @@ jobs: 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') - - bash: sudo chown -R $USER $CONDA - displayName: Take ownership of conda directory - condition: eq(variables.varOS, 'Darwin') - - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" displayName: Add conda to PATH condition: eq(variables.varOS, 'Windows_NT') diff --git a/.ci/azure/wheels.yml b/.ci/azure/wheels.yml deleted file mode 100644 index ea25e794e..000000000 --- a/.ci/azure/wheels.yml +++ /dev/null @@ -1,63 +0,0 @@ -jobs: -- job: - displayName: "Build wheels on ${{ variables.image }}" - strategy: - matrix: - linux-Python310: - image: 'Ubuntu-20.04' - CIBW_BUILD: 'cp310-*' - linux-Python311: - image: 'Ubuntu-20.04' - CIBW_BUILD: 'cp311-*' - linux-Python312: - image: 'Ubuntu-20.04' - CIBW_BUILD: 'cp312-*' - osx-Python310: - image: 'macOS-12' - CIBW_BUILD: 'cp310-*' - CIBW_ARCHS_MACOS: 'x86_64 arm64' - osx-Python311: - image: 'macOS-12' - CIBW_BUILD: 'cp311-*' - CIBW_ARCHS_MACOS: 'x86_64 arm64' - osx-Python312: - image: 'macOS-12' - CIBW_BUILD: 'cp312-*' - CIBW_ARCHS_MACOS: 'x86_64 arm64' - win-Python310: - image: 'windows-2019' - CIBW_BUILD: 'cp310-*' - CIBW_ARCHS_WINDOWS: 'AMD64' - CIBW_CONFIG_SETTINGS: 'setup-args=--vsenv' - win-Python311: - image: 'windows-2019' - CIBW_BUILD: 'cp311-*' - CIBW_ARCHS_WINDOWS: 'AMD64' - CIBW_CONFIG_SETTINGS: 'setup-args=--vsenv' - win-Python312: - image: 'windows-2019' - CIBW_BUILD: 'cp312-*' - CIBW_ARCHS_WINDOWS: 'AMD64' - CIBW_CONFIG_SETTINGS: 'setup-args=--vsenv' - pool: - vmImage: $(image) - steps: - - task: UsePythonVersion@0 - - - bash: - git fetch --tags - displayName: Fetch tags - - - bash: | - set -o errexit - python3 -m pip install --upgrade pip - pip3 install cibuildwheel==2.20.0 - displayName: Install dependencies - - - bash: cibuildwheel --output-dir wheelhouse . - displayName: Build wheels - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: 'wheelhouse' - ArtifactName: 'wheels' \ No newline at end of file diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 91748c856..3a5d24e10 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -4,21 +4,23 @@ channels: dependencies: - numpy>=1.22.4 - scipy>=1.8 + # optionals - vtk>=6 - pyvista - omf - matplotlib + # documentation - sphinx - - pydata-sphinx-theme==0.13.3 + - pydata-sphinx-theme==0.15.4 - sphinx-gallery>=0.1.13 - numpydoc>=1.5 - jupyter - graphviz - - pymatsolver>=0.1.2 - pillow - pooch + # testing - sympy - pytest diff --git a/.github/workflows/build_distributions.yml b/.github/workflows/build_distributions.yml new file mode 100644 index 000000000..6dff9ce6c --- /dev/null +++ b/.github/workflows/build_distributions.yml @@ -0,0 +1,67 @@ +name: Build Distribution artifacts + +on: [push, pull_request] + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + # macos-13 is an intel runner, macos-14 is apple silicon + os: [ubuntu-latest, windows-latest, macos-13, macos-14] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.21.3 + # env: + # CIBW_SOME_OPTION: value + # ... + # with: + # package-dir: . + # output-dir: wheelhouse + # config-file: "{package}/pyproject.toml" + + - 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: + # To test: repository-url: https://test.pypi.org/legacy/ diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ee9994deb..b05c304a5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,6 +15,11 @@ pr: 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" @@ -31,28 +36,12 @@ stages: jobs: - template: .ci/azure/docs.yml - - stage: BuildWheels - dependsOn: - - Testing - - DocBuild - displayName: "Build Wheels" - jobs: - - template: .ci/azure/wheels.yml - - - stage: BuildSource + - stage: Deploy + displayName: "Deploy Docs" dependsOn: - Testing - DocBuild - displayName: "Build Source distribution" - jobs: - - template: .ci/azure/sdist.yml - - - stage: Deploy - displayName: "Deploy Source, Wheels, and Docs" - dependsOn: - - BuildWheels - - BuildSource - condition: and(succeeded(), startsWith(variables['build.sourceBranch'], 'refs/tags/')) + condition: and(succeeded(), or(eq(variables.IS_TAG, true), eq(variables.IS_MAIN, true))) jobs: - template: .ci/azure/deploy.yml diff --git a/discretize/_extensions/geom.cpp b/discretize/_extensions/geom.cpp new file mode 100644 index 000000000..da5a541ad --- /dev/null +++ b/discretize/_extensions/geom.cpp @@ -0,0 +1,566 @@ +#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; +} \ No newline at end of file diff --git a/discretize/_extensions/geom.h b/discretize/_extensions/geom.h new file mode 100644 index 000000000..1139f56e3 --- /dev/null +++ b/discretize/_extensions/geom.h @@ -0,0 +1,96 @@ +#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 \ No newline at end of file diff --git a/discretize/_extensions/geom.pxd b/discretize/_extensions/geom.pxd new file mode 100644 index 000000000..097cfdcc0 --- /dev/null +++ b/discretize/_extensions/geom.pxd @@ -0,0 +1,31 @@ +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 + \ No newline at end of file diff --git a/discretize/_extensions/interputils_cython.pyx b/discretize/_extensions/interputils_cython.pyx index 6c95d06bc..c3072c808 100644 --- a/discretize/_extensions/interputils_cython.pyx +++ b/discretize/_extensions/interputils_cython.pyx @@ -228,10 +228,11 @@ def _tensor_volume_averaging(mesh_in, mesh_out, values=None, output=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: - v_o = np.zeros(mesh_out_shape, order='F') + output = np.zeros(mesh_out.n_cells, dtype=np.float64) else: - v_o = output.reshape(mesh_out_shape, order='F') - v_o.fill(0) + 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] @@ -245,7 +246,7 @@ def _tensor_volume_averaging(mesh_in, mesh_out, values=None, output=None): 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 v_o.reshape(-1, order='F') + return output # Else, build and return a sparse matrix representing the operation i_i = np.empty(w_shape, dtype=np.int32, order='F') diff --git a/discretize/_extensions/meson.build b/discretize/_extensions/meson.build index 590fca517..3da7263c9 100644 --- a/discretize/_extensions/meson.build +++ b/discretize/_extensions/meson.build @@ -1,38 +1,6 @@ # NumPy include directory -# The try-except is needed because when things are -# split across drives on Windows, there is no relative path and an exception -# gets raised. There may be other such cases, so add a catch-all and switch to -# an absolute path. Relative paths are needed when for example a virtualenv is -# placed inside the source tree; Meson rejects absolute paths to places inside -# the source tree. -# For cross-compilation it is often not possible to run the Python interpreter -# in order to retrieve numpy's include directory. It can be specified in the -# cross file instead: -# [properties] -# numpy-include-dir = /abspath/to/host-pythons/site-packages/numpy/core/include -# -# This uses the path as is, and avoids running the interpreter. -incdir_numpy = meson.get_external_property('numpy-include-dir', 'not-given') -if incdir_numpy == 'not-given' - incdir_numpy = run_command(py, - [ - '-c', - '''import os -import numpy as np -try: - incdir = os.path.relpath(np.get_include()) -except Exception: - incdir = np.get_include() -print(incdir) - ''' - ], - check: true - ).stdout().strip() -else - _incdir_numpy_abs = incdir_numpy -endif -inc_np = include_directories(incdir_numpy) -np_dep = declare_dependency(include_directories: inc_np) +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 @@ -44,7 +12,6 @@ else use_math_defines = [] endif -numpy_nodepr_api = '-DNPY_NO_DEPRECATED_API=NPY_1_9_API_VERSION' c_undefined_ok = ['-Wno-maybe-uninitialized'] cython_c_args = [numpy_nodepr_api, use_math_defines] @@ -54,6 +21,11 @@ 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' @@ -61,7 +33,7 @@ module_path = 'discretize/_extensions' py.extension_module( 'interputils_cython', 'interputils_cython.pyx', - include_directories: incdir_numpy, + cython_args: cython_args, c_args: cython_c_args, install: true, subdir: module_path, @@ -70,8 +42,8 @@ py.extension_module( py.extension_module( 'tree_ext', - ['tree_ext.pyx' , 'tree.cpp'], - include_directories: incdir_numpy, + ['tree_ext.pyx' , 'tree.cpp', 'geom.cpp'], + cython_args: cython_args, cpp_args: cython_cpp_args, install: true, subdir: module_path, @@ -82,7 +54,7 @@ py.extension_module( py.extension_module( 'simplex_helpers', 'simplex_helpers.pyx', - include_directories: incdir_numpy, + cython_args: cython_args, cpp_args: cython_cpp_args, install: true, subdir: module_path, diff --git a/discretize/_extensions/tree.cpp b/discretize/_extensions/tree.cpp index 023de540c..8899490d1 100644 --- a/discretize/_extensions/tree.cpp +++ b/discretize/_extensions/tree.cpp @@ -1,6 +1,7 @@ #include #include #include "tree.h" +#include "geom.h" #include #include #include @@ -365,6 +366,20 @@ void Cell::shift_centers(double *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){ @@ -380,551 +395,6 @@ void Cell::insert_cell(node_map_t& nodes, double *new_cell, int_t p_level, doubl } }; -void Cell::refine_ball(node_map_t& nodes, double* center, double r2, int_t p_level, double *xs, double *ys, double* zs, bool diag_balance){ - // early exit if my level is higher than or equal to target - if (level >= p_level || level == max_level){ - return; - } - // check if I intersect the ball - double xp = std::max(points[0]->location[0], std::min(center[0], points[3]->location[0])); - double yp = std::max(points[0]->location[1], std::min(center[1], points[3]->location[1])); - double zp = 0.0; - if (n_dim > 2){ - zp = std::max(points[0]->location[2], std::min(center[2], points[7]->location[2])); - } - - // xp, yp, zp is closest point in the cell to the center of the circle - // check if that point is in the circle! - double r2_test = (xp - center[0])*(xp - center[0]) + (yp - center[1]) *(yp - center[1]); - if (n_dim > 2){ - r2_test += (zp - center[2])*(zp - center[2]); - } - if (r2_test >= r2){ - // I do not intersect the ball - return; - } - // if I intersect cell, I will need to be divided (if I'm not already) - if(is_leaf()){ - divide(nodes, xs, ys, zs, true, diag_balance); - } - // recurse into children - children[0]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - children[1]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - children[2]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - children[3]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - if (n_dim > 2){ - children[4]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - children[5]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - children[6]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - children[7]->refine_ball(nodes, center, r2, p_level, xs, ys, zs, diag_balance); - } -} - -void Cell::refine_box(node_map_t& nodes, double* x0, double* x1, int_t p_level, double *xs, double *ys, double* zs, bool enclosed, bool diag_balance){ - // early exit if my level is higher than target - if (level >= p_level || level == max_level){ - return; - } - if (!enclosed){ - // check if I overlap (not if an edge overlaps) - // If I do not overlap the cells then return - if (x0[0] >= points[3]->location[0] || x1[0] <= points[0]->location[0]){ - return; - } - - if (x0[1] >= points[3]->location[1] || x1[1] <= points[0]->location[1]){ - return; - } - - if (n_dim>2 && (x0[2] >= points[7]->location[2] || x1[2] <= points[0]->location[2])){ - return; - } - - // check to see if I am completely enclosed (for faster subdivision of children) - enclosed = ( - points[0]->location[0] > x0[0] && points[3]->location[0] < x1[0] && - points[0]->location[1] > x0[1] && points[3]->location[1] < x1[1] && - (n_dim == 2 || (n_dim == 3 && points[0]->location[2] > x0[2] && points[7]->location[2] < x1[2])) - ); - - } - // Will only be here if I intersect the box - if(is_leaf()){ - divide(nodes, xs, ys, zs, true, diag_balance); - } - // recurse into children - children[0]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - children[1]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - children[2]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - children[3]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - if (n_dim > 2){ - children[4]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - children[5]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - children[6]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - children[7]->refine_box(nodes, x0, x1, p_level, xs, ys, zs, enclosed, diag_balance); - } -} - -void Cell::refine_line(node_map_t& nodes, double* x0, double* x1, double* diff_inv, int_t p_level, double *xs, double *ys, double* zs, bool diag_balance){ - // Return If I'm at max_level or p_level - if (level >= p_level || level == max_level){ - return; - } - // then check to see if I intersect the segment - double t0x, t0y, t0z, t1x, t1y, t1z; - double tminx, tminy, tminz, tmaxx, tmaxy, tmaxz; - double tmin, tmax; - - t0x = (points[0]->location[0] - x0[0]) * diff_inv[0]; - t1x = (points[3]->location[0] - x0[0]) * diff_inv[0]; - if (t0x <= t1x){ - tminx = t0x; - tmaxx = t1x; - }else{ - tminx = t1x; - tmaxx = t0x; - } - - t0y = (points[0]->location[1] - x0[1]) * diff_inv[1]; - t1y = (points[3]->location[1] - x0[1]) * diff_inv[1]; - if (t0y <= t1y){ - tminy = t0y; - tmaxy = t1y; - }else{ - tminy = t1y; - tmaxy = t0y; - } - - tmin = std::max(tminx, tminy); - tmax = std::min(tmaxx, tmaxy); - if (n_dim > 2){ - t0z = (points[0]->location[2] - x0[2]) * diff_inv[2]; - t1z = (points[7]->location[2] - x0[2]) * diff_inv[2]; - if (t0z <= t1z){ - tminz = t0z; - tmaxz = t1z; - }else{ - tminz = t1z; - tmaxz = t0z; - } - tmin = std::max(tmin, tminz); - tmax = std::min(tmax, tmaxz); - } - // now can test if I intersect! - if (tmax >= 0 && tmin <= 1 && tmin <= tmax){ - if(is_leaf()){ - divide(nodes, xs, ys, zs, true, diag_balance); - } - // recurse into children - for(int_t i = 0; i < (1<refine_line(nodes, x0, x1, diff_inv, p_level, xs, ys, zs, diag_balance); - } - } -} - -void Cell::refine_triangle( - node_map_t& nodes, - double* x0, double* x1, double* x2, - double* e0, double* e1, double* e2, - double* t_norm, - int_t p_level, double *xs, double *ys, double* zs, bool diag_balance -){ - // Return If I'm at max_level or p_level - if (level >= p_level || level == max_level){ - return; - } - // then check to see if I intersect the segment - 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 < n_dim; ++i){ - v0[i] = x0[i] - location[i]; - v1[i] = x1[i] - location[i]; - vmin = std::min(v0[i], v1[i]); - vmax = std::max(v0[i], v1[i]); - v2[i] = x2[i] - location[i]; - vmin = std::min(vmin, v2[i]); - vmax = std::max(vmax, v2[i]); - half[i] = location[i] - points[0]->location[i]; - - // Bounding box check - if (vmin > half[i] || vmax < -half[i]){ - return; - } - } - // 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; - } - - // 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; - } - - // 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; - } - - if(n_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; - } - // 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; - } - // 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; - } - // 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; - } - // 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; - } - // 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; - } - - // triangle normal axis - pmin = 0.0; - pmax = 0.0; - for(int_t i=0; i 0){ - pmin += t_norm[i] * (-half[i] - v0[i]); - pmax += t_norm[i] * (half[i] - v0[i]); - }else{ - pmin += t_norm[i] * (half[i] - v0[i]); - pmax += t_norm[i] * (-half[i] - v0[i]); - } - } - if (pmin > 0 || pmax < 0){ - return; - } - } - // If here, then I intersect the triangle! - if(is_leaf()){ - divide(nodes, xs, ys, zs, true, diag_balance); - } - for(int_t i = 0; i < (1<refine_triangle( - nodes, x0, x1, x2, e0, e1, e2, t_norm, p_level, xs, ys, zs, diag_balance - ); - } -} - -void Cell::refine_vert_triang_prism( - node_map_t& nodes, - double* x0, double* x1, double* x2, double h, - double* e0, double* e1, double* e2, double* t_norm, - int_t p_level, double *xs, double *ys, double* zs, bool diag_balance -){ - // Return If I'm at max_level or p_level - if (level >= p_level || level == max_level){ - return; - } - // check all the AABB faces - 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 < n_dim; ++i){ - v0[i] = x0[i] - location[i]; - v1[i] = x1[i] - location[i]; - vmin = std::min(v0[i], v1[i]); - vmax = std::max(v0[i], v1[i]); - v2[i] = x2[i] - location[i]; - vmin = std::min(vmin, v2[i]); - vmax = std::max(vmax, v2[i]); - if(i == 2){ - vmax += h; - } - half[i] = location[i] - points[0]->location[i]; - - // Bounding box check - if (vmin > half[i] || vmax < -half[i]){ - return; - } - } - // 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; - } - - // 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; - } - - // 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; - } - - // 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; - } - // 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; - } - // 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; - } - // 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; - } - // 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; - } - // 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; - } - - // triangle normal axis - p0 = t_norm[0] * v0[0] + t_norm[1] * v0[1] + t_norm[2] * v0[2]; - p1 = t_norm[0] * v0[0] + t_norm[1] * v0[1] + t_norm[2] * (v0[2] + h); - pmin = std::min(p0, p1); - pmax = std::max(p0, p1); - rad = std::abs(t_norm[0]) * half[0] + std::abs(t_norm[1]) * half[1] + std::abs(t_norm[2]) * half[2]; - if (pmin > rad || pmax < -rad){ - return; - } - // the axes defined by the three vertical prism faces - // should already be tested by the e0, e1, e2 cross z_hat tests - - // If here, then I intersect the triangle! - if(is_leaf()){ - divide(nodes, xs, ys, zs, true, diag_balance); - } - for(int_t i = 0; i < (1<refine_vert_triang_prism( - nodes, x0, x1, x2, h, e0, e1, e2, t_norm, p_level, xs, ys, zs, diag_balance - ); - } -} - -void Cell::refine_tetra( - node_map_t& nodes, - double* x0, double* x1, double* x2, double* x3, - double edge_tans[6][3], double face_normals[4][3], - int_t p_level, double *xs, double *ys, double* zs, bool diag_balance -){ - // Return If I'm at max_level or p_level - if (level >= p_level || level == max_level){ - return; - } - if (n_dim < 3){ - return; - } - // then check to see if I intersect the segment - double v0[3], v1[3], v2[3], v3[3], half[3]; - double p0, p1, p2, p3, pmin, pmax, rad; - for(int_t i=0; i < n_dim; ++i){ - v0[i] = x0[i] - location[i]; - v1[i] = x1[i] - location[i]; - v2[i] = x2[i] - location[i]; - v3[i] = x3[i] - location[i]; - half[i] = location[i] - points[0]->location[i]; - pmin = std::min(std::min(std::min(v0[i], v1[i]), v2[i]), v3[i]); - pmax = std::max(std::max(std::max(v0[i], v1[i]), v2[i]), v3[i]); - // Bounding box check - if (pmin > half[i] || pmax < -half[i]){ - return; - } - } - // first do the 3 edge cross tests that apply in 2D and 3D - 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; - } - - 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; - } - - 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; - } - } - // 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; - } - } - // If here, then I intersect the tetrahedron! - if(is_leaf()){ - divide(nodes, xs, ys, zs, true, diag_balance); - } - for(int_t i = 0; i < (1<refine_tetra( - nodes, x0, x1, x2, x3, edge_tans, face_normals, 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){ @@ -1257,29 +727,6 @@ Cell* Cell::containing_cell(double x, double y, double z){ return children[ix + 2*iy + 4*iz]->containing_cell(x, y, z); }; -void Cell::find_overlapping_cells(int_vec_t& cells, double xm, double xp, double ym, double yp, double zm, double zp){ - // If I do not overlap the cells - if (xm > points[3]->location[0] || xp < points[0]->location[0]){ - return; - } - - if (ym > points[3]->location[1] || yp < points[0]->location[1]){ - return; - } - - if (n_dim>2 && (zm > points[7]->location[2] || zp < points[0]->location[2])){ - return; - } - - if(this->is_leaf()){ - cells.push_back(index); - return; - } - for(int_t i = 0; i < (1<find_overlapping_cells(cells, xm, xp, ym, yp, zm, zp); - } -} - Cell::~Cell(){ if(is_leaf()){ return; @@ -1449,107 +896,6 @@ void Tree::refine_function(function test_func, bool diagonal_balance){ roots[iz][iy][ix]->refine_func(nodes, test_func, xs, ys, zs, diagonal_balance); }; -void Tree::refine_box(double* x0, double* x1, int_t p_level, bool diagonal_balance){ - for(int_t iz=0; izrefine_box(nodes, x0, x1, p_level, xs, ys, zs, false, diagonal_balance); -}; - -void Tree::refine_ball(double* center, double r, int_t p_level, bool diagonal_balance){ - double r2 = r*r; - for(int_t iz=0; izrefine_ball(nodes, center, r2, p_level, xs, ys, zs, diagonal_balance); -}; - -void Tree::refine_line(double* x0, double* x1, int_t p_level, bool diagonal_balance){ - double diff_inv[3]; - for(int_t i=0; irefine_line(nodes, x0, x1, diff_inv, p_level, xs, ys, zs, diagonal_balance); -}; - -void Tree::refine_triangle(double* x0, double* x1, double* x2, int_t p_level, bool diagonal_balance){ - double e0[3], e1[3], e2[3], t_norm[3]; - for(int_t i=0; i 2){ - t_norm[0] = e0[1] * e1[2] - e0[2] * e1[1]; - t_norm[1] = e0[2] * e1[0] - e0[0] * e1[2]; - t_norm[2] = e0[0] * e1[1] - e0[1] * e1[0]; - } - for(int_t iz=0; izrefine_triangle( - nodes, x0, x1, x2, e0, e1, e2, t_norm, p_level, xs, ys, zs, diagonal_balance - ); -}; - -void Tree::refine_vert_triang_prism(double* x0, double* x1, double* x2, double h, int_t p_level, bool diagonal_balance){ - double e0[3], e1[3], e2[3], t_norm[3]; - for(int_t i=0; i 2){ - t_norm[0] = e0[1] * e1[2] - e0[2] * e1[1]; - t_norm[1] = e0[2] * e1[0] - e0[0] * e1[2]; - t_norm[2] = e0[0] * e1[1] - e0[1] * e1[0]; - } - for(int_t iz=0; izrefine_vert_triang_prism( - nodes, x0, x1, x2, h, e0, e1, e2, t_norm, p_level, xs, ys, zs, diagonal_balance - ); -}; - -void Tree::refine_tetra(double* x0, double* x1, double* x2, double* x3, int_t p_level, bool diagonal_balance){ - double t_edges[6][3]; - double face_normals[4][3]; - for(int_t i=0; irefine_tetra( - nodes, x0, x1, x2, x3, t_edges, face_normals, p_level, xs, ys, zs, diagonal_balance - ); -}; - void Tree::finalize_lists(){ for(int_t iz=0; izcontaining_cell(x, y, z); } -int_vec_t Tree::find_overlapping_cells(double xm, double xp, double ym, double yp, double zm, double zp){ - int_vec_t overlaps; - for(int_t iz=0; izfind_overlapping_cells(overlaps, xm, xp, ym, yp, zm, zp); - } - } - } - return overlaps; - } - void Tree::shift_cell_centers(double *shift){ for(int_t iz=0; iz #include +#include "geom.h" + typedef std::size_t int_t; inline int_t key_func(int_t x, int_t y){ @@ -80,6 +82,9 @@ class Edge{ Edge *parents[2]; Edge(); Edge(Node& p1, Node&p2); + double operator[](int_t index){ + return location[index]; + }; }; class Face{ @@ -96,6 +101,9 @@ class Face{ Face *parent; Face(); Face(Node& p1, Node& p2, Node& p3, Node& p4); + double operator[](int_t index){ + return location[index]; + }; }; class Cell{ @@ -109,43 +117,68 @@ class Cell{ 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[4], int_t ndim, int_t maxlevel);//, function func); - Cell(Node *pts[4], Cell *parent); + Cell(Node *pts[8], int_t ndim, int_t maxlevel);//, function func); + Cell(Node *pts[8], Cell *parent); ~Cell(); - bool inline is_leaf(){ return children[0]==NULL;}; + 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{ @@ -174,26 +207,37 @@ class Tree{ 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 refine_function(function test_func, bool diagonal_balance=false); - void refine_ball(double *center, double r, int_t p_level, bool diagonal_balance=false); - void refine_box(double* x0, double* x1, int_t p_level, bool diagonal_balance=false); - void refine_line(double* x0, double* x1, int_t p_level, bool diag_balance=false); - void refine_triangle( - double* x0, double* x1, double* x2, int_t p_level, bool diag_balance=false - ); - void refine_tetra( - double* x0, double* x1, double* x2, double* x3, int_t p_level, bool diag_balance=false - ); - void refine_vert_triang_prism( - double* x0, double* x1, double* x2, double h, int_t p_level, bool diagonal_balance=false - ); void number(); void finalize_lists(); - void insert_cell(double *new_center, int_t p_level, bool diagonal_balance=false); + 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); - int_vec_t find_overlapping_cells(double xm, double xp, double ym, double yp, double zm, double zp); - void shift_cell_centers(double *shift); + + void refine_function(function test_func, 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 diff --git a/discretize/_extensions/tree.pxd b/discretize/_extensions/tree.pxd index bb2fc35ce..7b428fbc8 100644 --- a/discretize/_extensions/tree.pxd +++ b/discretize/_extensions/tree.pxd @@ -15,7 +15,7 @@ cdef extern from "tree.h": Node *parents[4] Node() Node(int_t, int_t, int_t, double, double, double) - int_t operator[](int_t) + double operator[](int_t) cdef cppclass Edge: int_t location_ind[3] @@ -29,6 +29,7 @@ cdef extern from "tree.h": Edge *parents[2] Edge() Edge(Node& p1, Node& p2) + double operator[](int_t) cdef cppclass Face: int_t location_ind[3] @@ -43,6 +44,7 @@ cdef extern from "tree.h": 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 @@ -62,6 +64,9 @@ cdef extern from "tree.h": long long int index double volume inline bool is_leaf() + inline Node* min_node() + inline Node* max_node() + double operator[](int_t) cdef cppclass PyWrapper: PyWrapper() @@ -85,16 +90,13 @@ cdef extern from "tree.h": void set_levels(int_t, int_t, int_t) void set_xs(double*, double*, double*) void refine_function(PyWrapper *, bool) - void refine_ball(double*, double, int_t, bool) - void refine_box(double*, double*, int_t, bool) - void refine_line(double*, double*, int_t, bool) - void refine_triangle(double*, double*, double*, int_t, bool) - void refine_vert_triang_prism(double*, double*, double*, double, int_t, bool) - void refine_tetra(double*, double*, double*, double*, int_t, bool) + + void refine_geom[T](const T&, int_t, 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_overlapping_cells(double xm, double xp, double ym, double yp, double zm, double zp) + vector[int_t] find_cells_geom[T](const T& geom) void shift_cell_centers(double*) diff --git a/discretize/_extensions/tree_ext.pyx b/discretize/_extensions/tree_ext.pyx index f253d1efe..07de49ebd 100644 --- a/discretize/_extensions/tree_ext.pyx +++ b/discretize/_extensions/tree_ext.pyx @@ -9,6 +9,7 @@ from libcpp cimport bool from numpy.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 @@ -54,18 +55,6 @@ cdef class TreeCell: cdef void _set(self, c_Cell* cell): self._cell = cell self._dim = cell.n_dim - self._x = cell.location[0] - self._x0 = cell.points[0].location[0] - - self._y = cell.location[1] - self._y0 = cell.points[0].location[1] - - self._wx = cell.points[3].location[0] - self._x0 - self._wy = cell.points[3].location[1] - self._y0 - if(self._dim > 2): - self._z = cell.location[2] - self._z0 = cell.points[0].location[2] - self._wz = cell.points[7].location[2] - self._z0 @property def nodes(self): @@ -146,8 +135,10 @@ cdef class TreeCell: (dim) numpy.ndarray Cell center location for the tree cell """ - if self._dim == 2: return np.array([self._x, self._y]) - return np.array([self._x, self._y, self._z]) + 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): @@ -162,8 +153,10 @@ cdef class TreeCell: (dim) numpy.ndarray Origin location ('anchor point') for the tree cell """ - if self._dim == 2: return np.array([self._x0, self._y0]) - return np.array([self._x0, self._y0, self._z0]) + 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): @@ -192,8 +185,19 @@ cdef class TreeCell: (dim) numpy.ndarray Cell dimension along each axis direction """ - if self._dim == 2: return np.array([self._wx, self._wy]) - return np.array([self._wx, self._wy, self._wz]) + 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): @@ -217,6 +221,43 @@ cdef class TreeCell: """ 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. @@ -238,63 +279,64 @@ cdef class TreeCell: neighbors = [-1]*self._dim*2 for i in range(self._dim*2): - if self._cell.neighbors[i] is NULL: + neighbor = self._cell.neighbors[i] + if neighbor is NULL: continue - elif self._cell.neighbors[i].is_leaf(): - neighbors[i] = self._cell.neighbors[i].index + elif neighbor.is_leaf(): + neighbors[i] = neighbor.index else: if self._dim==2: if i==0: - neighbors[i] = [self._cell.neighbors[i].children[1].index, - self._cell.neighbors[i].children[3].index] + neighbors[i] = [neighbor.children[1].index, + neighbor.children[3].index] elif i==1: - neighbors[i] = [self._cell.neighbors[i].children[0].index, - self._cell.neighbors[i].children[2].index] + neighbors[i] = [neighbor.children[0].index, + neighbor.children[2].index] elif i==2: - neighbors[i] = [self._cell.neighbors[i].children[2].index, - self._cell.neighbors[i].children[3].index] + neighbors[i] = [neighbor.children[2].index, + neighbor.children[3].index] else: - neighbors[i] = [self._cell.neighbors[i].children[0].index, - self._cell.neighbors[i].children[1].index] + neighbors[i] = [neighbor.children[0].index, + neighbor.children[1].index] else: if i==0: - neighbors[i] = [self._cell.neighbors[i].children[1].index, - self._cell.neighbors[i].children[3].index, - self._cell.neighbors[i].children[5].index, - self._cell.neighbors[i].children[7].index] + neighbors[i] = [neighbor.children[1].index, + neighbor.children[3].index, + neighbor.children[5].index, + neighbor.children[7].index] elif i==1: - neighbors[i] = [self._cell.neighbors[i].children[0].index, - self._cell.neighbors[i].children[2].index, - self._cell.neighbors[i].children[4].index, - self._cell.neighbors[i].children[6].index] + neighbors[i] = [neighbor.children[0].index, + neighbor.children[2].index, + neighbor.children[4].index, + neighbor.children[6].index] elif i==2: - neighbors[i] = [self._cell.neighbors[i].children[2].index, - self._cell.neighbors[i].children[3].index, - self._cell.neighbors[i].children[6].index, - self._cell.neighbors[i].children[7].index] + neighbors[i] = [neighbor.children[2].index, + neighbor.children[3].index, + neighbor.children[6].index, + neighbor.children[7].index] elif i==3: - neighbors[i] = [self._cell.neighbors[i].children[0].index, - self._cell.neighbors[i].children[1].index, - self._cell.neighbors[i].children[4].index, - self._cell.neighbors[i].children[5].index] + neighbors[i] = [neighbor.children[0].index, + neighbor.children[1].index, + neighbor.children[4].index, + neighbor.children[5].index] elif i==4: - neighbors[i] = [self._cell.neighbors[i].children[4].index, - self._cell.neighbors[i].children[5].index, - self._cell.neighbors[i].children[6].index, - self._cell.neighbors[i].children[7].index] + neighbors[i] = [neighbor.children[4].index, + neighbor.children[5].index, + neighbor.children[6].index, + neighbor.children[7].index] else: - neighbors[i] = [self._cell.neighbors[i].children[0].index, - self._cell.neighbors[i].children[1].index, - self._cell.neighbors[i].children[2].index, - self._cell.neighbors[i].children[3].index] + 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((self._cell.location_ind[0], self._cell.location_ind[1])) - return tuple((self._cell.location_ind[0], self._cell.location_ind[1], - self._cell.location_ind[2])) + return tuple((loc_ind[0], loc_ind[1])) + return tuple((loc_ind[0], loc_ind[1], loc_ind[2])) @property def _level(self): @@ -561,39 +603,37 @@ cdef class _TreeMesh: >>> ax.add_patch(circ) >>> plt.show() """ - points = np.require(np.atleast_2d(points), dtype=np.float64, - requirements='C') - if points.shape[1] != self.dim: - raise ValueError(f"points array must be (N, {self.dim})") + 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 - radii = np.require(np.atleast_1d(radii), dtype=np.float64, - requirements='C') - if radii.shape[0] == 1: - radii = np.full(points.shape[0], radii[0], dtype=np.float64) cdef double[:] rs = radii - if points.shape[0] != rs.shape[0]: - raise ValueError("radii length must match the points array's first dimension") - - levels = np.require(np.atleast_1d(levels), dtype=np.int32, - requirements='C') - if levels.shape[0] == 1: - levels = np.full(points.shape[0], levels[0], dtype=np.int32) cdef int[:] ls = levels - if points.shape[0] != ls.shape[0]: - raise ValueError("level length must match the points array's first dimension") + + 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(ls.shape[0]): - l = ls[i] - if l < 0: - l = (max_level + 1) - (abs(l) % (max_level + 1)) - self.tree.refine_ball(&cs[i, 0], rs[i], l, diag_balance) + 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) + self.tree.refine_geom(ball, l, diag_balance) + + i_c += cs_step + i_r += rs_step + i_l += l_step if finalize: self.finalize() @@ -648,37 +688,36 @@ cdef class _TreeMesh: >>> ax.add_patch(rect) >>> plt.show() """ - x0s = np.require(np.atleast_2d(x0s), dtype=np.float64, - requirements='C') - if x0s.shape[1] != self.dim: - raise ValueError(f"x0s array must be (N, {self.dim})") - x1s = np.require(np.atleast_2d(x1s), dtype=np.float64, - requirements='C') - if x1s.shape[1] != self.dim: - raise ValueError(f"x1s array must be (N, {self.dim})") - if x1s.shape[0] != x0s.shape[0]: - raise ValueError(f"x0s and x1s must have the same length") + 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 - levels = np.require(np.atleast_1d(levels), dtype=np.int32, - requirements='C') - if levels.shape[0] == 1: - levels = np.full(x0.shape[0], levels[0], dtype=np.int32) cdef int[:] ls = levels - if x0.shape[0] != ls.shape[0]: - raise ValueError("level length must match the points array's first dimension") + + 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(ls.shape[0]): - l = ls[i] - if l < 0: - l = (max_level + 1) - (abs(l) % (max_level + 1)) - self.tree.refine_box(&x0[i, 0], &x1[i, 0], l, diag_balance) + 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) + self.tree.refine_geom(box, l, diag_balance) + + i_x0 += x0_step + i_x1 += x1_step + i_l += l_step if finalize: self.finalize() @@ -729,33 +768,116 @@ cdef class _TreeMesh: >>> plt.show() """ - path = np.require(np.atleast_2d(path), dtype=np.float64, - requirements='C') - if path.shape[1] != self.dim: - raise ValueError(f"line_nodes array must be (N, {self.dim})") + 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 - levels = np.require(np.atleast_1d(levels), dtype=np.int32, - requirements='C') - cdef int n_segments = line_nodes.shape[0] - 1; - if levels.shape[0] == 1: - levels = np.full(n_segments, levels[0], dtype=np.int32) - if n_segments != levels.shape[0]: - raise ValueError(f"inconsistent number of line segments {n_segments} and levels {levels.shape[0]}") - 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): - l = ls[i] - if l < 0: - l = (max_level + 1) - (abs(l) % (max_level + 1)) - self.tree.refine_line(&line_nodes[i, 0], &line_nodes[i+1, 0], l, diag_balance) + line = geom.Line(self._dim, &line_nodes[i_line, 0], &line_nodes[i_line+1, 0]) + l = _wrap_levels(ls[i_l], max_level) + 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) + self.tree.refine_geom(plane, l, diag_balance) + + i_o += origin_step + i_n += normal_step + i_l += level_step if finalize: self.finalize() @@ -806,34 +928,33 @@ cdef class _TreeMesh: >>> plt.show() """ - triangle = np.require(np.atleast_2d(triangle), dtype=np.float64, requirements="C") - if triangle.ndim == 2: - triangle = triangle[None, ...] - if triangle.shape[-1] != self.dim or triangle.shape[-2] != 3: + 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})") - cdef double[:, :, :] tris = triangle - - levels = np.require(np.atleast_1d(levels), dtype=np.int32, - requirements='C') - cdef int n_triangles = triangle.shape[0]; - if levels.shape[0] == 1: - levels = np.full(n_triangles, levels[0], dtype=np.int32) - if n_triangles != levels.shape[0]: - raise ValueError(f"inconsistent number of triangles {n_triangles} and levels {levels.shape[0]}") + 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): - l = ls[i] - if l < 0: - l = (max_level + 1) - (abs(l) % (max_level + 1)) - self.tree.refine_triangle(&tris[i, 0, 0], &tris[i, 1, 0], &tris[i, 2, 0], l, diag_balance) + 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) + self.tree.refine_geom(triang, l, diag_balance) + + i_tri += tri_step + i_l += l_step if finalize: self.finalize() @@ -894,42 +1015,42 @@ cdef class _TreeMesh: """ if self.dim == 2: raise NotImplementedError("refine_vertical_trianglular_prism only implemented in 3D.") - triangle = np.require(np.atleast_2d(triangle), dtype=np.float64, requirements="C") - if triangle.ndim == 2: - triangle = triangle[None, ...] - if triangle.shape[-1] != self.dim or triangle.shape[-2] != 3: + 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})") - cdef double[:, :, :] tris = triangle h = np.require(np.atleast_1d(h), dtype=np.float64, requirements="C") - levels = np.require(np.atleast_1d(levels), dtype=np.int32, - requirements='C') - cdef int n_triangles = triangle.shape[0]; - if levels.shape[0] == 1: - levels = np.full(n_triangles, levels[0], dtype=np.int32) - if h.shape[0] == 1: - h = np.full(n_triangles, h[0], dtype=np.float64) - if n_triangles != levels.shape[0]: - raise ValueError(f"inconsistent number of triangles {n_triangles} and levels {levels.shape[0]}") - if n_triangles != h.shape[0]: - raise ValueError(f"inconsistent number of triangles {n_triangles} and heights {h.shape[0]}") if np.any(h < 0): raise ValueError("All heights must be positive.") - cdef int[:] ls = levels + 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): - l = ls[i] - if l < 0: - l = (max_level + 1) - (abs(l) % (max_level + 1)) - self.tree.refine_vert_triang_prism(&tris[i, 0, 0], &tris[i, 1, 0], &tris[i, 2, 0], hs[i], l, diag_balance) + 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) + 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() @@ -986,34 +1107,34 @@ cdef class _TreeMesh: """ if self.dim == 2: return self.refine_triangle(tetra, levels, finalize=finalize, diagonal_balance=diagonal_balance) - tetra = np.require(np.atleast_2d(tetra), dtype=np.float64, requirements="C") - if tetra.ndim == 2: - tetra = tetra[None, ...] - if tetra.shape[-1] != self.dim or tetra.shape[-2] != self.dim+1: + 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})") - cdef double[:, :, :] tris = tetra + levels = np.require(np.atleast_1d(levels), dtype=np.int32, requirements='C') - levels = np.require(np.atleast_1d(levels), dtype=np.int32, - requirements='C') - cdef int n_triangles = tetra.shape[0]; - if levels.shape[0] == 1: - levels = np.full(n_triangles, levels[0], dtype=np.int32) - if n_triangles != levels.shape[0]: - raise ValueError(f"inconsistent number of triangles {n_triangles} and levels {levels.shape[0]}") + 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 = ls[i] - if l < 0: - l = (max_level + 1) - (abs(l) % (max_level + 1)) - self.tree.refine_tetra(&tris[i, 0, 0], &tris[i, 1, 0], &tris[i, 2, 0], &tris[i, 3, 0], l, diag_balance) + 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]) + self.tree.refine_geom(tet, l, diag_balance) + + i_tri += tri_step + i_l += l_step if finalize: self.finalize() @@ -1050,25 +1171,29 @@ cdef class _TreeMesh: ----------------------- Total : 40 """ - points = np.require(np.atleast_2d(points), dtype=np.float64, - requirements='C') - if points.shape[1] != self.dim: - raise ValueError(f"points array must be (N, {self.dim})") + 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 = np.require(np.atleast_1d(levels), dtype=np.int32, - requirements='C') - if points.shape[0] != ls.shape[0]: - raise ValueError("level length must match the points array's first dimension") + 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 = ls[i] - if l < 0: - l = (max_level + 1) - (abs(l) % (max_level + 1)) - self.tree.insert_cell(&cs[i, 0], l, diagonal_balance) + l = _wrap_levels(ls[i_l], max_level) + self.tree.insert_cell(&cs[i_p, 0], l, diagonal_balance) + + i_l += l_step + i_p += p_step if finalize: self.finalize() @@ -1103,10 +1228,211 @@ cdef class _TreeMesh: """ return self._finalized + @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.""" 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') @@ -2566,7 +2892,7 @@ cdef class _TreeMesh: @cython.cdivision(True) def get_cells_along_line(self, x0, x1): - """Find the cells along a line segment defined by two points. + """Find the cells in order along a line segment. Parameters ---------- @@ -2635,6 +2961,10 @@ cdef class _TreeMesh: tz = INFINITY t = min(tx,ty,tz) + if t >= 1: + # then the segment ended in the current cell. + # do not bother checking anymore. + break #intersection point ipx = (bx-ax)*t+ax @@ -2642,24 +2972,30 @@ cdef class _TreeMesh: ipz = (bz-az)*t+az next_cell = cur_cell - if tx<=ty and tx<=tz: + 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 ty<=tx and ty<=tz: + 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 dim==3 and tz<=tx and tz<=ty: + 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) @@ -5179,47 +5515,28 @@ cdef class _TreeMesh: is_b |= (nodes[:, 2] == z0) | (nodes[:, 2] == zF) return sp.eye(self.n_nodes, format='csr')[is_b] - def _get_containing_cell_index(self, loc): - cdef double x, y, z - x = loc[0] - y = loc[1] - if self._dim == 3: - z = loc[2] - else: - z = 0 - return self.tree.containing_cell(x, y, z).index - - def _get_containing_cell_indexes(self, locs): - locs = np.require(np.atleast_2d(locs), dtype=np.float64, requirements='C') - cdef double[:,:] d_locs = locs - 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 _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): - index = np.require(np.atleast_1d(index), dtype=np.int64, requirements='C') - cdef np.int64_t[:] inds = index - cdef int_t n_cells = inds.shape[0] + 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): - levels[i] = self.tree.cells[inds[i]].level + ii = i if do_all else inds[i] + levels[i] = self.tree.cells[ii].level if n_cells == 1: return levels[0] else: @@ -6161,14 +6478,16 @@ cdef class _TreeMesh: cdef c_Cell * out_cell cdef c_Cell * in_cell - cdef np.float64_t[:] vals = np.array([]) - cdef np.float64_t[:] outs = np.array([]) + 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) + output = np.empty(self.n_cells, dtype=np.float64) + else: + output = np.require(output, dtype=np.float64, requirements=['A', 'W']) output[:] = 0 outs = output @@ -6181,8 +6500,10 @@ cdef class _TreeMesh: cdef vector[int_t] *overlapping_cells 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 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: @@ -6259,16 +6580,16 @@ cdef class _TreeMesh: return output return P + cdef geom.Box *box + cdef int_t last_point_ind = 7 if self._dim==3 else 3 for cell in self.tree.cells: - x1m = min(cell.points[0].location[0], xF[0]) - y1m = min(cell.points[0].location[1], xF[1]) + 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]) - 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]) - overlapping_cell_inds = meshin.tree.find_overlapping_cells(x1m, x1p, y1m, y1p, z1m, z1p) + box = new geom.Box(self._dim, x1m, x1p) + overlapping_cell_inds = meshin.tree.find_cells_geom(box[0]) + del box n_overlap = overlapping_cell_inds.size() weights = malloc(n_overlap*sizeof(double)) i = 0 @@ -6276,26 +6597,13 @@ cdef class _TreeMesh: nnz_row = 0 for in_cell_ind in overlapping_cell_inds: in_cell = meshin.tree.cells[in_cell_ind] - x2m = in_cell.points[0].location[0] - y2m = in_cell.points[0].location[1] - z2m = in_cell.points[0].location[2] - x2p = in_cell.points[3].location[0] - y2p = in_cell.points[3].location[1] - z2p = in_cell.points[7].location[2] if self._dim==3 else 0.0 + x2m = in_cell.min_node().location + x2p = in_cell.max_node().location - if x1m == xF[0] or x1p == origin[0]: - over_lap_vol = 1.0 - else: - over_lap_vol = min(x1p, x2p) - max(x1m, x2m) - if y1m == xF[1] or y1p == origin[1]: - over_lap_vol *= 1.0 - else: - over_lap_vol *= min(y1p, y2p) - max(y1m, y2m) - if self._dim==3: - if z1m == xF[2] or z1p == origin[2]: - over_lap_vol *= 1.0 - else: - over_lap_vol *= min(z1p, z2p) - max(z1m, z2m) + 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: @@ -6304,10 +6612,11 @@ cdef class _TreeMesh: 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 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): @@ -6329,8 +6638,10 @@ cdef class _TreeMesh: cdef vector[int_t] *overlapping_cells 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 double x1m[3] + cdef double x1p[3] + cdef double x2m[3] + cdef double x2p[3] cdef double[:] origin cdef double[:] xF @@ -6346,7 +6657,7 @@ cdef class _TreeMesh: same_base = False if same_base: - in_cell_inds = self._get_containing_cell_indexes(out_tens_mesh.cell_centers) + 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: @@ -6381,6 +6692,7 @@ cdef class _TreeMesh: 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 @@ -6389,31 +6701,35 @@ cdef class _TreeMesh: if values is not None: vals = values if output is None: - output = np.empty((nx, ny, nz), order='F') + output = np.empty(out_tens_mesh.n_cells, dtype=np.float64) else: - output = output.reshape((nx, ny, nz), order='F') + output = np.require(output, dtype=np.float64, requirements=['A', 'W']) output[:] = 0 - outs = output + 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 + 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): - z1m = min(nodes_z[iz], xF[2]) - z1p = max(nodes_z[iz+1], origin[2]) + x1m[2] = min(nodes_z[iz], xF[2]) + x1p[2] = max(nodes_z[iz+1], origin[2]) for iy in range(ny): - y1m = min(nodes_y[iy], xF[1]) - y1p = max(nodes_y[iy+1], origin[1]) + x1m[1] = min(nodes_y[iy], xF[1]) + x1p[1] = max(nodes_y[iy+1], origin[1]) for ix in range(nx): - x1m = min(nodes_x[ix], xF[0]) - x1p = max(nodes_x[ix+1], origin[0]) - overlapping_cell_inds = self.tree.find_overlapping_cells(x1m, x1p, y1m, y1p, z1m, z1p) + x1m[0] = min(nodes_x[ix], xF[0]) + x1p[0] = max(nodes_x[ix+1], origin[0]) + + box = new geom.Box(self._dim, x1m, x1p) + overlapping_cell_inds = self.tree.find_cells_geom(box[0]) + del box + n_overlap = overlapping_cell_inds.size() weights = malloc(n_overlap*sizeof(double)) i = 0 @@ -6421,26 +6737,13 @@ cdef class _TreeMesh: nnz_row = 0 for in_cell_ind in overlapping_cell_inds: in_cell = self.tree.cells[in_cell_ind] - x2m = in_cell.points[0].location[0] - y2m = in_cell.points[0].location[1] - z2m = in_cell.points[0].location[2] - x2p = in_cell.points[3].location[0] - y2p = in_cell.points[3].location[1] - z2p = in_cell.points[7].location[2] if self._dim==3 else 0.0 - - if x1m == xF[0] or x1p == origin[0]: - over_lap_vol = 1.0 - else: - over_lap_vol = min(x1p, x2p) - max(x1m, x2m) - if y1m == xF[1] or y1p == origin[1]: - over_lap_vol *= 1.0 - else: - over_lap_vol *= min(y1p, y2p) - max(y1m, y2m) - if self._dim==3: - if z1m == xF[2] or z1p == origin[2]: - over_lap_vol *= 1.0 - else: - over_lap_vol *= min(z1p, z2p) - max(z1m, z2m) + 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: @@ -6448,10 +6751,12 @@ cdef class _TreeMesh: 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 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): @@ -6464,7 +6769,7 @@ cdef class _TreeMesh: overlapping_cell_inds.clear() if not build_mat: - return output.reshape(-1, order='F') + return output return sp.csr_matrix((all_weights, row_inds, indptr), shape=(out_tens_mesh.n_cells, self.n_cells)) @cython.boundscheck(False) @@ -6491,7 +6796,7 @@ cdef class _TreeMesh: if same_base: - out_cell_inds = self._get_containing_cell_indexes(in_tens_mesh.cell_centers) + 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: @@ -6522,14 +6827,16 @@ cdef class _TreeMesh: else: xF = np.array([nodes_x[-1], nodes_y[-1], nodes_z[-1]]) - cdef np.float64_t[::1, :, :] vals = np.array([[[]]]) - cdef np.float64_t[:] outs = np.array([]) + 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) + output = np.empty(self.n_cells, dtype=np.float64) + else: + output = np.require(output, dtype=np.float64, requirements=['A', 'W']) output[:] = 0 outs = output @@ -6646,7 +6953,7 @@ cdef class _TreeMesh: Parameters ---------- - rectangle: (dim * 2) array_like + 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. @@ -6655,25 +6962,72 @@ cdef class _TreeMesh: list of int The indices of cells which overlap the axis aligned rectangle. """ - cdef double xm, ym, zm, xp, yp, zp - cdef double[:] origin = self._origin - cdef double[:] xF - if self.dim == 2: - xF = np.array([self._xs[-1], self._ys[-1]]) - else: - xF = np.array([self._xs[-1], self._ys[-1], self._zs[-1]]) - xm = min(rectangle[0], xF[0]) - xp = max(rectangle[1], origin[0]) - ym = min(rectangle[2], xF[1]) - yp = max(rectangle[3], origin[1]) - if self.dim==3: - zm = min(rectangle[4], xF[2]) - zp = max(rectangle[5], origin[2]) + return self.get_cells_in_aabb(*rectangle.reshape(self.dim, 2).T) + + 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: - zm = 0.0 - zp = 0.0 - return self.tree.find_overlapping_cells(xm, xp, ym, yp, zm, zp) + 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 diff --git a/discretize/base/base_mesh.py b/discretize/base/base_mesh.py index f51e7b121..be7bc4c6d 100644 --- a/discretize/base/base_mesh.py +++ b/discretize/base/base_mesh.py @@ -2410,15 +2410,15 @@ def get_face_inner_product_deriv( >>> import numpy as np >>> import matplotlib as mpl >>> mpl.rcParams.update({'font.size': 14}) - >>> np.random.seed(45) + >>> 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 = np.random.rand(mesh.nC) # physical property parameters - >>> u = np.random.rand(mesh.nF) # vector of shape (n_faces) + >>> 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) @@ -2449,8 +2449,8 @@ def get_face_inner_product_deriv( function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation of this function on a spy plot. - >>> m = np.random.rand(mesh.nC, 3) # anisotropic physical property parameters - >>> u = np.random.rand(mesh.nF) # vector of shape (n_faces) + >>> 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) @@ -2593,14 +2593,14 @@ def get_edge_inner_product_deriv( >>> import numpy as np >>> import matplotlib as mpl >>> mpl.rcParams.update({'font.size': 14}) - >>> np.random.seed(45) + >>> 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 = np.random.rand(mesh.nC) # physical property parameters - >>> u = np.random.rand(mesh.nF) # vector of shape (n_edges) + >>> 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) @@ -2631,8 +2631,8 @@ def get_edge_inner_product_deriv( function handle :math:`\mathbf{F}(\mathbf{u})` and plot the evaluation of this function on a spy plot. - >>> m = np.random.rand(mesh.nC, 3) # physical property parameters - >>> u = np.random.rand(mesh.nF) # vector of shape (n_edges) + >>> 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) @@ -4119,9 +4119,9 @@ def get_interpolation_matrix( >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt - >>> np.random.seed(14) + >>> rng = np.random.default_rng(14) - >>> locs = np.random.rand(50)*0.8+0.1 + >>> locs = rng.random(50)*0.8+0.1 >>> dense = np.linspace(0, 1, 200) >>> fun = lambda x: np.cos(2*np.pi*x) diff --git a/discretize/mixins/mesh_io.py b/discretize/mixins/mesh_io.py index 049cbb69c..742ed12e5 100644 --- a/discretize/mixins/mesh_io.py +++ b/discretize/mixins/mesh_io.py @@ -133,7 +133,7 @@ def unpackdx(fid, nrows): return tensMsh @classmethod - def read_UBC(cls, file_name, directory=""): + def read_UBC(cls, file_name, directory=None): """Read 2D or 3D tensor mesh from UBC-GIF formatted file. Parameters @@ -149,6 +149,8 @@ def read_UBC(cls, file_name, directory=""): 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) @@ -221,7 +223,7 @@ def _readModelUBC_3D(mesh, file_name): model = mkvc(model) return model - def read_model_UBC(mesh, file_name, directory=""): + def read_model_UBC(mesh, file_name, directory=None): """Read UBC-GIF formatted model file for 2D or 3D tensor mesh. Parameters @@ -236,6 +238,8 @@ def read_model_UBC(mesh, file_name, directory=""): (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) @@ -245,7 +249,7 @@ def read_model_UBC(mesh, file_name, directory=""): raise Exception("mesh must be a Tensor Mesh 2D or 3D") return model - def write_model_UBC(mesh, file_name, model, directory=""): + def write_model_UBC(mesh, file_name, model, directory=None): """Write 2D or 3D tensor model to UBC-GIF formatted file. Parameters @@ -257,6 +261,8 @@ def write_model_UBC(mesh, file_name, model, directory=""): 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 @@ -372,7 +378,7 @@ def writeF(fx, outStr=""): f.write(outStr) f.close() - def write_UBC(mesh, file_name, models=None, directory="", comment_lines=""): + 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 @@ -387,6 +393,8 @@ def write_UBC(mesh, file_name, models=None, directory="", comment_lines=""): 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) @@ -419,7 +427,7 @@ class TreeMeshIO(object): """ @classmethod - def read_UBC(TreeMesh, file_name, directory=""): + def read_UBC(TreeMesh, file_name, directory=None): """Read 3D tree mesh (OcTree mesh) from UBC-GIF formatted file. Parameters @@ -434,6 +442,8 @@ def read_UBC(TreeMesh, file_name, directory=""): 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) @@ -468,7 +478,7 @@ def read_UBC(TreeMesh, file_name, directory=""): mesh.__setstate__((indArr, levels)) return mesh - def read_model_UBC(mesh, file_name): + def read_model_UBC(mesh, file_name, directory=None): """Read UBC-GIF formatted file model file for 3D tree mesh (OcTree). Parameters @@ -477,7 +487,7 @@ def read_model_UBC(mesh, file_name): 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 lives (optional) + directory where the UBC-GIF file(s) lives (optional) Returns ------- @@ -485,10 +495,12 @@ def read_model_UBC(mesh, file_name): 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) + out[f] = mesh.read_model_UBC(f, directory=directory) return out modArr = np.loadtxt(file_name) @@ -502,7 +514,7 @@ def read_model_UBC(mesh, file_name): model = modArr[un_order].copy() # ensure a contiguous array return model - def write_UBC(mesh, file_name, models=None, directory=""): + def write_UBC(mesh, file_name, models=None, directory=None): """Write OcTree mesh (and models) to UBC-GIF formatted files. Parameters @@ -515,6 +527,8 @@ def write_UBC(mesh, file_name, models=None, directory=""): 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") @@ -545,13 +559,9 @@ def write_UBC(mesh, file_name, models=None, directory=""): 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) - def write_model_UBC(mesh, file_name, model, directory=""): + def write_model_UBC(mesh, file_name, model, directory=None): """Write 3D tree model (OcTree) to UBC-GIF formatted file. Parameters @@ -563,6 +573,8 @@ def write_model_UBC(mesh, file_name, model, directory=""): 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) diff --git a/discretize/mixins/mpl_mod.py b/discretize/mixins/mpl_mod.py index a3926b964..024470e67 100644 --- a/discretize/mixins/mpl_mod.py +++ b/discretize/mixins/mpl_mod.py @@ -425,7 +425,7 @@ def plot_slice( >>> from matplotlib import pyplot as plt >>> import discretize - >>> from pymatsolver import Solver + >>> 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)] @@ -437,7 +437,7 @@ def plot_slice( >>> q[[4, 4], [4, 4], [2, 6]]=[-1, 1] >>> q = discretize.utils.mkvc(q) >>> A = M.face_divergence * M.cell_gradient - >>> b = Solver(A) * (q) + >>> b = spsolve(A, q) and finaly, plot the vector values of the result, which are defined on faces @@ -704,9 +704,6 @@ def plot_3d_slicer( # Connect figure to scrolling fig.canvas.mpl_connect("scroll_event", tracker.onscroll) - # Show figure - plt.show() - # TensorMesh plotting def __plot_grid_tensor( self, @@ -2047,29 +2044,26 @@ def __plot_slice_tree( if not isinstance(ind, (np.integer, int)): raise ValueError("ind must be an integer") - cc_tensor = [None, None, None] - for i in range(3): - cc_tensor[i] = np.cumsum(np.r_[self.origin[i], self.h[i]]) - cc_tensor[i] = (cc_tensor[i][1:] + cc_tensor[i][:-1]) * 0.5 + 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) level_diff = self.max_level - temp_mesh.max_level - XS = [None, None, None] - XS[antiNormalInd[0]], XS[antiNormalInd[1]] = np.meshgrid( - cc_tensor[antiNormalInd[0]], cc_tensor[antiNormalInd[1]] - ) - XS[normalInd] = np.ones_like(XS[antiNormalInd[0]]) * slice_loc - loc_grid = np.c_[XS[0].reshape(-1), XS[1].reshape(-1), XS[2].reshape(-1)] - inds = np.unique(self._get_containing_cell_indexes(loc_grid)) - - grid2d = self.gridCC[inds][:, antiNormalInd] + # 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.nC, 3)) - tm_gridboost[:, antiNormalInd] = temp_mesh.gridCC + 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" @@ -2102,7 +2096,7 @@ def __plot_slice_tree( v = np.linalg.norm(v, axis=1) # interpolate values from self.gridCC to grid2d - ind_3d_to_2d = self._get_containing_cell_indexes(tm_gridboost) + ind_3d_to_2d = self.get_containing_cells(tm_gridboost) v2d = v[ind_3d_to_2d] out = temp_mesh.plot_image( @@ -2351,6 +2345,7 @@ def __init__( """Initialize interactive figure.""" _, plt = load_matplotlib() from matplotlib.widgets import Slider # Lazy loaded + from matplotlib.colors import Normalize # Add pcolor_opts to self self.pc_props = pcolor_opts if pcolor_opts is not None else {} @@ -2396,7 +2391,7 @@ def __init__( # 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_where(np.isnan(self.v), self.v) + 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 @@ -2436,30 +2431,22 @@ def __init__( else: aspect3 = 1.0 / aspect2 - # set color limits if clim is None (and norm doesn't have vmin, vmax). - if clim is None: - if "norm" in self.pc_props: - vmin = self.pc_props["norm"].vmin - vmax = self.pc_props["norm"].vmax - else: - vmin = vmax = None - clim = [ - np.nanmin(self.v) if vmin is None else vmin, - np.nanmax(self.v) if vmax is None else vmax, - ] - # In the case of a homogeneous fullspace provide a small range to - # avoid problems with colorbar and the three subplots. - if clim[0] == clim[1]: - clim[0] *= 0.99 - clim[1] *= 1.01 - - # ensure vmin/vmax of the norm is consistent with clim - if "norm" in self.pc_props: - self.pc_props["norm"].vmin = clim[0] - self.pc_props["norm"].vmax = clim[1] + # 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: - self.pc_props["vmin"] = clim[0] - self.pc_props["vmax"] = clim[1] + 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 @@ -2552,6 +2539,7 @@ def __init__( # 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]) diff --git a/discretize/mixins/vtk_mod.py b/discretize/mixins/vtk_mod.py index f79ca7fa3..aaba082ad 100644 --- a/discretize/mixins/vtk_mod.py +++ b/discretize/mixins/vtk_mod.py @@ -206,24 +206,22 @@ def __tree_mesh_to_vtk(mesh, models=None): # Make the data parts for the vtu object # Points - ptsMat = np.vstack((mesh.gridN, mesh.gridhN)) + nodes = mesh.total_nodes # Adjust if result was 2D (voxels are pixels in 2D): - VTK_CELL_TYPE = _vtk.VTK_VOXEL - if ptsMat.shape[1] == 2: - # Add Z values of 0.0 if 2D - ptsMat = np.c_[ptsMat, np.zeros(ptsMat.shape[0])] - VTK_CELL_TYPE = _vtk.VTK_PIXEL - if ptsMat.shape[1] != 3: - raise RuntimeError("Points of the mesh are improperly defined.") + VTK_CELL_TYPE = _vtk.VTK_VOXEL if mesh.dim == 3 else _vtk.VTK_PIXEL + # Rotate the points to the cartesian system - ptsMat = np.dot(ptsMat, mesh.rotation_matrix) + 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(ptsMat, deep=True)) + vtkPts.SetData(_nps.numpy_to_vtk(nodes, deep=True)) + # Cells - cellArray = [c for c in mesh] - cellConn = np.array([cell.nodes for cell in cellArray]) + cellConn = mesh.cell_nodes cellsMat = np.concatenate( (np.ones((cellConn.shape[0], 1), dtype=int) * cellConn.shape[1], cellConn), axis=1, @@ -239,7 +237,7 @@ def __tree_mesh_to_vtk(mesh, models=None): output.SetPoints(vtkPts) output.SetCells(VTK_CELL_TYPE, cellsArr) # Add the level of refinement as a cell array - cell_levels = np.array([cell._level for cell in cellArray]) + cell_levels = mesh._cell_levels_by_indexes() refineLevelArr = _nps.numpy_to_vtk(cell_levels, deep=1) refineLevelArr.SetName("octreeLevel") output.GetCellData().AddArray(refineLevelArr) diff --git a/discretize/operators/differential_operators.py b/discretize/operators/differential_operators.py index 80e0fb718..bda3954c0 100644 --- a/discretize/operators/differential_operators.py +++ b/discretize/operators/differential_operators.py @@ -13,6 +13,7 @@ av, av_extrap, make_boundary_bool, + cross2d, ) @@ -2207,7 +2208,12 @@ def boundary_edge_vector_integral(self): if self.dim > 2: Av *= 2 - w_cross_n = np.cross(-w, Av.T @ dA) + 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") diff --git a/discretize/tensor_mesh.py b/discretize/tensor_mesh.py index cd63271ec..f0b2694b9 100644 --- a/discretize/tensor_mesh.py +++ b/discretize/tensor_mesh.py @@ -592,6 +592,26 @@ def face_boundary_indices(self): 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. diff --git a/discretize/tests.py b/discretize/tests.py index 48e506518..34f5452b6 100644 --- a/discretize/tests.py +++ b/discretize/tests.py @@ -77,11 +77,10 @@ "You break it, you fix it.", ] -# Initiate random number generator -rng = np.random.default_rng() +_happiness_rng = np.random.default_rng() -def setup_mesh(mesh_type, nC, nDim): +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, @@ -99,19 +98,25 @@ def setup_mesh(mesh_type, nC, nDim): 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: + 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 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 - h2 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 - h3 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 + 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") @@ -126,14 +131,14 @@ def setup_mesh(mesh_type, nC, nDim): else: h = [nC, nC, nC] elif "random" in mesh_type: - h1 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 + h1 = rng.random(nC) * nC * 0.5 + nC * 0.5 if "symmetric" in mesh_type: h2 = [ 2 * np.pi, ] else: - h2 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 - h3 = np.random.rand(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 h[1] = h[1] * 2 * np.pi else: @@ -178,9 +183,9 @@ def setup_mesh(mesh_type, nC, nDim): if "uniform" in mesh_type or "notatree" in mesh_type: h = [nC, nC, nC] elif "random" in mesh_type: - h1 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 - h2 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 - h3 = np.random.rand(nC) * nC * 0.5 + nC * 0.5 + 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") @@ -215,13 +220,13 @@ class for the given operator. Within the test class, the user sets the parameter OrderTest inherits from :class:`unittest.TestCase`. - Parameters + 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 of the list {'uniformTensorMesh', 'randomTensorMesh', + 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) @@ -235,6 +240,10 @@ class for the given operator. Within the test class, the user sets the parameter 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 ----- @@ -301,6 +310,7 @@ class for the given operator. Within the test class, the user sets the parameter meshTypes = ["uniformTensorMesh"] _meshType = meshTypes[0] meshDimension = 3 + random_seed = None def setupMesh(self, nC): """Generate mesh and set as current mesh for testing. @@ -315,7 +325,9 @@ def setupMesh(self, nC): Float Maximum cell width for the mesh """ - mesh, max_h = setup_mesh(self._meshType, nC, self.meshDimension) + mesh, max_h = setup_mesh( + self._meshType, nC, self.meshDimension, random_seed=self.random_seed + ) self.M = mesh return max_h @@ -334,7 +346,7 @@ def getError(self): """ return 1.0 - def orderTest(self): + def orderTest(self, random_seed=None): """Perform an order test. For number of cells specified in meshSizes setup mesh, call getError @@ -359,6 +371,9 @@ def orderTest(self): "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() @@ -495,9 +510,9 @@ class in older versions of `discretize`. np.testing.assert_allclose(orders[-1], expected_order, rtol=rtol) elif test_type == "all": np.testing.assert_allclose(orders, expected_order, rtol=rtol) - print(np.random.choice(happiness)) + print(_happiness_rng.choice(happiness)) except AssertionError as err: - print(np.random.choice(sadness)) + print(_happiness_rng.choice(sadness)) raise err return orders @@ -551,6 +566,7 @@ def check_derivative( tolerance=0.85, eps=1e-10, ax=None, + random_seed=None, ): """Perform a basic derivative check. @@ -559,8 +575,8 @@ def check_derivative( Parameters ---------- - fctn : function - Function handle + fctn : callable + The function to test. x0 : numpy.ndarray Point at which to check derivative num : int, optional @@ -569,7 +585,7 @@ def check_derivative( 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. + step direction is chosen using `rng`. expectedOrder : int, optional The expected order of convergence for the numerical derivative tolerance : float, optional @@ -579,6 +595,10 @@ def check_derivative( 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 ------- @@ -590,10 +610,11 @@ def check_derivative( >>> 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, np.random.randn(5)) + >>> passed = tests.check_derivative(simplePass, rng.standard_normal(5), random_seed=rng) ==================== check_derivative ==================== iter h |ft-f0| |ft-f0-h*J0*dx| Order --------------------------------------------------------- @@ -610,6 +631,7 @@ def check_derivative( __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 @@ -626,7 +648,8 @@ def check_derivative( x0 = mkvc(x0) if dx is None: - dx = np.random.randn(len(x0)) + rng = np.random.default_rng(random_seed) + dx = rng.standard_normal(len(x0)) h = np.logspace(-1, -num, num) E0 = np.ones(h.shape) @@ -692,14 +715,17 @@ def _plot_it(axes, passed): # Thus it has no higher order derivatives. pass else: - test = np.mean(order1) > tolerance * expectedOrder + order_mean = np.mean(order1) + expected = tolerance * expectedOrder + test = order_mean > expected if not test: raise AssertionError( - f"\n Order mean {np.mean(order1)} is not greater than" - f" {tolerance} of the expected order {expectedOrder}." + 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(np.random.choice(happiness) + "\n") + print(_happiness_rng.choice(happiness) + "\n") if plotIt: _plot_it(ax, True) except AssertionError as err: @@ -708,7 +734,7 @@ def _plot_it(axes, passed): "*" * 57, "<" * 25, ">" * 25, "*" * 57 ) ) - print(np.random.choice(sadness) + "\n") + print(_happiness_rng.choice(sadness) + "\n") if plotIt: _plot_it(ax, False) raise err @@ -772,6 +798,7 @@ def assert_isadjoint( 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. @@ -822,6 +849,9 @@ def assert_isadjoint( 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 ------- @@ -836,6 +866,8 @@ def assert_isadjoint( """ __tracebackhide__ = True + rng = np.random.default_rng(random_seed) + def random(size, iscomplex): """Create random data of size and dtype of .""" out = rng.standard_normal(size) @@ -873,3 +905,117 @@ def random(size, iscomplex): ) 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 diff --git a/discretize/tree_mesh.py b/discretize/tree_mesh.py index fc5e81636..f049084b0 100644 --- a/discretize/tree_mesh.py +++ b/discretize/tree_mesh.py @@ -90,7 +90,6 @@ from discretize.base import BaseTensorMesh from discretize.operators import InnerProducts, DiffOperators from discretize.mixins import InterfaceMixins, TreeMeshIO -from discretize.utils import as_array_n_by_dim from discretize._extensions.tree_ext import _TreeMesh, TreeCell # NOQA F401 import numpy as np import scipy.sparse as sp @@ -435,7 +434,8 @@ def refine_bounding_box( >>> import matplotlib.pyplot as plt >>> import matplotlib.patches as patches >>> mesh = discretize.TreeMesh([32, 32]) - >>> points = np.random.rand(20, 2) * 0.25 + 3/8 + >>> 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 @@ -925,9 +925,7 @@ def face_z_divergence(self): # NOQA D102 def point2index(self, locs): # NOQA D102 # Documentation inherited from discretize.base.BaseMesh - locs = as_array_n_by_dim(locs, self.dim) - inds = self._get_containing_cell_indexes(locs) - return inds + return self.get_containing_cell_indexes(locs) def cell_levels_by_index(self, indices): """Fast function to return a list of levels for the given cell indices. @@ -948,14 +946,11 @@ def get_interpolation_matrix( # NOQA D102 self, locs, location_type="cell_centers", zeros_outside=False ): # Documentation inherited from discretize.base.BaseMesh - locs = as_array_n_by_dim(locs, self.dim) 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 = np.require(np.atleast_2d(locs), dtype=np.float64, requirements="C") - + 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"]: diff --git a/discretize/unstructured_mesh.py b/discretize/unstructured_mesh.py index f7a70e32d..408442cd0 100644 --- a/discretize/unstructured_mesh.py +++ b/discretize/unstructured_mesh.py @@ -3,7 +3,7 @@ import numpy as np import scipy.sparse as sp from scipy.spatial import KDTree -from discretize.utils import Identity, invert_blocks, spzeros +from discretize.utils import Identity, invert_blocks, spzeros, cross2d from discretize.base import BaseMesh from discretize._extensions.simplex_helpers import ( _build_faces_edges, @@ -264,7 +264,7 @@ def face_normals(self): # NOQA D102 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.cross(self.edge_tangents, [0, 0, 1])[:, :-1] + 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 @@ -346,7 +346,7 @@ def edge_curl(self): # NOQA D102 # 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(np.cross(l20, l01)) + cp = np.sign(cross2d(l20, l01)) face_areas = face_areas * cp # don't due *= here @@ -405,7 +405,8 @@ def __get_inner_product_projection_matrices( ): if getattr(self, "_proj_stash", None) is None: self._proj_stash = {} - if i_type not in 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": @@ -456,8 +457,8 @@ def __get_inner_product_projection_matrices( ) Ps.append(T @ P) - self._proj_stash[i_type] = (Ps, T_col_inds, T_ind_ptr) - Ps, T_col_inds, T_ind_ptr = self._proj_stash[i_type] + 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: @@ -811,10 +812,7 @@ def get_interpolation_matrix( # NOQA D102 n_items = self.n_edges elif location_type[:-2] == "faces": # grab the barycentric transforms associated with each simplex: - ts = transform[ - inds, - :, - ] + 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] @@ -822,18 +820,26 @@ def get_interpolation_matrix( # NOQA D102 # [1, 2], [0, 2], [0, 1] if self.dim == 2: + # i j k + # t0 t1 t2 + # 0 0 1 + # t1 * i - t0 * j + f0 = ( - barys[:, 1] * (np.cross(ts[:, 2], [0, 0, 1])[:, i_dir]) - + barys[:, 2] * (np.cross([0, 0, 1], ts[:, 1])[:, i_dir]) - ) * areas[faces[:, 0]] + cross2d(barys[:, 1:], ts[:, 1:, 1 - i_dir]) * areas[faces[:, 0]] + ) f1 = ( - barys[:, 0] * (np.cross(ts[:, 2], [0, 0, 1])[:, i_dir]) - + barys[:, 2] * (np.cross([0, 0, 1], ts[:, 0])[:, i_dir]) - ) * areas[faces[:, 1]] + cross2d(barys[:, [0, 2]], ts[:, [0, 2], 1 - i_dir]) + * areas[faces[:, 1]] + ) f2 = ( - barys[:, 0] * (np.cross(ts[:, 1], [0, 0, 1])[:, i_dir]) - + barys[:, 1] * (np.cross([0, 0, 1], ts[:, 0])[:, i_dir]) - ) * areas[faces[:, 2]] + 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: @@ -1218,7 +1224,7 @@ def boundary_edge_vector_integral(self): # NOQA D102 Pe = self.project_edge_to_boundary_edge n_boundary_edges, n_edges = Pe.shape index = boundary_face_edges - w_cross_n = np.cross(-self.edge_tangents[index], dA) + 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 diff --git a/discretize/utils/__init__.py b/discretize/utils/__init__.py index 13b0dbc17..4d28c02a9 100644 --- a/discretize/utils/__init__.py +++ b/discretize/utils/__init__.py @@ -77,6 +77,7 @@ invert_blocks make_property_tensor inverse_property_tensor + cross2d Mesh Utilities -------------- diff --git a/discretize/utils/interpolation_utils.py b/discretize/utils/interpolation_utils.py index 807dba1d6..4e542b90d 100644 --- a/discretize/utils/interpolation_utils.py +++ b/discretize/utils/interpolation_utils.py @@ -83,11 +83,11 @@ def interpolation_matrix(locs, x, y=None, z=None): >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt - >>> np.random.seed(14) + >>> rng = np.random.default_rng(14) Create an interpolation matrix - >>> locs = np.random.rand(50)*0.8+0.1 + >>> 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) @@ -216,6 +216,7 @@ def volume_average(mesh_in, mesh_out, values=None, output=None): >>> 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]) @@ -225,7 +226,7 @@ def volume_average(mesh_in, mesh_out, values=None, output=None): interpolate it to the output mesh. >>> from discretize.utils import volume_average - >>> model1 = np.random.rand(mesh_in.nC) + >>> 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 diff --git a/discretize/utils/matrix_utils.py b/discretize/utils/matrix_utils.py index 7f0b21679..bb35cf84a 100644 --- a/discretize/utils/matrix_utils.py +++ b/discretize/utils/matrix_utils.py @@ -34,8 +34,9 @@ def mkvc(x, n_dims=1): >>> from discretize.utils import mkvc >>> import numpy as np + >>> rng = np.random.default_rng(856) - >>> a = np.random.rand(3, 2) + >>> a = rng.random(3, 2) >>> a array([[0.33534155, 0.25334363], [0.07147884, 0.81080958], @@ -555,7 +556,8 @@ def get_subarray(A, ind): >>> from discretize.utils import get_subarray >>> import numpy as np - >>> A = np.random.rand(3, 3) + >>> 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], @@ -1143,6 +1145,7 @@ def make_property_tensor(mesh, tensor): >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl + >>> rng = np.random.default_rng(421) Define a 2D tensor mesh @@ -1152,9 +1155,9 @@ def make_property_tensor(mesh, tensor): Define a physical property for all cases (2D) >>> sigma_scalar = 4. - >>> sigma_isotropic = np.random.randint(1, 10, mesh.nC) - >>> sigma_anisotropic = np.random.randint(1, 10, (mesh.nC, 2)) - >>> sigma_tensor = np.random.randint(1, 10, (mesh.nC, 3)) + >>> 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 @@ -1295,6 +1298,7 @@ def inverse_property_tensor(mesh, tensor, return_matrix=False): >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import matplotlib as mpl + >>> rng = np.random.default_rng(421) Define a 2D tensor mesh @@ -1304,9 +1308,9 @@ def inverse_property_tensor(mesh, tensor, return_matrix=False): Define a physical property for all cases (2D) >>> sigma_scalar = 4. - >>> sigma_isotropic = np.random.randint(1, 10, mesh.nC) - >>> sigma_anisotropic = np.random.randint(1, 10, (mesh.nC, 2)) - >>> sigma_tensor = np.random.randint(1, 10, (mesh.nC, 3)) + >>> 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 @@ -1397,6 +1401,31 @@ def inverse_property_tensor(mesh, tensor, return_matrix=False): 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. diff --git a/discretize/utils/mesh_utils.py b/discretize/utils/mesh_utils.py index 192eb96d2..bf512c2c3 100644 --- a/discretize/utils/mesh_utils.py +++ b/discretize/utils/mesh_utils.py @@ -28,7 +28,7 @@ def random_model(shape, seed=None, anisotropy=None, its=100, bounds=None): ---------- shape : (dim) tuple of int shape of the model. - seed : int, optional + 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 @@ -67,15 +67,14 @@ def random_model(shape, seed=None, anisotropy=None, its=100, bounds=None): if bounds is None: bounds = [0, 1] + rng = np.random.default_rng(seed) if seed is None: - seed = np.random.randint(1e3) - print("Using a seed of: ", seed) + print("Using a seed of: ", rng.bit_generator.seed_seq) if type(shape) in num_types: shape = (shape,) # make it a tuple for consistency - np.random.seed(seed) - mr = np.random.rand(*shape) + mr = rng.random(shape) if anisotropy is None: if len(shape) == 1: smth = np.array([1, 10.0, 1], dtype=float) @@ -423,8 +422,9 @@ def mesh_builder_xyz( >>> import discretize >>> import matplotlib.pyplot as plt >>> import numpy as np + >>> rng = np.random.default_rng(87142) - >>> xy_loc = np.random.randn(8,2) + >>> 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]], diff --git a/docs/_static/versions.json b/docs/_static/versions.json new file mode 100644 index 000000000..7b2780fe4 --- /dev/null +++ b/docs/_static/versions.json @@ -0,0 +1,13 @@ +[ + { "name": "main", + "version": "dev", + "url": "https://discretize.simpeg.xyz/en/main/" + }, + { + "name": "0.10.0 (stable)", + "version": "v0.10.0", + "url": "https://discretize.simpeg.xyz/en/v0.10.0/", + "preferred": true + } +] + diff --git a/docs/conf.py b/docs/conf.py index ced3d2480..e0165cb41 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,6 +15,7 @@ import sys from pathlib import Path from datetime import datetime +from packaging.version import parse import discretize from sphinx_gallery.sorting import FileNameSortKey import shutil @@ -94,8 +95,16 @@ # # 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]) +# version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -144,7 +153,6 @@ if link_github: import inspect - from packaging.version import parse extensions.append("sphinx.ext.linkcode") @@ -197,16 +205,7 @@ def linkcode_resolve(domain, info): fn = os.path.relpath(fn, start=os.path.dirname(discretize.__file__)) except ValueError: return None - - discretize_version = parse(discretize.__version__) - tag = ( - "main" - if discretize_version.is_devrelease - else f"v{discretize_version.public}" - ) - return ( - f"https://github.com/simpeg/discretize/blob/{tag}/discretize/{fn}{linespec}" - ) + return f"https://github.com/simpeg/discretize/blob/{branch}/discretize/{fn}{linespec}" else: extensions.append("sphinx.ext.viewcode") @@ -246,6 +245,7 @@ def linkcode_resolve(domain, info): dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), ] + # Use Pydata Sphinx theme html_theme = "pydata_sphinx_theme" @@ -279,7 +279,13 @@ def linkcode_resolve(domain, info): "use_edit_page_button": False, "collapse_navigation": True, "navbar_align": "left", # make elements closer to logo on the left - "navbar_end": ["theme-switcher", "navbar-icon-links"], + "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" diff --git a/docs/content/additional_resources.rst b/docs/content/additional_resources.rst index 4e94eafd0..373fc7f22 100644 --- a/docs/content/additional_resources.rst +++ b/docs/content/additional_resources.rst @@ -14,14 +14,14 @@ found on the official websites of the packages Python for scientific computing ------------------------------- -* `Python for Scientists `_ Links to commonly used packages, Matlab to Python comparison +* `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 `_ +* `Python vs Matlab `_ Lessons in Python ----------------- diff --git a/docs/release/0.10.0-notes.rst b/docs/release/0.10.0-notes.rst index 7cf5c73f7..05e0e71ab 100644 --- a/docs/release/0.10.0-notes.rst +++ b/docs/release/0.10.0-notes.rst @@ -22,7 +22,7 @@ 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 -seperate from the install environment, as the build requirements are not the same as the runtime +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 @@ -30,11 +30,11 @@ systems from python 3.8 to 3.12. Our goal is to provide a pre-compiled wheel for scipy provides wheels for. Tensor Mesh ------------- -You can now directly index a ``TensorMesh``, and it will then return a ``TensorCell`` object. -This functionality mimics what is currently available in ``TreeMesh``s. +----------- +You can now directly index a ``TensorMesh`` and return a ``TensorCell`` object. +This functionality mimics what is currently available in ``TreeMesh``. -``Tensor Mesh`` also has a ``cell_nodes`` property that list the indices of each node of every +``TensorMesh`` also has a ``cell_nodes`` property that list the indices of each node of every cell (again similar to the ``TreeMesh``). Face Properties diff --git a/docs/release/0.7.1-notes.rst b/docs/release/0.7.1-notes.rst index 1eeab2052..292e52d77 100644 --- a/docs/release/0.7.1-notes.rst +++ b/docs/release/0.7.1-notes.rst @@ -42,4 +42,4 @@ Pull requests * `#256 `__: Update mpl_mod.py * `#258 `__: Numpy docstrings api review * `#262 `__: Fix wrong colour for fullspaces - again / -`#264 `__: patch for fullspace slicer colorscales +* `#264 `__: patch for fullspace slicer colorscales diff --git a/docs/release/0.8.1-notes.rst b/docs/release/0.8.1-notes.rst index e3536e2f3..a2ac19517 100644 --- a/docs/release/0.8.1-notes.rst +++ b/docs/release/0.8.1-notes.rst @@ -54,5 +54,5 @@ Pull requests * `#284 `__: Improve load time * `#285 `__: zeros_outside * `#286 `__: Cell node tree -* `#288 `__: Allow np.int_ +* `#288 `__: Allow ``np.int_`` * `#289 `__: 0.8.1 Release diff --git a/examples/plot_cahn_hilliard.py b/examples/plot_cahn_hilliard.py index 6ccfd2c2f..2784df405 100644 --- a/examples/plot_cahn_hilliard.py +++ b/examples/plot_cahn_hilliard.py @@ -44,9 +44,9 @@ """ import discretize -from pymatsolver import Solver import numpy as np import matplotlib.pyplot as plt +from scipy.sparse.linalg import spsolve def run(plotIt=True, n=60): @@ -92,7 +92,7 @@ def run(plotIt=True, n=60): MAT = dt * A * d2fdphi2 - I - dt * A * L rhs = (dt * A * d2fdphi2 - I) * phi - dt * A * dfdphi - phi = Solver(MAT) * rhs + phi = spsolve(MAT, rhs) if elapsed > capture[jj]: PHIS += [(elapsed, phi.copy())] diff --git a/examples/plot_dc_resistivity.py b/examples/plot_dc_resistivity.py index d8ee4abda..8bfdf76f0 100644 --- a/examples/plot_dc_resistivity.py +++ b/examples/plot_dc_resistivity.py @@ -6,9 +6,9 @@ """ import discretize -from pymatsolver import SolverLU import numpy as np import matplotlib.pyplot as plt +from scipy.sparse.linalg import spsolve def run(plotIt=True): @@ -37,12 +37,10 @@ def DCfun(mesh, pts): # Step3: Solve DC problem (LU solver) AtM, rhstM = DCfun(tM, pts) - AinvtM = SolverLU(AtM) - phitM = AinvtM * rhstM + phitM = spsolve(AtM, rhstM) ArM, rhsrM = DCfun(rM, pts) - AinvrM = SolverLU(ArM) - phirM = AinvrM * rhsrM + phirM = spsolve(ArM, rhsrM) if not plotIt: return diff --git a/examples/plot_slicer_demo.py b/examples/plot_slicer_demo.py index 2573e72fd..bcd5f19a9 100644 --- a/examples/plot_slicer_demo.py +++ b/examples/plot_slicer_demo.py @@ -9,10 +9,13 @@ Using the inversion result from the example notebook `plot_laguna_del_maule_inversion.ipynb `_ -In the notebook, you have to use :code:`%matplotlib notebook`. +You have to use :code:`%matplotlib notebook` in Jupyter Notebook, and +:code:`%matplotlib widget` in Jupyter Lab (latter requires the package +``ipympl``). """ # %matplotlib notebook +# %matplotlib widget import discretize import numpy as np import matplotlib.pyplot as plt @@ -47,6 +50,7 @@ # ^^^^^^^^^^^^^^^^^^^ mesh.plot_3d_slicer(Lpout) +plt.show() ############################################################################### # 1.2 Create a function to improve plots, labeling after creation @@ -101,6 +105,7 @@ def beautify(title, fig=None): # mesh.plot_3d_slicer(Lpout) beautify("mesh.plot_3d_slicer(Lpout)") +plt.show() ############################################################################### # 1.3 Set `xslice`, `yslice`, and `zslice`; transparent region @@ -115,6 +120,7 @@ def beautify(title, fig=None): "mesh.plot_3d_slicer(" "\nLpout, 370000, 6002500, -2500, transparent=[[-0.02, 0.1]])" ) +plt.show() ############################################################################### # 1.4 Set `clim`, use `pcolor_opts` to show grid lines @@ -127,6 +133,7 @@ def beautify(title, fig=None): "mesh.plot_3d_slicer(\nLpout, clim=[-0.4, 0.2], " "pcolor_opts={'edgecolor': 'k', 'linewidth': 0.1})" ) +plt.show() ############################################################################### # 1.5 Use `pcolor_opts` to set `SymLogNorm`, and another `cmap` @@ -139,6 +146,7 @@ def beautify(title, fig=None): "mesh.plot_3d_slicer(Lpout," "\npcolor_opts={'norm': SymLogNorm(linthresh=0.01),'cmap': 'RdBu_r'})`" ) +plt.show() ############################################################################### # 1.6 Use :code:`aspect` and :code:`grid` @@ -156,6 +164,7 @@ def beautify(title, fig=None): mesh.plot_3d_slicer(Lpout, aspect=["equal", 1.5], grid=[4, 4, 3]) beautify("mesh.plot_3d_slicer(Lpout, aspect=['equal', 1.5], grid=[4, 4, 3])") +plt.show() ############################################################################### # 1.7 Transparency-slider @@ -166,6 +175,7 @@ def beautify(title, fig=None): mesh.plot_3d_slicer(Lpout, transparent="slider") beautify("mesh.plot_3d_slicer(Lpout, transparent='slider')") +plt.show() ############################################################################### diff --git a/meson.build b/meson.build index f48d064e3..eb8cd0e77 100644 --- a/meson.build +++ b/meson.build @@ -14,7 +14,7 @@ print(get_version())''' ).stdout().strip(), license: 'MIT', - meson_version: '>= 1.1.0', + meson_version: '>= 1.4.0', default_options: [ 'buildtype=debugoptimized', 'b_ndebug=if-release', diff --git a/pyproject.toml b/pyproject.toml index ca54b91cd..c4b6f5fb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,12 +65,11 @@ omf = ["omf"] all = ["discretize[plot,viz,omf]"] doc = [ "sphinx!=4.1.0", - "pydata-sphinx-theme==0.9.0", + "pydata-sphinx-theme==0.15.4", "sphinx-gallery==0.1.13", "numpydoc>=1.5", "jupyter", "graphviz", - "pymatsolver>=0.1.2", "pillow", "pooch", "discretize[all]", @@ -118,6 +117,13 @@ build-verbosity = "3" # test importing discretize to make sure externals are loadable. test-command = 'python -c "import discretize; print(discretize.__version__)"' + +# use the visual studio compilers +[tool.cibuildwheel.windows.config-settings] +setup-args = [ + '--vsenv' +] + [tool.coverage.run] branch = true source = ["discretize", "tests", "examples", "tutorials"] diff --git a/tests/base/test_coordutils.py b/tests/base/test_coordutils.py index 97d565df3..efa15ba37 100644 --- a/tests/base/test_coordutils.py +++ b/tests/base/test_coordutils.py @@ -4,15 +4,16 @@ tol = 1e-15 +rng = np.random.default_rng(523) + class coorutilsTest(unittest.TestCase): def test_rotation_matrix_from_normals(self): - np.random.seed(0) - v0 = np.random.rand(3) + v0 = rng.random(3) v0 *= 1.0 / np.linalg.norm(v0) np.random.seed(5) - v1 = np.random.rand(3) + v1 = rng.random(3) v1 *= 1.0 / np.linalg.norm(v1) Rf = utils.rotation_matrix_from_normals(v0, v1) @@ -22,12 +23,11 @@ def test_rotation_matrix_from_normals(self): self.assertTrue(np.linalg.norm(utils.mkvc(Ri.dot(v1) - v0)) < tol) def test_rotate_points_from_normals(self): - np.random.seed(10) - v0 = np.random.rand(3) + v0 = rng.random(3) v0 *= 1.0 / np.linalg.norm(v0) np.random.seed(15) - v1 = np.random.rand(3) + v1 = rng.random(3) v1 *= 1.0 / np.linalg.norm(v1) v2 = utils.mkvc(utils.rotate_points_from_normals(utils.mkvc(v0, 2).T, v0, v1)) @@ -35,16 +35,15 @@ def test_rotate_points_from_normals(self): self.assertTrue(np.linalg.norm(v2 - v1) < tol) def test_rotateMatrixFromNormals(self): - np.random.seed(20) - n0 = np.random.rand(3) + n0 = rng.random(3) n0 *= 1.0 / np.linalg.norm(n0) np.random.seed(25) - n1 = np.random.rand(3) + n1 = rng.random(3) n1 *= 1.0 / np.linalg.norm(n1) np.random.seed(30) - scale = np.random.rand(100, 1) + scale = rng.random((100, 1)) XYZ0 = scale * n0 XYZ1 = scale * n1 diff --git a/tests/base/test_interpolation.py b/tests/base/test_interpolation.py index c9826128a..1b433e9bd 100644 --- a/tests/base/test_interpolation.py +++ b/tests/base/test_interpolation.py @@ -3,8 +3,6 @@ import discretize -np.random.seed(182) - MESHTYPES = ["uniformTensorMesh", "randomTensorMesh"] TOLERANCES = [0.9, 0.5, 0.5] call1 = lambda fun, xyz: fun(xyz) @@ -43,12 +41,13 @@ class TestInterpolation1D(discretize.tests.OrderTest): - LOCS = np.random.rand(50) * 0.6 + 0.2 name = "Interpolation 1D" meshTypes = MESHTYPES tolerance = TOLERANCES meshDimension = 1 meshSizes = [8, 16, 32, 64, 128] + random_seed = np.random.default_rng(55124) + LOCS = random_seed.random(50) * 0.6 + 0.2 def getError(self): funX = lambda x: np.cos(2 * np.pi * x) @@ -95,11 +94,12 @@ def test_outliers(self): class TestInterpolation2d(discretize.tests.OrderTest): name = "Interpolation 2D" - LOCS = np.random.rand(50, 2) * 0.6 + 0.2 meshTypes = MESHTYPES tolerance = TOLERANCES meshDimension = 2 meshSizes = [8, 16, 32, 64] + random_seed = np.random.default_rng(2457) + LOCS = random_seed.random((50, 2)) * 0.6 + 0.2 def getError(self): funX = lambda x, y: np.cos(2 * np.pi * y) @@ -178,13 +178,16 @@ def test_exceptions(self): class TestInterpolationSymCyl(discretize.tests.OrderTest): name = "Interpolation Symmetric 3D" - LOCS = np.c_[ - np.random.rand(4) * 0.6 + 0.2, np.zeros(4), np.random.rand(4) * 0.6 + 0.2 - ] meshTypes = ["uniform_symmetric_CylMesh"] # MESHTYPES + tolerance = 0.6 meshDimension = 3 meshSizes = [32, 64, 128, 256] + random_seed = np.random.default_rng(81756234) + LOCS = np.c_[ + random_seed.random(4) * 0.6 + 0.2, + np.zeros(4), + random_seed.random(4) * 0.6 + 0.2, + ] def getError(self): funX = lambda x, y: np.cos(2 * np.pi * y) @@ -244,14 +247,15 @@ def test_orderEy(self): class TestInterpolationCyl(discretize.tests.OrderTest): name = "Interpolation Cylindrical 3D" - LOCS = np.c_[ - np.random.rand(20) * 0.6 + 0.2, - 2 * np.pi * (np.random.rand(20) * 0.6 + 0.2), - np.random.rand(20) * 0.6 + 0.2, - ] meshTypes = ["uniformCylMesh", "randomCylMesh"] # MESHTYPES + meshDimension = 3 meshSizes = [8, 16, 32, 64] + random_seed = np.random.default_rng(876234) + LOCS = np.c_[ + random_seed.random(20) * 0.6 + 0.2, + 2 * np.pi * (random_seed.random(20) * 0.6 + 0.2), + random_seed.random(20) * 0.6 + 0.2, + ] def getError(self): func = lambda x, y, z: np.cos(2 * np.pi * x) + np.cos(y) + np.cos(2 * np.pi * z) @@ -314,8 +318,9 @@ def test_orderEz(self): class TestInterpolation3D(discretize.tests.OrderTest): + random_seed = np.random.default_rng(234) name = "Interpolation" - LOCS = np.random.rand(50, 3) * 0.6 + 0.2 + LOCS = random_seed.random((50, 3)) * 0.6 + 0.2 meshTypes = MESHTYPES tolerance = TOLERANCES meshDimension = 3 diff --git a/tests/base/test_operators.py b/tests/base/test_operators.py index aa84d76f5..b5fc225a3 100644 --- a/tests/base/test_operators.py +++ b/tests/base/test_operators.py @@ -5,7 +5,7 @@ # Tolerance TOL = 1e-14 -np.random.seed(1) +gen = np.random.default_rng(1) MESHTYPES = [ "uniformTensorMesh", @@ -506,8 +506,8 @@ def test_orderE2FV(self): class TestAverating2DSimple(unittest.TestCase): def setUp(self): - hx = np.random.rand(10) - hy = np.random.rand(10) + hx = gen.random(10) + hy = gen.random(10) self.mesh = discretize.TensorMesh([hx, hy]) def test_constantEdges(self): @@ -624,9 +624,9 @@ def test_orderCC2FV(self): class TestAverating3DSimple(unittest.TestCase): def setUp(self): - hx = np.random.rand(10) - hy = np.random.rand(10) - hz = np.random.rand(10) + hx = gen.random(10) + hy = gen.random(10) + hz = gen.random(10) self.mesh = discretize.TensorMesh([hx, hy, hz]) def test_constantEdges(self): @@ -778,7 +778,7 @@ def test_DivCurl(self): mesh, _ = discretize.tests.setup_mesh( meshType, self.meshSize, self.meshDimension ) - v = np.random.rand(mesh.nE) + v = gen.random(mesh.nE) divcurlv = mesh.face_divergence * (mesh.edge_curl * v) rel_err = np.linalg.norm(divcurlv) / np.linalg.norm(v) passed = rel_err < self.tol @@ -792,7 +792,7 @@ def test_CurlGrad(self): mesh, _ = discretize.tests.setup_mesh( meshType, self.meshSize, self.meshDimension ) - v = np.random.rand(mesh.nN) + v = gen.random(mesh.nN) curlgradv = mesh.edge_curl * (mesh.nodal_gradient * v) rel_err = np.linalg.norm(curlgradv) / np.linalg.norm(v) passed = rel_err < self.tol diff --git a/tests/base/test_slicer.py b/tests/base/test_slicer.py new file mode 100644 index 000000000..303761be8 --- /dev/null +++ b/tests/base/test_slicer.py @@ -0,0 +1,56 @@ +import numpy as np +import pytest +from matplotlib.colors import Normalize +import discretize +from discretize.mixins.mpl_mod import Slicer + + +@pytest.fixture() +def mesh(): + return discretize.TensorMesh([9, 10, 11]) + + +def test_slicer_errors(mesh): + model = np.ones(mesh.shape_cells) + with pytest.raises( + ValueError, + match="(Passing a Normalize instance simultaneously with clim is not supported).*", + ): + Slicer(mesh, model, clim=[0, 1], pcolor_opts={"norm": Normalize()}) + + +def test_slicer_default_clim(mesh): + model = np.ones(mesh.shape_cells) + model[0, 0, 0] = 0.5 + slc = Slicer(mesh, model) + norm = slc.pc_props["norm"] + assert (norm.vmin, norm.vmax) == (0.5, 1.0) + + +def test_slicer_set_clim(mesh): + model = np.ones(mesh.shape_cells) + slc = Slicer(mesh, model, clim=(0.5, 1.5)) + norm = slc.pc_props["norm"] + assert (norm.vmin, norm.vmax) == (0.5, 1.5) + + +def test_slicer_set_norm(mesh): + model = np.ones(mesh.shape_cells) + norm = Normalize(0.5, 1.5) + slc = Slicer(mesh, model, pcolor_opts={"norm": norm}) + norm = slc.pc_props["norm"] + assert (norm.vmin, norm.vmax) == (0.5, 1.5) + + +def test_slicer_ones_clim(mesh): + model = np.ones(mesh.shape_cells) + slc = Slicer(mesh, model) + norm = slc.pc_props["norm"] + assert (norm.vmin, norm.vmax) == (0.9, 1.1) + + +def test_slicer_zeros_clim(mesh): + model = np.zeros(mesh.shape_cells) + slc = Slicer(mesh, model) + norm = slc.pc_props["norm"] + assert (norm.vmin, norm.vmax) == (-0.1, 0.1) diff --git a/tests/base/test_tensor.py b/tests/base/test_tensor.py index 9349cafa7..b0c30dec4 100644 --- a/tests/base/test_tensor.py +++ b/tests/base/test_tensor.py @@ -2,10 +2,12 @@ import numpy as np import unittest import discretize -from pymatsolver import Solver +from scipy.sparse.linalg import spsolve TOL = 1e-10 +gen = np.random.default_rng(123) + class BasicTensorMeshTests(unittest.TestCase): def setUp(self): @@ -251,9 +253,9 @@ def test_serialization(self): self.assertTrue(np.all(self.mesh2.gridCC == mesh.gridCC)) -class TestTensorMeshCellNodes: +class TestTensorMeshProperties: """ - Test TensorMesh.cell_nodes + Test some of the properties in TensorMesh """ @pytest.fixture(params=[1, 2, 3], ids=["dims-1", "dims-2", "dims-3"]) @@ -261,16 +263,24 @@ def mesh(self, request): """Sample TensorMesh.""" if request.param == 1: h = [10] + origin = (-35.5,) elif request.param == 2: h = [10, 15] + origin = (-35.5, 105.3) else: h = [10, 15, 20] - return discretize.TensorMesh(h) + origin = (-35.5, 105.3, -27.3) + return discretize.TensorMesh(h, origin=origin) def test_cell_nodes(self, mesh): """Test TensorMesh.cell_nodes.""" expected_cell_nodes = np.array([cell.nodes for cell in mesh]) - np.testing.assert_allclose(mesh.cell_nodes, expected_cell_nodes) + np.testing.assert_equal(mesh.cell_nodes, expected_cell_nodes) + + def test_cell_bounds(self, mesh): + """Test TensorMesh.cell_bounds.""" + expected_cell_bounds = np.array([cell.bounds for cell in mesh]) + np.testing.assert_equal(mesh.cell_bounds, expected_cell_bounds) class TestPoissonEqn(discretize.tests.OrderTest): @@ -296,7 +306,7 @@ def getError(self): err = np.linalg.norm((sA - sN), np.inf) else: fA = fun(self.M.gridCC) - fN = Solver(D * G) * (sol(self.M.gridCC)) + fN = spsolve(D * G, sol(self.M.gridCC)) err = np.linalg.norm((fA - fN), np.inf) return err diff --git a/tests/base/test_tensor_innerproduct.py b/tests/base/test_tensor_innerproduct.py index 156403255..7f2ebd9db 100644 --- a/tests/base/test_tensor_innerproduct.py +++ b/tests/base/test_tensor_innerproduct.py @@ -4,8 +4,6 @@ from discretize import TensorMesh from discretize.utils import sdinv -np.random.seed(50) - class TestInnerProducts(discretize.tests.OrderTest): """Integrate an function over a unit cube domain @@ -750,7 +748,7 @@ class TestTensorSizeErrorRaises(unittest.TestCase): def setUp(self): self.mesh3D = TensorMesh([4, 4, 4]) - self.model = np.random.rand(self.mesh3D.nC) + self.model = np.ones(self.mesh3D.nC) def test_edge_inner_product_surface(self): self.assertRaises( diff --git a/tests/base/test_tensor_innerproduct_derivs.py b/tests/base/test_tensor_innerproduct_derivs.py index 0273192c6..af8a94f70 100644 --- a/tests/base/test_tensor_innerproduct_derivs.py +++ b/tests/base/test_tensor_innerproduct_derivs.py @@ -3,7 +3,7 @@ import discretize from discretize import TensorMesh -np.random.seed(50) +rng = np.random.default_rng(542) class TestInnerProductsDerivsTensor(unittest.TestCase): @@ -19,8 +19,8 @@ def doTestFace( mesh.number(balance=False) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nF) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nC * rep) + v = rng.random(mesh.nF) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nC * rep) def fun(sig): M = mesh.get_face_inner_product( @@ -42,7 +42,9 @@ def fun(sig): fast, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=452 + ) def doTestEdge( self, h, rep, fast, meshType, invert_model=False, invert_matrix=False @@ -56,8 +58,8 @@ def doTestEdge( mesh.number(balance=False) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nE) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nC * rep) + v = rng.random(mesh.nE) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nC * rep) def fun(sig): M = mesh.get_edge_inner_product( @@ -79,7 +81,9 @@ def fun(sig): fast, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=4567 + ) def test_FaceIP_1D_float(self): self.assertTrue(self.doTestFace([10], 0, False, "Tensor")) @@ -341,8 +345,8 @@ def doTestFace(self, h, rep, meshType, invert_model=False, invert_matrix=False): mesh.number(balance=False) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nF) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nF * rep) + v = rng.random(mesh.nF) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nF * rep) def fun(sig): M = mesh.get_face_inner_product_surface( @@ -360,7 +364,9 @@ def fun(sig): rep, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=421 + ) def doTestEdge(self, h, rep, meshType, invert_model=False, invert_matrix=False): if meshType == "Curv": @@ -372,8 +378,8 @@ def doTestEdge(self, h, rep, meshType, invert_model=False, invert_matrix=False): mesh.number(balance=False) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nE) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nF * rep) + v = rng.random(mesh.nE) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nF * rep) def fun(sig): M = mesh.get_edge_inner_product_surface( @@ -391,7 +397,9 @@ def fun(sig): rep, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=31 + ) def test_FaceIP_2D_float(self): self.assertTrue(self.doTestFace([10, 4], 0, "Tensor")) @@ -477,8 +485,8 @@ def doTestEdge(self, h, rep, meshType, invert_model=False, invert_matrix=False): mesh.number(balance=False) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nE) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nE * rep) + v = rng.random(mesh.nE) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nE * rep) def fun(sig): M = mesh.get_edge_inner_product_line( @@ -500,7 +508,9 @@ def fun(sig): rep, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=64 + ) def test_EdgeIP_2D_float(self): self.assertTrue(self.doTestEdge([10, 4], 0, "Tensor")) @@ -544,7 +554,7 @@ class TestTensorSizeErrorRaises(unittest.TestCase): def setUp(self): self.mesh3D = TensorMesh([4, 4, 4]) - self.model = np.random.rand(self.mesh3D.nC) + self.model = rng.random(self.mesh3D.nC) def test_edge_inner_product_surface_deriv(self): self.assertRaises( diff --git a/tests/base/test_tensor_omf.py b/tests/base/test_tensor_omf.py index 467cff20d..d4179d545 100644 --- a/tests/base/test_tensor_omf.py +++ b/tests/base/test_tensor_omf.py @@ -50,6 +50,7 @@ def test_to_omf(self): self.assertTrue(np.allclose(models[name], arr)) def test_from_omf(self): + rng = np.random.default_rng(52134) omf_element = omf.VolumeElement( name="vol_ir", geometry=omf.VolumeGridGeometry( @@ -65,7 +66,7 @@ def test_from_omf(self): omf.ScalarData( name="Random Data", location="cells", - array=np.random.rand(10, 15, 20).flatten(), + array=rng.random((10, 15, 20)).flatten(), ) ], ) diff --git a/tests/base/test_tests.py b/tests/base/test_tests.py index d2f279cf7..fbf789ca2 100644 --- a/tests/base/test_tests.py +++ b/tests/base/test_tests.py @@ -26,6 +26,7 @@ def test_defaults(self, capsys): mesh1.n_cells, mesh2.n_cells, assert_error=False, + random_seed=41, ) out2, _ = capsys.readouterr() assert out1 @@ -38,6 +39,7 @@ def test_defaults(self, capsys): lambda v: P.T * v, mesh1.n_cells, mesh2.n_cells, + random_seed=42, ) def test_different_shape(self): @@ -53,7 +55,13 @@ def adj(inp): out = np.expand_dims(inp, 1) return np.tile(out, nt) - assert_isadjoint(fwd, adj, shape_u=(4, nt), shape_v=(4,)) + assert_isadjoint( + fwd, + adj, + shape_u=(4, nt), + shape_v=(4,), + random_seed=42, + ) def test_complex_clinear(self): # The complex conjugate is self-adjoint, real-linear. @@ -65,6 +73,7 @@ def test_complex_clinear(self): complex_u=True, complex_v=True, clinear=False, + random_seed=112, ) @@ -73,20 +82,29 @@ def test_simplePass(self): def simplePass(x): return np.sin(x), sp.diags(np.cos(x)) - check_derivative(simplePass, np.random.randn(5), plotIt=False) + rng = np.random.default_rng(5322) + check_derivative( + simplePass, rng.standard_normal(5), plotIt=False, random_seed=42 + ) def test_simpleFunction(self): def simpleFunction(x): return np.sin(x), lambda xi: np.cos(x) * xi - check_derivative(simpleFunction, np.random.randn(5), plotIt=False) + rng = np.random.default_rng(5322) + check_derivative( + simpleFunction, rng.standard_normal(5), plotIt=False, random_seed=23 + ) def test_simpleFail(self): def simpleFail(x): return np.sin(x), -sp.diags(np.cos(x)) + rng = np.random.default_rng(5322) with pytest.raises(AssertionError): - check_derivative(simpleFail, np.random.randn(5), plotIt=False) + check_derivative( + simpleFail, rng.standard_normal(5), plotIt=False, random_seed=64 + ) @pytest.mark.parametrize("test_type", ["mean", "min", "last", "all", "mean_at_least"]) diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index d34303f13..7c950e058 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -22,6 +22,7 @@ mesh_builder_xyz, refine_tree_xyz, unpack_widths, + cross2d, ) import discretize @@ -123,7 +124,8 @@ def test_index_cube_3D(self): ) def test_invXXXBlockDiagonal(self): - a = [np.random.rand(5, 1) for i in range(4)] + rng = np.random.default_rng(78352) + a = [rng.random((5, 1)) for i in range(4)] B = inverse_2x2_block_diagonal(*a) @@ -137,7 +139,7 @@ def test_invXXXBlockDiagonal(self): Z2 = B * A - sp.identity(10) self.assertTrue(np.linalg.norm(Z2.todense().ravel(), 2) < TOL) - a = [np.random.rand(5, 1) for i in range(9)] + a = [rng.random((5, 1)) for i in range(9)] B = inverse_3x3_block_diagonal(*a) A = sp.vstack( @@ -153,10 +155,11 @@ def test_invXXXBlockDiagonal(self): self.assertTrue(np.linalg.norm(Z3.todense().ravel(), 2) < TOL) def test_inverse_property_tensor2D(self): + rng = np.random.default_rng(763) M = discretize.TensorMesh([6, 6]) - a1 = np.random.rand(M.nC) - a2 = np.random.rand(M.nC) - a3 = np.random.rand(M.nC) + a1 = rng.random(M.nC) + a2 = rng.random(M.nC) + a3 = rng.random(M.nC) prop1 = a1 prop2 = np.c_[a1, a2] prop3 = np.c_[a1, a2, a3] @@ -173,10 +176,11 @@ def test_inverse_property_tensor2D(self): self.assertTrue(np.linalg.norm(Z.todense().ravel(), 2) < TOL) def test_TensorType2D(self): + rng = np.random.default_rng(8546) M = discretize.TensorMesh([6, 6]) - a1 = np.random.rand(M.nC) - a2 = np.random.rand(M.nC) - a3 = np.random.rand(M.nC) + a1 = rng.random(M.nC) + a2 = rng.random(M.nC) + a3 = rng.random(M.nC) prop1 = a1 prop2 = np.c_[a1, a2] prop3 = np.c_[a1, a2, a3] @@ -188,13 +192,14 @@ def test_TensorType2D(self): self.assertTrue(TensorType(M, None) == -1) def test_TensorType3D(self): + rng = np.random.default_rng(78352) M = discretize.TensorMesh([6, 6, 7]) - a1 = np.random.rand(M.nC) - a2 = np.random.rand(M.nC) - a3 = np.random.rand(M.nC) - a4 = np.random.rand(M.nC) - a5 = np.random.rand(M.nC) - a6 = np.random.rand(M.nC) + a1 = rng.random(M.nC) + a2 = rng.random(M.nC) + a3 = rng.random(M.nC) + a4 = rng.random(M.nC) + a5 = rng.random(M.nC) + a6 = rng.random(M.nC) prop1 = a1 prop2 = np.c_[a1, a2, a3] prop3 = np.c_[a1, a2, a3, a4, a5, a6] @@ -206,13 +211,14 @@ def test_TensorType3D(self): self.assertTrue(TensorType(M, None) == -1) def test_inverse_property_tensor3D(self): + rng = np.random.default_rng(78352) M = discretize.TensorMesh([6, 6, 6]) - a1 = np.random.rand(M.nC) - a2 = np.random.rand(M.nC) - a3 = np.random.rand(M.nC) - a4 = np.random.rand(M.nC) - a5 = np.random.rand(M.nC) - a6 = np.random.rand(M.nC) + a1 = rng.random(M.nC) + a2 = rng.random(M.nC) + a3 = rng.random(M.nC) + a4 = rng.random(M.nC) + a5 = rng.random(M.nC) + a6 = rng.random(M.nC) prop1 = a1 prop2 = np.c_[a1, a2, a3] prop3 = np.c_[a1, a2, a3, a4, a5, a6] @@ -563,8 +569,8 @@ def test_active_from_xyz(self): mesh_tree, topo3D, grid_reference="N", method="nearest" ) - self.assertIn(indtopoCC.sum(), [6292, 6299]) - self.assertIn(indtopoN.sum(), [4632, 4639]) + self.assertIn(indtopoCC.sum(), [6285, 6292, 6299]) + self.assertIn(indtopoN.sum(), [4625, 4632, 4639]) # Test 3D CYL Mesh ncr = 10 # number of mesh cells in r @@ -602,5 +608,15 @@ def test_active_from_xyz(self): ) +def test_cross2d(): + x = np.linspace(3, 4, 20).reshape(10, 2) + y = np.linspace(1, 2, 20).reshape(10, 2) + + x_boost = np.c_[x, np.zeros(10)] + y_boost = np.c_[y, np.zeros(10)] + + np.testing.assert_allclose(np.cross(x_boost, y_boost)[:, -1], cross2d(x, y)) + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_view.py b/tests/base/test_view.py index bcab25849..64baf98df 100644 --- a/tests/base/test_view.py +++ b/tests/base/test_view.py @@ -6,8 +6,6 @@ import pytest -np.random.seed(16) - TOL = 1e-1 @@ -51,7 +49,7 @@ def test_incorrectAxesWarnings(self): def test_plot_image(self): with self.assertRaises(NotImplementedError): - self.mesh.plot_image(np.random.rand(self.mesh.nC)) + self.mesh.plot_image(np.empty(self.mesh.nC)) if __name__ == "__main__": diff --git a/tests/base/test_volume_avg.py b/tests/base/test_volume_avg.py index ed319bf9e..da4575e6e 100644 --- a/tests/base/test_volume_avg.py +++ b/tests/base/test_volume_avg.py @@ -1,444 +1,130 @@ import numpy as np -import unittest +import pytest import discretize from discretize.utils import volume_average -from numpy.testing import assert_array_equal, assert_allclose - - -class TestVolumeAverage(unittest.TestCase): - def test_tensor_to_tensor(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - h2 = np.random.rand(16) - h2 /= h2.sum() - - h1s = [] - h2s = [] - for i in range(3): - print(f"Tensor to Tensor {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - mesh1 = discretize.TensorMesh(h1s) - mesh2 = discretize.TensorMesh(h2s) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - vol1 = np.sum(mesh1.cell_volumes * in_put) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tree_to_tree(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - h2 = np.random.rand(16) - h2 /= h2.sum() - - h1s = [h1] - h2s = [h2] - insert_1 = [0.25] - insert_2 = [0.75] - for i in range(1, 3): - print(f"Tree to Tree {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - insert_1.append(0.25) - insert_2.append(0.75) - mesh1 = discretize.TreeMesh(h1s) - mesh1.insert_cells([insert_1], [4]) - mesh2 = discretize.TreeMesh(h2s) - mesh2.insert_cells([insert_2], [4]) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - vol1 = np.sum(mesh1.cell_volumes * in_put) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tree_to_tensor(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - h2 = np.random.rand(16) - h2 /= h2.sum() - - h1s = [h1] - h2s = [h2] - insert_1 = [0.25] - for i in range(1, 3): - print(f"Tree to Tensor {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - insert_1.append(0.25) - mesh1 = discretize.TreeMesh(h1s) - mesh1.insert_cells([insert_1], [4]) - mesh2 = discretize.TensorMesh(h2s) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - vol1 = np.sum(mesh1.cell_volumes * in_put) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tensor_to_tree(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - h2 = np.random.rand(16) - h2 /= h2.sum() - - h1s = [h1] - h2s = [h2] - insert_2 = [0.75] - for i in range(1, 3): - print(f"Tensor to Tree {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - insert_2.append(0.75) - mesh1 = discretize.TensorMesh(h1s) - mesh2 = discretize.TreeMesh(h2s) - mesh2.insert_cells([insert_2], [4]) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - vol1 = np.sum(mesh1.cell_volumes * in_put) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_errors(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - h2 = np.random.rand(16) - h2 /= h2.sum() - mesh1D = discretize.TensorMesh([h1]) - mesh2D = discretize.TensorMesh([h1, h1]) - mesh3D = discretize.TensorMesh([h1, h1, h1]) - - hr = np.r_[1, 1, 0.5] - hz = np.r_[2, 1] - meshCyl = discretize.CylindricalMesh([hr, 1, hz], np.r_[0.0, 0.0, 0.0]) - mesh2 = discretize.TreeMesh([h2, h2]) - mesh2.insert_cells([0.75, 0.75], [4]) - - with self.assertRaises(TypeError): - # Gives a wrong typed object to the function - volume_average(mesh1D, h1) - with self.assertRaises(NotImplementedError): - # Gives a wrong typed mesh - volume_average(meshCyl, mesh2) - with self.assertRaises(ValueError): - # Gives mismatching mesh dimensions - volume_average(mesh2D, mesh3D) - - model1 = np.random.randn(mesh2D.nC) - bad_model1 = np.random.randn(3) - bad_model2 = np.random.rand(1) - # gives input values with incorrect lengths - with self.assertRaises(ValueError): - volume_average(mesh2D, mesh2, bad_model1) - with self.assertRaises(ValueError): - volume_average(mesh2D, mesh2, model1, bad_model2) - - def test_tree_to_tree_same_base(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - - h1s = [h1] - insert_1 = [0.25] - insert_2 = [0.75] - for i in range(1, 3): - print(f"Tree to Tree {i+1}D: same base", end="") - h1s.append(h1) - insert_1.append(0.25) - insert_2.append(0.75) - mesh1 = discretize.TreeMesh(h1s) - mesh1.insert_cells([insert_1], [4]) - mesh2 = discretize.TreeMesh(h1s) - mesh2.insert_cells([insert_2], [4]) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - vol1 = np.sum(mesh1.cell_volumes * in_put) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tree_to_tensor_same_base(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - - h1s = [h1] - insert_1 = [0.25] - for i in range(1, 3): - print(f"Tree to Tensor {i+1}D same base: ", end="") - h1s.append(h1) - insert_1.append(0.25) - mesh1 = discretize.TreeMesh(h1s) - mesh1.insert_cells([insert_1], [4]) - mesh2 = discretize.TensorMesh(h1s) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - vol1 = np.sum(mesh1.cell_volumes * in_put) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tensor_to_tree_same_base(self): - h1 = np.random.rand(16) - h1 /= h1.sum() - - h1s = [h1] - insert_2 = [0.75] - for i in range(1, 3): - print(f"Tensor to Tree {i+1}D same base: ", end="") - h1s.append(h1) - insert_2.append(0.75) - mesh1 = discretize.TensorMesh(h1s) - mesh2 = discretize.TreeMesh(h1s) - mesh2.insert_cells([insert_2], [4]) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - vol1 = np.sum(mesh1.cell_volumes * in_put) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tensor_to_tensor_sub(self): - h1 = np.ones(32) - h2 = np.ones(16) - - h1s = [] - h2s = [] - for i in range(3): - print(f"Tensor to smaller Tensor {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - mesh1 = discretize.TensorMesh(h1s) - mesh2 = discretize.TensorMesh(h2s) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - # get cells in extent of smaller mesh - cells = mesh1.gridCC < [16] * (i + 1) - if i > 0: - cells = np.all(cells, axis=1) - - vol1 = np.sum(mesh1.cell_volumes[cells] * in_put[cells]) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tree_to_tree_sub(self): - h1 = np.ones(32) - h2 = np.ones(16) - - h1s = [h1] - h2s = [h2] - insert_1 = [12] - insert_2 = [4] - for i in range(1, 3): - print(f"Tree to smaller Tree {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - insert_1.append(12) - insert_2.append(4) - mesh1 = discretize.TreeMesh(h1s) - mesh1.insert_cells([insert_1], [4]) - mesh2 = discretize.TreeMesh(h2s) - mesh2.insert_cells([insert_2], [4]) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - # get cells in extent of smaller mesh - cells = mesh1.gridCC < [16] * (i + 1) - if i > 0: - cells = np.all(cells, axis=1) - - vol1 = np.sum(mesh1.cell_volumes[cells] * in_put[cells]) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tree_to_tensor_sub(self): - h1 = np.ones(32) - h2 = np.ones(16) - - h1s = [h1] - insert_1 = [12] - h2s = [h2] - for i in range(1, 3): - print(f"Tree to smaller Tensor {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - insert_1.append(12) - mesh1 = discretize.TreeMesh(h1s) - mesh1.insert_cells([insert_1], [4]) - mesh2 = discretize.TensorMesh(h2s) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - # get cells in extent of smaller mesh - cells = mesh1.gridCC < [16] * (i + 1) - if i > 0: - cells = np.all(cells, axis=1) - - vol1 = np.sum(mesh1.cell_volumes[cells] * in_put[cells]) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - def test_tensor_to_tree_sub(self): - h1 = np.ones(32) - h2 = np.ones(16) - - h1s = [h1] - h2s = [h2] - insert_2 = [4] - for i in range(1, 3): - print(f"Tensor to smaller Tree {i+1}D: ", end="") - h1s.append(h1) - h2s.append(h2) - insert_2.append(4) - mesh1 = discretize.TensorMesh(h1s) - mesh2 = discretize.TreeMesh(h2s) - mesh2.insert_cells([insert_2], [4]) - - in_put = np.random.rand(mesh1.nC) - out_put = np.empty(mesh2.nC) - # test the three ways of calling... - out1 = volume_average(mesh1, mesh2, in_put, out_put) - assert_array_equal(out1, out_put) - - out2 = volume_average(mesh1, mesh2, in_put) - assert_allclose(out1, out2) - - Av = volume_average(mesh1, mesh2) - out3 = Av @ in_put - assert_allclose(out1, out3) - - # get cells in extent of smaller mesh - cells = mesh1.gridCC < [16] * (i + 1) - if i > 0: - cells = np.all(cells, axis=1) - - vol1 = np.sum(mesh1.cell_volumes[cells] * in_put[cells]) - vol2 = np.sum(mesh2.cell_volumes * out3) - print(vol1, vol2) - self.assertAlmostEqual(vol1, vol2) - - -if __name__ == "__main__": - unittest.main() +from numpy.testing import assert_allclose + + +def generate_mesh(dim, mesh_type, tree_point=None, sub_mesh=False, seed=0): + if seed is None: + h = np.ones(16) + else: + rng = np.random.default_rng(seed) + h = rng.random(16) + h /= h.sum() + if sub_mesh: + h = h[:8] + if tree_point is not None: + tree_point = tree_point * 0.5 + hs = [h] * dim + mesh = mesh_type(hs) + if isinstance(mesh, discretize.TreeMesh): + mesh.insert_cells(tree_point, -1) + return mesh + + +@pytest.mark.parametrize("same_base", [True, False]) +@pytest.mark.parametrize("sub_mesh", [True, False]) +@pytest.mark.parametrize( + "dim, mesh1_type, mesh2_type", + [ + (1, discretize.TensorMesh, discretize.TensorMesh), + (2, discretize.TensorMesh, discretize.TensorMesh), + (3, discretize.TensorMesh, discretize.TensorMesh), + (2, discretize.TreeMesh, discretize.TensorMesh), + (3, discretize.TreeMesh, discretize.TensorMesh), + (2, discretize.TensorMesh, discretize.TreeMesh), + (3, discretize.TensorMesh, discretize.TreeMesh), + (2, discretize.TreeMesh, discretize.TreeMesh), + (3, discretize.TreeMesh, discretize.TreeMesh), + ], +) +def test_volume_average(dim, mesh1_type, mesh2_type, same_base, sub_mesh, seed=102): + if dim == 1: + if mesh1_type is discretize.TreeMesh or mesh2_type is discretize.TreeMesh: + pytest.skip("TreeMesh only in 2D or higher") + + p1 = p2 = None + if mesh1_type is discretize.TreeMesh: + p1 = np.asarray([0.25] * dim) + if mesh2_type is discretize.TreeMesh: + p2 = np.asarray([0.75] * dim) + + rng = np.random.default_rng(seed) + + if not sub_mesh: + seed1, seed2 = rng.integers(554, size=(2,)) + if same_base: + seed2 = seed1 + else: + seed1 = seed2 = None + + mesh1 = generate_mesh(dim, mesh1_type, tree_point=p1, seed=seed1) + mesh2 = generate_mesh(dim, mesh2_type, tree_point=p2, sub_mesh=sub_mesh, seed=seed2) + + model_in = rng.random(mesh1.nC) + model_out1 = np.empty(mesh2.nC) + + # test the three ways of calling... + + # providing an output array + out1 = volume_average(mesh1, mesh2, model_in, model_out1) + # assert_array_equal(out1, model_out1) + assert out1 is model_out1 + + # only providing input array + out2 = volume_average(mesh1, mesh2, model_in) + assert_allclose(out1, out2) + + # not providing either (which constructs a sparse matrix representing the operation) + Av = volume_average(mesh1, mesh2) + out3 = Av @ model_in + assert_allclose(out1, out3) + + # test for mass conserving properties: + if sub_mesh: + # get cells in extent of smaller mesh + cells = mesh1.cell_centers < 8 + if dim > 1: + cells = np.all(cells, axis=1) + + mass1 = np.sum(mesh1.cell_volumes[cells] * model_in[cells]) + else: + mass1 = np.sum(mesh1.cell_volumes * model_in) + mass2 = np.sum(mesh2.cell_volumes * out3) + assert_allclose(mass1, mass2) + + +def test_errors(): + h1 = np.ones(16) + np.arange(16) + h1 /= h1.sum() + h2 = np.ones(16) + 2 * np.arange(16) + h2 /= h2.sum() + mesh1D = discretize.TensorMesh([h1]) + mesh2D = discretize.TensorMesh([h1, h1]) + mesh3D = discretize.TensorMesh([h1, h1, h1]) + + hr = np.r_[1, 1, 0.5] + hz = np.r_[2, 1] + meshCyl = discretize.CylindricalMesh([hr, 1, hz], np.r_[0.0, 0.0, 0.0]) + mesh2 = discretize.TreeMesh([h2, h2]) + mesh2.insert_cells([0.75, 0.75], [4]) + + with pytest.raises(TypeError): + # Gives a wrong typed object to the function + volume_average(mesh1D, h1) + with pytest.raises(NotImplementedError): + # Gives a wrong typed mesh + volume_average(meshCyl, mesh2) + with pytest.raises(ValueError): + # Gives mismatching mesh dimensions + volume_average(mesh2D, mesh3D) + + model1 = np.random.randn(mesh2D.nC) + bad_model1 = np.array([-1, 2, 3]) + bad_model2 = np.array([1]) + # gives input values with incorrect lengths + with pytest.raises(ValueError): + volume_average(mesh2D, mesh2, bad_model1) + with pytest.raises(ValueError): + volume_average(mesh2D, mesh2, model1, bad_model2) diff --git a/tests/boundaries/test_boundary_integrals.py b/tests/boundaries/test_boundary_integrals.py index dbe224a76..be0a0e409 100644 --- a/tests/boundaries/test_boundary_integrals.py +++ b/tests/boundaries/test_boundary_integrals.py @@ -268,14 +268,14 @@ def getError(self): def test_orderWeakCellGradIntegral(self): self.name = "3D - weak cell gradient integral w/boundary" self.myTest = "cell_grad" - self.orderTest() + self.orderTest(random_seed=51235) def test_orderWeakEdgeDivIntegral(self): self.name = "3D - weak edge divergence integral w/boundary" self.myTest = "edge_div" - self.orderTest() + self.orderTest(random_seed=51123) def test_orderWeakFaceCurlIntegral(self): self.name = "3D - weak face curl integral w/boundary" self.myTest = "face_curl" - self.orderTest() + self.orderTest(random_seed=5522) diff --git a/tests/boundaries/test_boundary_maxwell.py b/tests/boundaries/test_boundary_maxwell.py index 1ffd35888..ea19ec063 100644 --- a/tests/boundaries/test_boundary_maxwell.py +++ b/tests/boundaries/test_boundary_maxwell.py @@ -1,7 +1,7 @@ import numpy as np import discretize from discretize import utils -from pymatsolver import Pardiso +from scipy.sparse.linalg import spsolve class TestFz2D_InhomogeneousDirichlet(discretize.tests.OrderTest): @@ -33,7 +33,7 @@ def getError(self): A = V @ C @ MeI @ C.T @ V + V rhs = V @ q_ana + V @ C @ MeI @ M_be @ ez_bc - ez_test = Pardiso(A.tocsr()) * rhs + ez_test = spsolve(A, rhs) if self._meshType == "rotateCurv": err = np.linalg.norm(mesh.cell_volumes * (ez_test - ez_ana)) else: @@ -52,7 +52,7 @@ class TestE3D_Inhomogeneous(discretize.tests.OrderTest): meshTypes = ["uniformTensorMesh", "uniformTree", "rotateCurv"] meshDimension = 3 expectedOrders = [2, 2, 1] - meshSizes = [8, 16, 32] + meshSizes = [4, 8, 16] def getError(self): # Test function @@ -99,7 +99,7 @@ def q_fun(x): A = C.T @ Mf @ C + Me rhs = Me @ q_ana + M_be * h_bc - e_test = Pardiso(A.tocsr(), is_symmetric=True, is_positive_definite=True) * rhs + e_test = spsolve(A, rhs) diff = e_test - e_ana if "Face" in self.myTest: diff --git a/tests/boundaries/test_boundary_poisson.py b/tests/boundaries/test_boundary_poisson.py index 2c5e85a07..3c8a0eaa7 100644 --- a/tests/boundaries/test_boundary_poisson.py +++ b/tests/boundaries/test_boundary_poisson.py @@ -4,7 +4,7 @@ import unittest import discretize from discretize import utils -from pymatsolver import Solver, Pardiso +from scipy.sparse.linalg import spsolve class TestCC1D_InhomogeneousDirichlet(discretize.tests.OrderTest): @@ -39,7 +39,7 @@ def getError(self): A = V @ D @ MfI @ G rhs = V @ q_ana - V @ D @ MfI @ M_bf @ phi_bc - phi_test = Solver(A) * rhs + phi_test = spsolve(A, rhs) err = np.linalg.norm((phi_test - phi_ana)) / np.sqrt(mesh.n_cells) return err @@ -79,7 +79,7 @@ def getError(self): A = V @ D @ MfI @ G rhs = V @ q_ana - V @ D @ MfI @ M_bf @ phi_bc - phi_test = Solver(A) * rhs + phi_test = spsolve(A, rhs) if self._meshType == "rotateCurv": err = np.linalg.norm(mesh.cell_volumes * (phi_test - phi_ana)) else: @@ -261,10 +261,10 @@ def getError(self): rhs = V @ q_ana - V @ D @ MfI @ b_bc if self.myTest == "xc": - xc = Solver(A) * rhs + xc = spsolve(A, rhs) err = np.linalg.norm(xc - xc_ana) / np.sqrt(mesh.n_cells) elif self.myTest == "xcJ": - xc = Solver(A) * rhs + xc = spsolve(A, rhs) j = MfI @ ((-G + B_bc) @ xc + b_bc) err = np.linalg.norm(j - j_ana, np.inf) return err @@ -332,7 +332,7 @@ def getError(self): A = V @ D @ MfI @ (-G + B_bc) rhs = V @ q_ana - V @ D @ MfI @ b_bc - phi_test = Solver(A) * rhs + phi_test = spsolve(A, rhs) if self._meshType == "rotateCurv": err = np.linalg.norm(mesh.cell_volumes * (phi_test - phi_ana)) @@ -352,7 +352,7 @@ class TestCC3D_InhomogeneousMixed(discretize.tests.OrderTest): meshTypes = ["uniformTensorMesh", "uniformTree", "rotateCurv"] meshDimension = 3 expectedOrders = [2, 2, 2] - meshSizes = [2, 4, 8, 16, 32] + meshSizes = [2, 4, 8, 16] def getError(self): # Test function @@ -421,7 +421,7 @@ def getError(self): A = V @ D @ MfI @ (-G + B_bc) rhs = V @ q_ana - V @ D @ MfI @ b_bc - phi_test = Pardiso(A.tocsr()) * rhs + phi_test = spsolve(A, rhs) if self._meshType == "rotateCurv": err = np.linalg.norm(mesh.cell_volumes * (phi_test - phi_ana)) @@ -488,7 +488,7 @@ def getError(self): rhs = P_f.T @ rhs - (P_f.T @ A @ P_b) @ phi_ana[[0]] A = P_f.T @ A @ P_f - phi_test = Solver(A) * rhs + phi_test = spsolve(A, rhs) if self.boundary_type == "Nuemann": phi_test = P_f @ phi_test + P_b @ phi_ana[[0]] @@ -590,7 +590,7 @@ def getError(self): rhs = P_f.T @ rhs - (P_f.T @ A @ P_b) @ phi_ana[[0]] A = P_f.T @ A @ P_f - phi_test = Solver(A) * rhs + phi_test = spsolve(A, rhs) if self.boundary_type == "Nuemann": phi_test = P_f @ phi_test + P_b @ phi_ana[[0]] @@ -620,7 +620,7 @@ class TestN3D_boundaries(discretize.tests.OrderTest): meshDimension = 3 expectedOrders = 2 tolerance = 0.6 - meshSizes = [2, 4, 8, 16, 32] + meshSizes = [2, 4, 8, 16] # meshSizes = [4] def getError(self): @@ -715,7 +715,7 @@ def getError(self): rhs = P_f.T @ rhs - (P_f.T @ A @ P_b) @ phi_ana[[0]] A = P_f.T @ A @ P_f - phi_test = Pardiso(A.tocsr()) * rhs + phi_test = spsolve(A, rhs) if self.boundary_type == "Nuemann": phi_test = P_f @ phi_test + P_b @ phi_ana[[0]] diff --git a/tests/boundaries/test_errors.py b/tests/boundaries/test_errors.py index b8475acda..41430b0a1 100644 --- a/tests/boundaries/test_errors.py +++ b/tests/boundaries/test_errors.py @@ -3,6 +3,9 @@ import discretize +rng = np.random.default_rng(53679) + + class RobinOperatorTest(unittest.TestCase): def setUp(self): self.mesh = discretize.TensorMesh([18, 20, 32]) @@ -28,7 +31,7 @@ def testCellGradBroadcasting(self): np.testing.assert_equal(bt, b1) self.assertEqual((B1 - B_t).nnz, 0) - gamma = np.random.rand(n_boundary_faces, 2) + gamma = rng.random((n_boundary_faces, 2)) B1, b1 = mesh.cell_gradient_weak_form_robin( alpha=0.5, beta=1.5, gamma=gamma[:, 0] ) @@ -78,7 +81,7 @@ def testEdgeDivBroadcasting(self): np.testing.assert_allclose(bt, b1) np.testing.assert_allclose(B1.data, B_t.data) - gamma = np.random.rand(n_boundary_faces, 2) + gamma = rng.random((n_boundary_faces, 2)) B1, b1 = mesh.edge_divergence_weak_form_robin( alpha=0.5, beta=1.5, gamma=gamma[:, 0] ) @@ -90,7 +93,7 @@ def testEdgeDivBroadcasting(self): np.testing.assert_allclose(B1.data, B3.data) np.testing.assert_allclose(np.c_[b1, b2], b3) - gamma = np.random.rand(n_boundary_nodes, 2) + gamma = rng.random((n_boundary_nodes, 2)) B1, b1 = mesh.edge_divergence_weak_form_robin( alpha=0.5, beta=1.5, gamma=gamma[:, 0] ) diff --git a/tests/boundaries/test_tensor_boundary.py b/tests/boundaries/test_tensor_boundary.py index c32a55bac..94be4aa0a 100644 --- a/tests/boundaries/test_tensor_boundary.py +++ b/tests/boundaries/test_tensor_boundary.py @@ -1,7 +1,7 @@ import numpy as np import unittest import discretize -from pymatsolver import Solver +from scipy.sparse.linalg import spsolve MESHTYPES = ["uniformTensorMesh"] @@ -225,8 +225,7 @@ def q_fun(x): if self.myTest == "xc": # TODO: fix the null space - Ainv = Solver(A) - xc = Ainv * rhs + xc = spsolve(A, rhs) err = np.linalg.norm((xc - xc_ana), np.inf) else: NotImplementedError @@ -320,8 +319,7 @@ def gamma_fun(alpha, beta, phi, phi_deriv): A = Div * MfrhoI * G if self.myTest == "xc": - Ainv = Solver(A) - xc = Ainv * rhs + xc = spsolve(A, rhs) err = np.linalg.norm((xc - xc_ana), np.inf) else: NotImplementedError @@ -452,8 +450,7 @@ def gamma_fun(alpha, beta, phi, phi_deriv): if self.myTest == "xc": # TODO: fix the null space - Ainv = Solver(A) - xc = Ainv * rhs + xc = spsolve(A, rhs) err = np.linalg.norm((xc - xc_ana), np.inf) else: NotImplementedError diff --git a/tests/boundaries/test_tensor_boundary_poisson.py b/tests/boundaries/test_tensor_boundary_poisson.py index df3763117..5f5b191d6 100644 --- a/tests/boundaries/test_tensor_boundary_poisson.py +++ b/tests/boundaries/test_tensor_boundary_poisson.py @@ -3,7 +3,7 @@ import unittest import discretize from discretize import utils -from pymatsolver import Solver, SolverCG +from scipy.sparse.linalg import spsolve MESHTYPES = ["uniformTensorMesh"] @@ -55,13 +55,12 @@ def getError(self): err = np.linalg.norm((q - q_ana), np.inf) elif self.myTest == "xc": # TODO: fix the null space - solver = SolverCG(A, maxiter=1000) - xc = solver * rhs + xc = spsolve(A, rhs) print("ACCURACY", np.linalg.norm(utils.mkvc(A * xc) - rhs)) err = np.linalg.norm((xc - xc_ana), np.inf) elif self.myTest == "xcJ": # TODO: fix the null space - xc = Solver(A) * rhs + xc = spsolve(A, rhs) print(np.linalg.norm(utils.mkvc(A * xc) - rhs)) j = McI * (G * xc + P * phi_bc) err = np.linalg.norm((j - j_ana), np.inf) @@ -141,10 +140,10 @@ def getError(self): elif self.myTest == "q": err = np.linalg.norm((q - q_ana), np.inf) elif self.myTest == "xc": - xc = Solver(A) * (rhs) + xc = spsolve(A, rhs) err = np.linalg.norm((xc - xc_ana), np.inf) elif self.myTest == "xcJ": - xc = Solver(A) * (rhs) + xc = spsolve(A, rhs) j = McI * (G * xc + P * bc) err = np.linalg.norm((j - j_ana), np.inf) diff --git a/tests/cyl/test_cyl.py b/tests/cyl/test_cyl.py index c3bdd22af..e33d431b6 100644 --- a/tests/cyl/test_cyl.py +++ b/tests/cyl/test_cyl.py @@ -5,7 +5,7 @@ import discretize from discretize import tests, utils -np.random.seed(13) +rng = np.random.default_rng(87564123) class TestCylSymmetricMesh(unittest.TestCase): @@ -161,8 +161,8 @@ def test_getInterpMatCartMesh_Faces(self): fycc = Mr.aveFy2CC * Mr.reshape(frect, "F", "Fy") fzcc = Mr.reshape(frect, "F", "Fz") - indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5]) - indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5]) + indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5])[0] + indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5])[0] TOL = 1e-2 assert np.abs(float(fxcc[indX]) - 1) < TOL @@ -194,8 +194,8 @@ def test_getInterpMatCartMesh_Faces2Edges(self): eycc = Mr.aveEy2CC * Mr.reshape(ecart, "E", "Ey") ezcc = Mr.reshape(ecart, "E", "Ez") - indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5]) - indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5]) + indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5])[0] + indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5])[0] TOL = 1e-2 assert np.abs(float(excc[indX]) - 1) < TOL @@ -225,8 +225,8 @@ def test_getInterpMatCartMesh_Edges(self): eycc = Mr.aveEy2CC * Mr.reshape(ecart, "E", "Ey") ezcc = Mr.aveEz2CC * Mr.reshape(ecart, "E", "Ez") - indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5]) - indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5]) + indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5])[0] + indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5])[0] TOL = 1e-2 assert np.abs(float(excc[indX]) - 0) < TOL @@ -256,8 +256,8 @@ def test_getInterpMatCartMesh_Edges2Faces(self): eycc = Mr.aveFy2CC * Mr.reshape(frect, "F", "Fy") ezcc = Mr.reshape(frect, "F", "Fz") - indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5]) - indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5]) + indX = utils.closest_points_index(Mr, [0.45, -0.2, 0.5])[0] + indY = utils.closest_points_index(Mr, [-0.2, 0.45, 0.5])[0] TOL = 1e-2 assert np.abs(float(excc[indX]) - 0) < TOL @@ -388,8 +388,8 @@ class TestCellGrad2D_Dirichlet(unittest.TestCase): # self.orderTest() def setUp(self): - hx = np.random.rand(10) - hz = np.random.rand(10) + hx = rng.random(10) + hz = rng.random(10) self.mesh = discretize.CylindricalMesh([hx, 1, hz]) def test_NotImplementedError(self): @@ -399,8 +399,8 @@ def test_NotImplementedError(self): class TestAveragingSimple(unittest.TestCase): def setUp(self): - hx = np.random.rand(10) - hz = np.random.rand(10) + hx = rng.random(10) + hz = rng.random(10) self.mesh = discretize.CylindricalMesh([hx, 1, hz]) def test_simpleEdges(self): diff --git a/tests/cyl/test_cyl3D.py b/tests/cyl/test_cyl3D.py index 1c98b60ce..4a7110046 100644 --- a/tests/cyl/test_cyl3D.py +++ b/tests/cyl/test_cyl3D.py @@ -4,8 +4,6 @@ import discretize from discretize import utils -np.random.seed(16) - TOL = 1e-1 diff --git a/tests/cyl/test_cylOperators.py b/tests/cyl/test_cylOperators.py index 1dca64497..4486fca19 100644 --- a/tests/cyl/test_cylOperators.py +++ b/tests/cyl/test_cylOperators.py @@ -6,15 +6,13 @@ import discretize from discretize import tests -np.random.seed(16) - TOL = 1e-1 # ----------------------------- Test Operators ------------------------------ # -MESHTYPES = ["uniformCylMesh", "randomCylMesh"] +MESHTYPES = ["uniformCylMesh"] call2 = lambda fun, xyz: fun(xyz[:, 0], xyz[:, 2]) call3 = lambda fun, xyz: fun(xyz[:, 0], xyz[:, 1], xyz[:, 2]) cyl_row2 = lambda g, xfun, yfun: np.c_[call2(xfun, g), call2(yfun, g)] diff --git a/tests/cyl/test_cyl_innerproducts.py b/tests/cyl/test_cyl_innerproducts.py index b9752bfcd..875b54140 100644 --- a/tests/cyl/test_cyl_innerproducts.py +++ b/tests/cyl/test_cyl_innerproducts.py @@ -9,7 +9,7 @@ TOL = 1e-1 TOLD = 0.7 # tolerance on deriv checks -np.random.seed(99) +rng = np.random.default_rng(99) class FaceInnerProductFctsIsotropic(object): @@ -465,8 +465,8 @@ class TestCylInnerProducts_Deriv(unittest.TestCase): def setUp(self): n = 2 self.mesh = discretize.CylindricalMesh([n, 1, n]) - self.face_vec = np.random.rand(self.mesh.nF) - self.edge_vec = np.random.rand(self.mesh.nE) + self.face_vec = rng.random(self.mesh.nF) + self.edge_vec = rng.random(self.mesh.nE) # make up a smooth function self.x0 = 2 * self.mesh.gridCC[:, 0] ** 2 + self.mesh.gridCC[:, 2] ** 4 @@ -478,7 +478,9 @@ def fun(x): print("Testing FaceInnerProduct Isotropic") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=532 + ) ) def test_FaceInnerProductIsotropicDerivInvProp(self): @@ -491,7 +493,9 @@ def fun(x): print("Testing FaceInnerProduct Isotropic InvProp") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=75 + ) ) def test_FaceInnerProductIsotropicDerivInvMat(self): @@ -504,7 +508,9 @@ def fun(x): print("Testing FaceInnerProduct Isotropic InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=1 + ) ) def test_FaceInnerProductIsotropicDerivInvPropInvMat(self): @@ -519,7 +525,9 @@ def fun(x): print("Testing FaceInnerProduct Isotropic InvProp InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=74 + ) ) def test_EdgeInnerProductIsotropicDeriv(self): @@ -530,7 +538,9 @@ def fun(x): print("Testing EdgeInnerProduct Isotropic") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=345 + ) ) def test_EdgeInnerProductIsotropicDerivInvProp(self): @@ -543,7 +553,9 @@ def fun(x): print("Testing EdgeInnerProduct Isotropic InvProp") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=643 + ) ) def test_EdgeInnerProductIsotropicDerivInvMat(self): @@ -556,7 +568,9 @@ def fun(x): print("Testing EdgeInnerProduct Isotropic InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=363 + ) ) def test_EdgeInnerProductIsotropicDerivInvPropInvMat(self): @@ -571,7 +585,9 @@ def fun(x): print("Testing EdgeInnerProduct Isotropic InvProp InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=773 + ) ) @@ -579,8 +595,8 @@ class TestCylInnerProductsAnisotropic_Deriv(unittest.TestCase): def setUp(self): n = 60 self.mesh = discretize.CylindricalMesh([n, 1, n]) - self.face_vec = np.random.rand(self.mesh.nF) - self.edge_vec = np.random.rand(self.mesh.nE) + self.face_vec = rng.random(self.mesh.nF) + self.edge_vec = rng.random(self.mesh.nE) # make up a smooth function self.x0 = np.array( [2 * self.mesh.gridCC[:, 0] ** 2 + self.mesh.gridCC[:, 2] ** 4] @@ -603,7 +619,9 @@ def fun(x): print("Testing FaceInnerProduct Anisotropic") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=2436 + ) ) def test_FaceInnerProductAnisotropicDerivInvProp(self): @@ -621,7 +639,9 @@ def fun(x): print("Testing FaceInnerProduct Anisotropic InvProp") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=634 + ) ) def test_FaceInnerProductAnisotropicDerivInvMat(self): @@ -639,7 +659,9 @@ def fun(x): print("Testing FaceInnerProduct Anisotropic InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=222 + ) ) def test_FaceInnerProductAnisotropicDerivInvPropInvMat(self): @@ -661,7 +683,9 @@ def fun(x): print("Testing FaceInnerProduct Anisotropic InvProp InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=654 + ) ) def test_EdgeInnerProductAnisotropicDeriv(self): @@ -679,7 +703,9 @@ def fun(x): print("Testing EdgeInnerProduct Anisotropic") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=7754 + ) ) def test_EdgeInnerProductAnisotropicDerivInvProp(self): @@ -697,7 +723,9 @@ def fun(x): print("Testing EdgeInnerProduct Anisotropic InvProp") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=1164 + ) ) def test_EdgeInnerProductAnisotropicDerivInvMat(self): @@ -715,7 +743,9 @@ def fun(x): print("Testing EdgeInnerProduct Anisotropic InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=643 + ) ) def test_EdgeInnerProductAnisotropicDerivInvPropInvMat(self): @@ -737,7 +767,9 @@ def fun(x): print("Testing EdgeInnerProduct Anisotropic InvProp InvMat") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=8654 + ) ) @@ -745,8 +777,8 @@ class TestCylInnerProductsFaceProperties_Deriv(unittest.TestCase): def setUp(self): n = 2 self.mesh = discretize.CylindricalMesh([n, 1, n]) - self.face_vec = np.random.rand(self.mesh.nF) - self.edge_vec = np.random.rand(self.mesh.nE) + self.face_vec = rng.random(self.mesh.nF) + self.edge_vec = rng.random(self.mesh.nE) # make up a smooth function self.x0 = np.r_[ 2 * self.mesh.gridFx[:, 0] ** 2 + self.mesh.gridFx[:, 2] ** 4, @@ -761,7 +793,9 @@ def fun(x): print("Testing FaceInnerProduct Isotropic (Face Properties)") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=234 + ) ) def test_FaceInnerProductIsotropicDerivInvProp(self): @@ -774,7 +808,9 @@ def fun(x): print("Testing FaceInnerProduct Isotropic InvProp (Face Properties)") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=7543 + ) ) def test_FaceInnerProductIsotropicDerivInvMat(self): @@ -787,7 +823,9 @@ def fun(x): print("Testing FaceInnerProduct Isotropic InvMat (Face Properties)") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=2745725 + ) ) def test_EdgeInnerProductIsotropicDeriv(self): @@ -798,7 +836,9 @@ def fun(x): print("Testing EdgeInnerProduct Isotropic (Face Properties)") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=6654 + ) ) def test_EdgeInnerProductIsotropicDerivInvProp(self): @@ -811,7 +851,9 @@ def fun(x): print("Testing EdgeInnerProduct Isotropic InvProp (Face Properties)") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=4564 + ) ) def test_EdgeInnerProductIsotropicDerivInvMat(self): @@ -824,5 +866,7 @@ def fun(x): print("Testing EdgeInnerProduct Isotropic InvMat (Face Properties)") return self.assertTrue( - tests.check_derivative(fun, self.x0, num=7, tolerance=TOLD, plotIt=False) + tests.check_derivative( + fun, self.x0, num=7, tolerance=TOLD, plotIt=False, random_seed=2355 + ) ) diff --git a/tests/cyl/test_cyl_operators.py b/tests/cyl/test_cyl_operators.py index e7e78c807..fa43d6b33 100644 --- a/tests/cyl/test_cyl_operators.py +++ b/tests/cyl/test_cyl_operators.py @@ -13,6 +13,8 @@ variable_names=list("RTZ"), ) +rng = np.random.default_rng(563279) + def lambdify_vector(variabls, u_vecs, func): funcs = [sp.lambdify(variabls, func.coeff(u_hat), "numpy") for u_hat in u_vecs] @@ -346,7 +348,7 @@ def get_error(n_cells): def test_mimetic_div_curl(mesh_type): mesh, _ = setup_mesh(mesh_type, 10) - v = np.random.rand(mesh.n_edges) + v = rng.random(mesh.n_edges) divcurlv = mesh.face_divergence @ (mesh.edge_curl @ v) np.testing.assert_allclose(divcurlv, 0, atol=1e-11) @@ -355,7 +357,7 @@ def test_mimetic_div_curl(mesh_type): def test_mimetic_curl_grad(mesh_type): mesh, _ = setup_mesh(mesh_type, 10) - v = np.random.rand(mesh.n_nodes) + v = rng.random(mesh.n_nodes) divcurlv = mesh.edge_curl @ (mesh.nodal_gradient @ v) np.testing.assert_allclose(divcurlv, 0, atol=1e-11) diff --git a/tests/simplex/test_inner_products.py b/tests/simplex/test_inner_products.py index 88d454f8a..a5cae4ce6 100644 --- a/tests/simplex/test_inner_products.py +++ b/tests/simplex/test_inner_products.py @@ -4,6 +4,8 @@ import scipy.sparse as sp from discretize.utils import example_simplex_mesh +rng = np.random.default_rng(4421) + def u(*args): if len(args) == 1: @@ -332,8 +334,8 @@ class TestInnerProductsDerivs(unittest.TestCase): def doTestFace(self, h, rep): nodes, simplices = example_simplex_mesh(h) mesh = discretize.SimplexMesh(nodes, simplices) - v = np.random.rand(mesh.n_faces) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nC * rep) + v = rng.random(mesh.n_faces) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nC * rep) def fun(sig): M = mesh.get_face_inner_product(sig) @@ -341,13 +343,15 @@ def fun(sig): return M * v, Md(v) print("Face", rep) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=5352 + ) def doTestEdge(self, h, rep): nodes, simplices = example_simplex_mesh(h) mesh = discretize.SimplexMesh(nodes, simplices) - v = np.random.rand(mesh.n_edges) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nC * rep) + v = rng.random(mesh.n_edges) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nC * rep) def fun(sig): M = mesh.get_edge_inner_product(sig) @@ -355,7 +359,9 @@ def fun(sig): return M * v, Md(v) print("Edge", rep) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=532 + ) def test_FaceIP_2D_float(self): self.assertTrue(self.doTestFace([10, 4], 0)) @@ -544,7 +550,7 @@ def setUp(self): def test_bad_model_size(self): mesh = self.mesh - bad_model = np.random.rand(mesh.n_cells, 5) + bad_model = rng.random((mesh.n_cells, 5)) with self.assertRaises(ValueError): mesh.get_face_inner_product(bad_model) with self.assertRaises(ValueError): @@ -552,7 +558,7 @@ def test_bad_model_size(self): def test_cant_invert(self): mesh = self.mesh - good_model = np.random.rand(mesh.n_cells) + good_model = rng.random(mesh.n_cells) with self.assertRaises(NotImplementedError): mesh.get_face_inner_product(good_model, invert_matrix=True) with self.assertRaises(NotImplementedError): diff --git a/tests/simplex/test_operators.py b/tests/simplex/test_operators.py index 2e3916b31..f0a79fd97 100644 --- a/tests/simplex/test_operators.py +++ b/tests/simplex/test_operators.py @@ -1,5 +1,8 @@ import numpy as np +import pytest + import discretize +from discretize import SimplexMesh from discretize.utils import example_simplex_mesh @@ -179,3 +182,16 @@ def test_grad_order(self): self.name = "SimplexMesh grad order test" self._test_type = "Grad" self.orderTest() + + +@pytest.mark.parametrize("i_type", ["E", "F"]) +def test_simplex_projection_caching(i_type): + n = 5 + mesh = SimplexMesh(*example_simplex_mesh((n, n))) + P1 = mesh._SimplexMesh__get_inner_product_projection_matrices( + i_type, with_volume=False, return_pointers=False + ) + P2 = mesh._SimplexMesh__get_inner_product_projection_matrices( + i_type, with_volume=True, return_pointers=False + ) + assert P1 is not P2 diff --git a/tests/simplex/test_utils.py b/tests/simplex/test_utils.py index 87138c1b9..e0c4fc1a4 100644 --- a/tests/simplex/test_utils.py +++ b/tests/simplex/test_utils.py @@ -13,6 +13,8 @@ except ImportError: has_vtk = False +rng = np.random.default_rng(87916253) + class SimplexTests(unittest.TestCase): def test_init_errors(self): @@ -42,7 +44,7 @@ def test_init_errors(self): with self.assertRaises(ValueError): # pass bad dimensionality - discretize.SimplexMesh(np.random.rand(10, 4), simplices[:, :-1]) + discretize.SimplexMesh(np.ones((10, 4)), simplices[:, :-1]) def test_find_containing(self): n = 4 @@ -81,11 +83,11 @@ def test_image_plotting(self): points, simplices = discretize.utils.example_simplex_mesh((n, n)) mesh = discretize.SimplexMesh(points, simplices) - cc_dat = np.random.rand(mesh.n_cells) - n_dat = np.random.rand(mesh.n_nodes) - f_dat = np.random.rand(mesh.n_faces) - e_dat = np.random.rand(mesh.n_edges) - ccv_dat = np.random.rand(mesh.n_cells, 2) + cc_dat = rng.random(mesh.n_cells) + n_dat = rng.random(mesh.n_nodes) + f_dat = rng.random(mesh.n_faces) + e_dat = rng.random(mesh.n_edges) + ccv_dat = rng.random((mesh.n_cells, 2)) mesh.plot_image(cc_dat) mesh.plot_image(ccv_dat, v_type="CCv", view="vec") @@ -106,7 +108,7 @@ def test_image_plotting(self): points, simplices = discretize.utils.example_simplex_mesh((n, n, n)) mesh = discretize.SimplexMesh(points, simplices) - cc_dat = np.random.rand(mesh.n_cells) + cc_dat = rng.random(mesh.n_cells) mesh.plot_image(cc_dat) plt.close("all") @@ -128,7 +130,7 @@ def test_2D_vtk(self): n = 5 points, simplices = discretize.utils.example_simplex_mesh((n, n)) mesh = discretize.SimplexMesh(points, simplices) - cc_dat = np.random.rand(mesh.n_cells) + cc_dat = rng.random(mesh.n_cells) vtk_obj = mesh.to_vtk(models={"info": cc_dat}) @@ -149,7 +151,7 @@ def test_3D_vtk(self): n = 5 points, simplices = discretize.utils.example_simplex_mesh((n, n, n)) mesh = discretize.SimplexMesh(points, simplices) - cc_dat = np.random.rand(mesh.n_cells) + cc_dat = rng.random(mesh.n_cells) vtk_obj = mesh.to_vtk(models={"info": cc_dat}) diff --git a/tests/tree/test_intersections.py b/tests/tree/test_intersections.py new file mode 100644 index 000000000..701830faf --- /dev/null +++ b/tests/tree/test_intersections.py @@ -0,0 +1,223 @@ +import discretize +import numpy as np +import pytest +from discretize.tests import assert_cell_intersects_geometric + +pytestmark = pytest.mark.parametrize("dim", [2, 3]) + + +def test_point_locator(dim): + point = [0.44] * dim + mesh = discretize.TreeMesh([32] * dim) + + mesh.insert_cells(point, -1) + + ind = mesh.get_containing_cells(point) + cell = mesh[ind] + assert_cell_intersects_geometric(cell, point) + + +def test_ball_locator(dim): + center = [0.44] * dim + radius = 0.12 + + mesh = discretize.TreeMesh([32] * dim) + mesh.refine_ball(center, radius, -1) + + cells = mesh.get_cells_in_ball(center, radius) + + r2 = radius * radius + + def ball_intersects(cell): + a = cell.origin + b = a + cell.h + dr = np.maximum(a, np.minimum(center, b)) - center + r2_test = np.sum(dr * dr) + return r2_test < r2 + + # ensure it found all of the cells by using a brute force search + test = [] + for cell in mesh: + if ball_intersects(cell): + test.append(cell.index) + np.testing.assert_array_equal(test, cells) + + +def test_line_locator(dim): + segment = np.array([[0.12, 0.33, 0.19], [0.32, 0.93, 0.68]])[:, :dim] + + mesh = discretize.TreeMesh([32] * dim) + mesh.refine_line(segment, -1) + + cell_inds = mesh.get_cells_on_line(segment) + + # ensure it found all of the cells by using a brute force search + test = [] + for cell in mesh: + try: + assert_cell_intersects_geometric(cell, segment, edges=[0, 1]) + test.append(cell.index) + except AssertionError: + pass + np.testing.assert_array_equal(test, cell_inds) + + +def test_box_locator(dim): + xmin = [0.2] * dim + xmax = [0.4] * dim + points = np.stack([xmin, xmax]) + + mesh = discretize.TreeMesh([32] * dim) + mesh.refine_box(xmin, xmax, -1) + + cell_inds = mesh.get_cells_in_aabb(xmin, xmax) + + # ensure it found all of the cells by using a brute force search + test = [] + for cell in mesh: + try: + assert_cell_intersects_geometric(cell, points) + test.append(cell.index) + except AssertionError: + pass + np.testing.assert_array_equal(test, cell_inds) + + +def test_plane_locator(dim): + if dim == 2: + p0 = [2, 2] + normal = [-1, 1] + p1 = [-2, -2] + points = np.stack([p0, p1]) + edges = [0, 1] + faces = None + elif dim == 3: + p0 = [20, 20, 20] + normal = [-1, -1, 2] + # define 4 corner points (including p0) of a plane to create triangles + # to verify the refine functionallity + p1 = [20, -20, 0] + p2 = [-20, 20, 0] + p3 = [-20, -20, -20] + points = np.stack([p0, p1, p2, p3]) + edges = [[0, 1], [0, 2]] + faces = [[0, 1, 2]] + + mesh = discretize.TreeMesh([16] * dim) + mesh.refine_plane(p0, normal, -1) + + cell_inds = mesh.get_cells_on_plane(p0, normal) + + # ensure it found all of the cells by using a brute force search + test = [] + for cell in mesh: + try: + assert_cell_intersects_geometric(cell, points, edges=edges, faces=faces) + test.append(cell.index) + except AssertionError: + pass + np.testing.assert_array_equal(test, cell_inds) + + +def test_triangle_locator(dim): + triangle = np.array([[0.14, 0.31, 0.23], [0.32, 0.96, 0.41], [0.23, 0.87, 0.72]])[ + :, :dim + ] + edges = [[0, 1], [0, 2], [1, 2]] + faces = [0, 1, 2] + + mesh = discretize.TreeMesh([32] * dim) + mesh.refine_triangle(triangle, -1) + + cell_inds = mesh.get_cells_in_triangle(triangle) + + # test it throws an error without giving enough points to triangle. + with pytest.raises(ValueError): + mesh.get_cells_in_triangle(triangle[:-1]) + + # ensure it found all of the cells by using a brute force search + test = [] + for cell in mesh: + try: + assert_cell_intersects_geometric(cell, triangle, edges=edges, faces=faces) + test.append(cell.index) + except AssertionError: + pass + np.testing.assert_array_equal(test, cell_inds) + + +def test_vert_tri_prism_locator(dim): + xyz = np.array( + [ + [0.41, 0.21, 0.11], + [0.21, 0.61, 0.22], + [0.71, 0.71, 0.31], + ] + ) + h = 0.48 + + points = np.concatenate([xyz, xyz + [0, 0, h]]) + # only need to define the unique edge tangents (minus axis-aligned ones) + edges = [ + [0, 1], + [0, 2], + [1, 2], + ] + faces = [ + [0, 1, 2], + ] + + mesh = discretize.TreeMesh([16] * dim) + if dim == 2: + with pytest.raises(NotImplementedError): + mesh.refine_vertical_trianglular_prism(xyz, h, -1) + else: + mesh.refine_vertical_trianglular_prism(xyz, h, -1) + + # test it throws an error on incorrect number of points for the triangle + with pytest.raises(ValueError): + mesh.get_cells_in_vertical_trianglular_prism(xyz[:-1], h) + + cell_inds = mesh.get_cells_in_vertical_trianglular_prism(xyz, h) + + # ensure it found all of the cells by using a brute force search + test = [] + for cell in mesh: + try: + assert_cell_intersects_geometric(cell, points, edges=edges, faces=faces) + test.append(cell.index) + except AssertionError: + pass + np.testing.assert_array_equal(test, cell_inds) + + +def test_tetrahedron_locator(dim): + simplex = np.array( + [[0.32, 0.21, 0.15], [0.82, 0.19, 0.34], [0.14, 0.82, 0.29], [0.32, 0.27, 0.83]] + )[: dim + 1, :dim] + edges = [[0, 1], [0, 2], [1, 2], [0, 3], [1, 3], [2, 3]][: (dim - 1) * 3] + faces = [ + [0, 1, 2], + [0, 1, 3], + [0, 2, 3], + [1, 2, 3], + ][: 3 * dim - 5] + + mesh = discretize.TreeMesh([16] * dim) + mesh.refine_tetrahedron(simplex, -1) + + cell_inds = mesh.get_cells_in_tetrahedron(simplex) + + # test it throws an error without giving enough points to triangle. + with pytest.raises(ValueError): + mesh.get_cells_in_tetrahedron(simplex[:-1]) + + # ensure it found all of the cells by using a brute force search + test = [] + for cell in mesh: + try: + assert_cell_intersects_geometric(cell, simplex, edges=edges, faces=faces) + test.append(cell.index) + except AssertionError: + pass + np.testing.assert_array_equal(test, cell_inds) diff --git a/tests/tree/test_refine.py b/tests/tree/test_refine.py index e2ec53da4..b2ec1d7e9 100644 --- a/tests/tree/test_refine.py +++ b/tests/tree/test_refine.py @@ -1,92 +1,70 @@ import discretize import numpy as np import pytest +from discretize.tests import assert_cell_intersects_geometric def test_2d_line(): - segments = np.array([[0.1, 0.3], [0.3, 0.9]]) + segments = np.array([[0.12, 0.33], [0.32, 0.93]]) - mesh = discretize.TreeMesh([64, 64]) - mesh.refine_line(segments, mesh.max_level) + mesh1 = discretize.TreeMesh([64, 64]) + mesh1.refine_line(segments, -1) - cells = mesh.get_cells_along_line(segments[0], segments[1]) - levels = mesh.cell_levels_by_index(cells) + def refine_line(cell): + return assert_cell_intersects_geometric( + cell, segments, edges=[0, 1], as_refine=True + ) - np.testing.assert_equal(levels, mesh.max_level) + mesh2 = discretize.TreeMesh([64, 64]) + mesh2.refine(refine_line) + + assert mesh2.equals(mesh1) def test_3d_line(): - segments = np.array([[0.1, 0.3, 0.2], [0.3, 0.9, 0.7]]) + segments = np.array([[0.12, 0.33, 0.19], [0.32, 0.93, 0.68]]) + + mesh1 = discretize.TreeMesh([64, 64, 64]) + mesh1.refine_line(segments, -1) - mesh = discretize.TreeMesh([64, 64, 64]) - mesh.refine_line(segments, mesh.max_level) + def refine_line(cell): + return assert_cell_intersects_geometric( + cell, segments, edges=[0, 1], as_refine=True + ) - cells = mesh.get_cells_along_line(segments[0], segments[1]) - levels = mesh.cell_levels_by_index(cells) + mesh2 = discretize.TreeMesh([64, 64, 64]) + mesh2.refine(refine_line) - np.testing.assert_equal(levels, mesh.max_level) + assert mesh2.equals(mesh1) def test_line_errors(): mesh = discretize.TreeMesh([64, 64]) - segments2D = np.array([[0.1, 0.3], [0.3, 0.9]]) - segments3D = np.array([[0.1, 0.3, 0.2], [0.3, 0.9, 0.7]]) + rng = np.random.default_rng(512) + segments2D = rng.random((5, 2)) + segments3D = rng.random((5, 3)) # incorrect dimension with pytest.raises(ValueError): mesh.refine_line(segments3D, mesh.max_level, finalize=False) # incorrect number of levels + # 4 segments won't broadcast to 2 levels with pytest.raises(ValueError): mesh.refine_line(segments2D, [mesh.max_level, 3], finalize=False) def test_triangle2d(): - # define a slower function that is surely accurate triangle = np.array([[0.14, 0.31], [0.32, 0.96], [0.23, 0.87]]) - edges = np.stack( - [ - triangle[1] - triangle[0], - triangle[2] - triangle[1], - triangle[2] - triangle[0], - ] - ) + edges = [[0, 1], [0, 2], [1, 2]] - def project_min_max(points, axis): - ps = points @ axis - return ps.min(), ps.max() - - def refine_triangle2d(cell): - # The underlying C functions are more optimized - # but this is more explicit - x0 = cell.origin - xF = x0 + cell.h - - mins = triangle.min(axis=0) - if np.any(mins > xF): - return 0 - maxs = triangle.max(axis=0) - if np.any(maxs < x0): - return 0 - - box_points = np.array( - [ - [x0[0], x0[1]], - [x0[0], xF[1]], - [xF[0], x0[1]], - [xF[0], xF[1]], - ] + def refine_triangle(cell): + return assert_cell_intersects_geometric( + cell, triangle, edges=edges, as_refine=True ) - for i in range(3): - axis = [-edges[i, 1], edges[i, 0]] - bmin, bmax = project_min_max(box_points, axis) - tmin, tmax = project_min_max(triangle, axis) - if bmax < tmin or bmin > tmax: - return 0 - return -1 mesh1 = discretize.TreeMesh([64, 64]) - mesh1.refine(refine_triangle2d) + mesh1.refine(refine_triangle) mesh2 = discretize.TreeMesh([64, 64]) mesh2.refine_triangle(triangle, -1) @@ -95,60 +73,14 @@ def refine_triangle2d(cell): def test_triangle3d(): - # define a slower function that is surely accurate triangle = np.array([[0.14, 0.31, 0.23], [0.32, 0.96, 0.41], [0.23, 0.87, 0.72]]) - edges = np.stack( - [ - triangle[1] - triangle[0], - triangle[2] - triangle[1], - triangle[2] - triangle[0], - ] - ) - triangle_norm = np.cross(edges[0], edges[1]) - triangle_proj = triangle[0] @ triangle_norm - - def project_min_max(points, axis): - ps = points @ axis - return ps.min(), ps.max() - - box_normals = np.eye(3) + edges = [[0, 1], [0, 2], [1, 2]] + faces = [0, 1, 2] def refine_triangle(cell): - # The underlying C functions are more optimized - # but this is more explicit - x0 = cell.origin - xF = x0 + cell.h - - mins = triangle.min(axis=0) - if np.any(mins > xF): - return 0 - maxs = triangle.max(axis=0) - if np.any(maxs < x0): - return 0 - - box_points = np.array( - [ - [x0[0], x0[1], x0[2]], - [x0[0], xF[1], x0[2]], - [xF[0], x0[1], x0[2]], - [xF[0], xF[1], x0[2]], - [x0[0], x0[1], xF[2]], - [x0[0], xF[1], xF[2]], - [xF[0], x0[1], xF[2]], - [xF[0], xF[1], xF[2]], - ] + return assert_cell_intersects_geometric( + cell, triangle, edges=edges, faces=faces, as_refine=True ) - for i in range(3): - for j in range(3): - axis = np.cross(edges[i], box_normals[j]) - bmin, bmax = project_min_max(box_points, axis) - tmin, tmax = project_min_max(triangle, axis) - if bmax < tmin or bmin > tmax: - return 0 - bmin, bmax = project_min_max(box_points, triangle_norm) - if bmax < triangle_proj or bmin > triangle_proj: - return 0 - return -1 mesh1 = discretize.TreeMesh([64, 64, 64]) mesh1.refine(refine_triangle) @@ -160,9 +92,9 @@ def refine_triangle(cell): def test_triangle_errors(): - not_triangles_array = np.random.rand(4, 2, 2) - triangle3 = np.random.rand(3, 3) - triangles2 = np.random.rand(10, 3, 2) + not_triangles_array = np.empty((4, 2, 2)) + triangle3 = np.empty((3, 3)) + triangles2 = np.empty((10, 3, 2)) levels = np.full(8, -1) mesh1 = discretize.TreeMesh([64, 64]) @@ -184,49 +116,15 @@ def test_tetra2d(): # It actually calls triangle refine... just double check that works # define a slower function that is surely accurate triangle = np.array([[0.14, 0.31], [0.32, 0.96], [0.23, 0.87]]) - edges = np.stack( - [ - triangle[1] - triangle[0], - triangle[2] - triangle[1], - triangle[2] - triangle[0], - ] - ) + edges = [[0, 1], [0, 2], [1, 2]] - def project_min_max(points, axis): - ps = points @ axis - return ps.min(), ps.max() - - def refine_triangle2d(cell): - # The underlying C functions are more optimized - # but this is more explicit - x0 = cell.origin - xF = x0 + cell.h - - mins = triangle.min(axis=0) - if np.any(mins > xF): - return 0 - maxs = triangle.max(axis=0) - if np.any(maxs < x0): - return 0 - - box_points = np.array( - [ - [x0[0], x0[1]], - [x0[0], xF[1]], - [xF[0], x0[1]], - [xF[0], xF[1]], - ] + def refine_triangle(cell): + return assert_cell_intersects_geometric( + cell, triangle, edges=edges, as_refine=True ) - for i in range(3): - axis = [-edges[i, 1], edges[i, 0]] - bmin, bmax = project_min_max(box_points, axis) - tmin, tmax = project_min_max(triangle, axis) - if bmax < tmin or bmin > tmax: - return 0 - return -1 mesh1 = discretize.TreeMesh([64, 64]) - mesh1.refine(refine_triangle2d) + mesh1.refine(refine_triangle) mesh2 = discretize.TreeMesh([64, 64]) mesh2.refine_tetrahedron(triangle, -1) @@ -235,84 +133,21 @@ def refine_triangle2d(cell): def test_tetra3d(): - # define a slower function that is surely accurate simplex = np.array( [[0.32, 0.21, 0.15], [0.82, 0.19, 0.34], [0.14, 0.82, 0.29], [0.32, 0.27, 0.83]] ) - edges = np.stack( - [ - simplex[1] - simplex[0], - simplex[2] - simplex[0], - simplex[2] - simplex[1], - simplex[3] - simplex[0], - simplex[3] - simplex[1], - simplex[3] - simplex[2], - ] - ) - - def project_min_max(points, axis): - ps = points @ axis - return ps.min(), ps.max() - - box_normals = np.eye(3) + edges = [[0, 1], [0, 2], [1, 2], [0, 3], [1, 3], [2, 3]] + faces = [ + [0, 1, 2], + [0, 1, 3], + [0, 2, 3], + [1, 2, 3], + ] def refine_simplex(cell): - x0 = cell.origin - xF = x0 + cell.h - simp = simplex - - # Bounding box tests - # 3(x2) box face normals - mins = simp.min(axis=0) - if np.any(mins > xF): - return 0 - maxs = simp.max(axis=0) - if np.any(maxs < x0): - return 0 - - box_points = np.array( - [ - [x0[0], x0[1], x0[2]], - [x0[0], xF[1], x0[2]], - [xF[0], x0[1], x0[2]], - [xF[0], xF[1], x0[2]], - [x0[0], x0[1], xF[2]], - [x0[0], xF[1], xF[2]], - [xF[0], x0[1], xF[2]], - [xF[0], xF[1], xF[2]], - ] + return assert_cell_intersects_geometric( + cell, simplex, edges=edges, faces=faces, as_refine=True ) - # 3 box edges tangents and 6 simplex edge tangents - for i in range(6): - for j in range(3): - axis = np.cross(edges[i], box_normals[j]) - bmin, bmax = project_min_max(box_points, axis) - tmin, tmax = project_min_max(simp, axis) - if bmax < tmin or bmin > tmax: - return 0 - - # 4 simplex faces - axis = np.cross(edges[0], edges[1]) - tmin, tmax = project_min_max(simp, axis) - bmin, bmax = project_min_max(box_points, axis) - if bmax < tmin or bmin > tmax: - return 0 - axis = np.cross(edges[0], edges[3]) - tmin, tmax = project_min_max(simp, axis) - bmin, bmax = project_min_max(box_points, axis) - if bmax < tmin or bmin > tmax: - return 0 - axis = np.cross(edges[1], edges[4]) - tmin, tmax = project_min_max(simp, axis) - bmin, bmax = project_min_max(box_points, axis) - if bmax < tmin or bmin > tmax: - return 0 - axis = np.cross(edges[2], edges[5]) - tmin, tmax = project_min_max(simp, axis) - bmin, bmax = project_min_max(box_points, axis) - if bmax < tmin or bmin > tmax: - return 0 - return -1 mesh1 = discretize.TreeMesh([32, 32, 32]) mesh1.refine(refine_simplex) @@ -324,9 +159,9 @@ def refine_simplex(cell): def test_tetra_errors(): - not_simplex_array = np.random.rand(4, 3, 3) - simplex = np.random.rand(4, 2) - simplices = np.random.rand(10, 4, 3) + not_simplex_array = np.empty((4, 3, 3)) + simplex = np.empty((4, 2)) + simplices = np.empty((10, 4, 3)) levels = np.full(8, -1) mesh1 = discretize.TreeMesh([32, 32, 32]) @@ -346,12 +181,13 @@ def test_tetra_errors(): def test_box_errors(): mesh = discretize.TreeMesh([64, 64]) - x0s = np.array([0.1, 0.2]) - x0s2d = np.array([[0.1, 0.1], [0.5, 0.5]]) - x1s2d = np.array([[0.2, 0.3], [0.8, 0.9]]) + rng = np.random.default_rng(32) + x0s = rng.random((3, 2)) + x0s2d = 0.5 * rng.random((2, 2)) + x1s2d = 0.5 * rng.random((2, 2)) + 0.5 - x0s3d = np.array([[0.1, 0.1, 0.1], [0.5, 0.5, 0.5]]) - x1s3d = np.array([[0.2, 0.3, 0.1], [0.8, 0.9, 0.75]]) + x0s3d = 0.5 * rng.random((2, 3)) + x1s3d = 0.5 * rng.random((2, 3)) + 0.5 # incorrect dimension on x0 with pytest.raises(ValueError): @@ -412,27 +248,27 @@ def test_refine_triang_prism(): ) h = 0.48 - simps = np.array([[0, 1, 2]]) - - n_ps = len(xyz) - simps1 = np.c_[simps[:, 0], simps[:, 1], simps[:, 2], simps[:, 0]] + [0, 0, 0, n_ps] - simps2 = np.c_[simps[:, 0], simps[:, 1], simps[:, 2], simps[:, 1]] + [ - n_ps, - n_ps, - n_ps, - 0, + all_points = np.concatenate([xyz, xyz + [0, 0, h]]) + # only need to define the unique edge tangents (minus axis-aligned ones) + edges = [ + [0, 1], + [0, 2], + [1, 2], ] - simps3 = np.c_[simps[:, 1], simps[:, 2], simps[:, 0], simps[:, 2]] + [ - 0, - 0, - n_ps, - n_ps, + + # and define unique face normals (absent any face parallel to an axis, + # or with normal defined by an axis and an edge above.) + faces = [ + [0, 1, 2], ] - simps = np.r_[simps1, simps2, simps3] - points = np.r_[xyz, xyz + [0, 0, h]] + def refine_vert(cell): + return assert_cell_intersects_geometric( + cell, all_points, edges=edges, faces=faces, as_refine=True + ) + mesh1 = discretize.TreeMesh([32, 32, 32]) - mesh1.refine_tetrahedron(points[simps], -1) + mesh1.refine(refine_vert) mesh2 = discretize.TreeMesh([32, 32, 32]) mesh2.refine_vertical_trianglular_prism(xyz, h, -1) @@ -461,24 +297,25 @@ def test_refine_triang_prism_errors(): mesh.refine_vertical_trianglular_prism(xyz[:, :-1], h, -1) # incorrect levels and triangles - ps = np.random.rand(10, 3, 3) + ps = np.empty((10, 3, 3)) with pytest.raises(ValueError): mesh.refine_vertical_trianglular_prism(ps, h, [-1, -2]) # incorrect heights and triangles - ps = np.random.rand(10, 3, 3) + ps = np.empty((10, 3, 3)) with pytest.raises(ValueError): mesh.refine_vertical_trianglular_prism(ps, [h, h], -1) # negative heights - ps = np.random.rand(10, 3, 3) + ps = np.empty((10, 3, 3)) with pytest.raises(ValueError): mesh.refine_vertical_trianglular_prism(ps, -h, -1) def test_bounding_box(): # No padding - xyz = np.random.rand(20, 2) * 0.25 + 3 / 8 + rng = np.random.default_rng(51623978) + xyz = rng.random((20, 2)) * 0.25 + 3 / 8 mesh1 = discretize.TreeMesh([32, 32]) mesh1.refine_bounding_box(xyz, -1, None) @@ -512,7 +349,7 @@ def test_bounding_box(): def test_bounding_box_errors(): mesh1 = discretize.TreeMesh([32, 32]) - xyz = np.random.rand(20, 3) + xyz = np.empty((20, 3)) # incorrect padding shape with pytest.raises(ValueError): mesh1.refine_bounding_box(xyz, -1, [[2, 3, 4]]) @@ -620,3 +457,36 @@ def test_refine_surface_errors(): with pytest.raises(IndexError): mesh.refine_surface(points, 20) + + +def test_refine_plane2D(): + p0 = [2, 2] + normal = [-1, 1] + p1 = [-2, -2] + + mesh1 = discretize.TreeMesh([64, 64]) + mesh1.refine_plane(p0, normal, -1) + + mesh2 = discretize.TreeMesh([64, 64]) + mesh2.refine_line(np.stack([p0, p1]), -1) + + assert mesh1.equals(mesh2) + + +def test_refine_plane3D(): + p0 = [20, 20, 20] + normal = [-1, -1, 2] + # define 4 corner points (including p0) of a plane to create triangles + # to verify the refine functionallity + p1 = [20, -20, 0] + p2 = [-20, 20, 0] + p3 = [-20, -20, -20] + tris = np.stack([[p0, p1, p2], [p1, p2, p3]]) + + mesh1 = discretize.TreeMesh([64, 64, 64]) + mesh1.refine_plane(p0, normal, -1) + + mesh2 = discretize.TreeMesh([64, 64, 64]) + mesh2.refine_triangle(tris, -1) + + assert mesh1.equals(mesh2) diff --git a/tests/tree/test_tree.py b/tests/tree/test_tree.py index d63310a3d..de487f7d2 100644 --- a/tests/tree/test_tree.py +++ b/tests/tree/test_tree.py @@ -5,12 +5,14 @@ TOL = 1e-8 +rng = np.random.default_rng(6234) + class TestSimpleQuadTree(unittest.TestCase): def test_counts(self): nc = 8 - h1 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 - h2 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 + h1 = rng.random(nc) * nc * 0.5 + nc * 0.5 + h2 = rng.random(nc) * nc * 0.5 + nc * 0.5 h = [hi / np.sum(hi) for hi in [h1, h2]] # normalize M = discretize.TreeMesh(h) points = np.array([[0.1, 0.1]]) @@ -132,9 +134,9 @@ def test_serialization(self): class TestOcTree(unittest.TestCase): def test_counts(self): nc = 8 - h1 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 - h2 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 - h3 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 + 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 M = discretize.TreeMesh(h, levels=3) points = np.array([[0.2, 0.1, 0.7], [0.8, 0.4, 0.2]]) @@ -311,8 +313,8 @@ def refinefcn(cell): def test_cell_nodes(self): # 2D nc = 8 - h1 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 - h2 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 + h1 = rng.random(nc) * nc * 0.5 + nc * 0.5 + h2 = rng.random(nc) * nc * 0.5 + nc * 0.5 h = [hi / np.sum(hi) for hi in [h1, h2]] # normalize M = discretize.TreeMesh(h) points = np.array([[0.2, 0.1], [0.8, 0.4]]) @@ -326,9 +328,9 @@ def test_cell_nodes(self): # 3D nc = 8 - h1 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 - h2 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 - h3 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 + 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 M = discretize.TreeMesh(h, levels=3) points = np.array([[0.2, 0.1, 0.7], [0.8, 0.4, 0.2]]) @@ -346,8 +348,8 @@ class TestTreeMeshNodes: def sample_mesh(self, request): """Return a sample TreeMesh""" nc = 8 - h1 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 - h2 = np.random.rand(nc) * nc * 0.5 + nc * 0.5 + h1 = rng.random(nc) * nc * 0.5 + nc * 0.5 + h2 = rng.random(nc) * nc * 0.5 + nc * 0.5 if request.param == "2D": h = [hi / np.sum(hi) for hi in [h1, h2]] # normalize mesh = discretize.TreeMesh(h) @@ -355,7 +357,7 @@ def sample_mesh(self, request): levels = np.array([1, 2]) mesh.insert_cells(points, levels, finalize=True) else: - h3 = np.random.rand(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 mesh = discretize.TreeMesh(h, levels=3) points = np.array([[0.2, 0.1, 0.7], [0.8, 0.4, 0.2]]) @@ -378,6 +380,64 @@ def test_total_nodes(self, sample_mesh): ) +class TestTreeCellBounds: + """Test ``TreeCell.bounds`` method""" + + @pytest.fixture(params=["2D", "3D"]) + def mesh(self, request): + """Return a sample TreeMesh""" + nc = 16 + if request.param == "2D": + h = [nc, nc] + origin = (-32.4, 245.4) + mesh = discretize.TreeMesh(h, origin) + p1 = (origin[0] + 0.4, origin[1] + 0.4) + p2 = (origin[0] + 0.6, origin[1] + 0.6) + mesh.refine_box(p1, p2, levels=5, finalize=True) + else: + h = [nc, nc, nc] + origin = (-32.4, 245.4, 192.3) + mesh = discretize.TreeMesh(h, origin) + p1 = (origin[0] + 0.4, origin[1] + 0.4, origin[2] + 0.7) + p2 = (origin[0] + 0.6, origin[1] + 0.6, origin[2] + 0.9) + mesh.refine_box(p1, p2, levels=5, finalize=True) + return mesh + + def test_bounds(self, mesh): + """Test bounds method of one of the cells in the mesh.""" + cell = mesh[16] + nodes = mesh.nodes[cell.nodes] + x1, x2 = nodes[0][0], nodes[-1][0] + y1, y2 = nodes[0][1], nodes[-1][1] + if mesh.dim == 2: + expected_bounds = np.array([x1, x2, y1, y2]) + else: + z1, z2 = nodes[0][2], nodes[-1][2] + expected_bounds = np.array([x1, x2, y1, y2, z1, z2]) + np.testing.assert_equal(cell.bounds, expected_bounds) + + def test_bounds_relations(self, mesh): + """Test if bounds are in the right order for one cell in the mesh.""" + cell = mesh[16] + if mesh.dim == 2: + x1, x2, y1, y2 = cell.bounds + assert x1 < x2 + assert y1 < y2 + else: + x1, x2, y1, y2, z1, z2 = cell.bounds + assert x1 < x2 + assert y1 < y2 + assert z1 < z2 + + def test_cell_bounds(self, mesh): + """Test cell_bounds method of the tree mesh.""" + cell_bounds = mesh.cell_bounds + cell_bounds_slow = np.empty((mesh.n_cells, 2 * mesh.dim)) + for i, cell in enumerate(mesh): + cell_bounds_slow[i] = cell.bounds + np.testing.assert_equal(cell_bounds, cell_bounds_slow) + + class Test2DInterpolation(unittest.TestCase): def setUp(self): def topo(x): @@ -407,12 +467,12 @@ def function(cell): self.M = M def test_fx(self): - r = np.random.rand(self.M.nFx) + r = rng.random(self.M.nFx) P = self.M.get_interpolation_matrix(self.M.gridFx, "Fx") self.assertLess(np.abs(P[:, : self.M.nFx] * r - r).max(), TOL) def test_fy(self): - r = np.random.rand(self.M.nFy) + r = rng.random(self.M.nFy) P = self.M.get_interpolation_matrix(self.M.gridFy, "Fy") self.assertLess(np.abs(P[:, self.M.nFx :] * r - r).max(), TOL) @@ -437,36 +497,36 @@ def function(cell): self.M = M def test_Fx(self): - r = np.random.rand(self.M.nFx) + r = rng.random(self.M.nFx) P = self.M.get_interpolation_matrix(self.M.gridFx, "Fx") self.assertLess(np.abs(P[:, : self.M.nFx] * r - r).max(), TOL) def test_Fy(self): - r = np.random.rand(self.M.nFy) + r = rng.random(self.M.nFy) P = self.M.get_interpolation_matrix(self.M.gridFy, "Fy") self.assertLess( np.abs(P[:, self.M.nFx : (self.M.nFx + self.M.nFy)] * r - r).max(), TOL ) def test_Fz(self): - r = np.random.rand(self.M.nFz) + r = rng.random(self.M.nFz) P = self.M.get_interpolation_matrix(self.M.gridFz, "Fz") self.assertLess(np.abs(P[:, (self.M.nFx + self.M.nFy) :] * r - r).max(), TOL) def test_Ex(self): - r = np.random.rand(self.M.nEx) + r = rng.random(self.M.nEx) P = self.M.get_interpolation_matrix(self.M.gridEx, "Ex") self.assertLess(np.abs(P[:, : self.M.nEx] * r - r).max(), TOL) def test_Ey(self): - r = np.random.rand(self.M.nEy) + r = rng.random(self.M.nEy) P = self.M.get_interpolation_matrix(self.M.gridEy, "Ey") self.assertLess( np.abs(P[:, self.M.nEx : (self.M.nEx + self.M.nEy)] * r - r).max(), TOL ) def test_Ez(self): - r = np.random.rand(self.M.nEz) + r = rng.random(self.M.nEz) P = self.M.get_interpolation_matrix(self.M.gridEz, "Ez") self.assertLess(np.abs(P[:, (self.M.nEx + self.M.nEy) :] * r - r).max(), TOL) diff --git a/tests/tree/test_tree_balancing.py b/tests/tree/test_tree_balancing.py index 994d522c1..8721c2daf 100644 --- a/tests/tree/test_tree_balancing.py +++ b/tests/tree/test_tree_balancing.py @@ -150,7 +150,7 @@ def test_refine_tetra(): mesh1.refine_tetrahedron(simplex, -1) bad_nodes = check_for_diag_unbalance(mesh1) - assert len(bad_nodes) == 62 + assert len(bad_nodes) == 64 mesh2 = discretize.TreeMesh([32, 32, 32], diagonal_balance=True) mesh2.refine_tetrahedron(simplex, -1) diff --git a/tests/tree/test_tree_innerproduct_derivs.py b/tests/tree/test_tree_innerproduct_derivs.py index 96885a749..da4ff3026 100644 --- a/tests/tree/test_tree_innerproduct_derivs.py +++ b/tests/tree/test_tree_innerproduct_derivs.py @@ -2,6 +2,8 @@ import unittest import discretize +rng = np.random.default_rng(678423) + class TestInnerProductsDerivsTensor(unittest.TestCase): def doTestFace( @@ -15,8 +17,8 @@ def doTestFace( mesh.refine(lambda xc: 3) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nF) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nC * rep) + v = rng.random(mesh.nF) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nC * rep) def fun(sig): M = mesh.get_face_inner_product( @@ -38,7 +40,9 @@ def fun(sig): fast, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=4421 + ) def doTestEdge( self, h, rep, fast, meshType, invert_model=False, invert_matrix=False @@ -51,8 +55,8 @@ def doTestEdge( mesh.refine(lambda xc: 3) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nE) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nC * rep) + v = rng.random(mesh.nE) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nC * rep) def fun(sig): M = mesh.get_edge_inner_product( @@ -74,7 +78,9 @@ def fun(sig): fast, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=643 + ) def test_FaceIP_2D_float_Tree(self): self.assertTrue(self.doTestFace([8, 8], 0, False, "Tree")) @@ -169,8 +175,8 @@ def doTestFace(self, h, rep, meshType, invert_model=False, invert_matrix=False): mesh.refine(lambda xc: 3) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nF) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nF * rep) + v = rng.random(mesh.nF) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nF * rep) def fun(sig): M = mesh.get_face_inner_product_surface( @@ -191,7 +197,9 @@ def fun(sig): # fast, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=677 + ) def doTestEdge(self, h, rep, meshType, invert_model=False, invert_matrix=False): if meshType == "Curv": @@ -202,8 +210,8 @@ def doTestEdge(self, h, rep, meshType, invert_model=False, invert_matrix=False): mesh.refine(lambda xc: 3) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nE) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nF * rep) + v = rng.random(mesh.nE) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nF * rep) def fun(sig): M = mesh.get_edge_inner_product_surface( @@ -224,7 +232,9 @@ def fun(sig): # fast, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=543 + ) def test_FaceIP_2D_float_fast_Tree(self): self.assertTrue(self.doTestFace([8, 8], 0, "Tree")) @@ -261,8 +271,8 @@ def doTestEdge(self, h, rep, meshType, invert_model=False, invert_matrix=False): mesh.refine(lambda xc: 3) elif meshType == "Tensor": mesh = discretize.TensorMesh(h) - v = np.random.rand(mesh.nE) - sig = np.random.rand(1) if rep == 0 else np.random.rand(mesh.nE * rep) + v = rng.random(mesh.nE) + sig = rng.random(1) if rep == 0 else rng.random(mesh.nE * rep) def fun(sig): M = mesh.get_edge_inner_product_line( @@ -283,7 +293,9 @@ def fun(sig): # fast, ("harmonic" if invert_model and invert_matrix else "standard"), ) - return discretize.tests.check_derivative(fun, sig, num=5, plotIt=False) + return discretize.tests.check_derivative( + fun, sig, num=5, plotIt=False, random_seed=23 + ) def test_EdgeIP_2D_float_fast_Tree(self): self.assertTrue(self.doTestEdge([8, 8], 0, "Tree")) diff --git a/tests/tree/test_tree_interpolation.py b/tests/tree/test_tree_interpolation.py index 2886a2970..d4688364b 100644 --- a/tests/tree/test_tree_interpolation.py +++ b/tests/tree/test_tree_interpolation.py @@ -42,7 +42,6 @@ class TestInterpolation2d(discretize.tests.OrderTest): """ name = "Interpolation 2D" - # LOCS = np.random.rand(50, 2)*0.6+0.2 # location_type = 'Ex' X, Y = np.mgrid[0:1:250j, 0:1:250j] LOCS = np.c_[X.reshape(-1), Y.reshape(-1)] @@ -129,7 +128,6 @@ def test_orderEy(self): class TestInterpolation3D(discretize.tests.OrderTest): name = "Interpolation" - # LOCS = np.random.rand(50, 3)*0.6+0.2 X, Y, Z = np.mgrid[0:1:50j, 0:1:50j, 0:1:50j] LOCS = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] meshTypes = MESHTYPES diff --git a/tests/tree/test_tree_io.py b/tests/tree/test_tree_io.py index 91689b94c..e149edcb6 100644 --- a/tests/tree/test_tree_io.py +++ b/tests/tree/test_tree_io.py @@ -1,9 +1,8 @@ import numpy as np -import unittest -import os import discretize import pickle import json +import pytest try: import vtk # NOQA F401 @@ -13,206 +12,72 @@ has_vtk = False -class TestOcTreeMeshIO(unittest.TestCase): - def setUp(self): +@pytest.fixture(params=[2, 3]) +def mesh(request): + dim = request.param + if dim == 2: + mesh = discretize.TreeMesh([8, 8]) + mesh.refine(2, finalize=False) + mesh.refine_ball([0.25, 0.25], 0.25, 3) + else: h = np.ones(16) mesh = discretize.TreeMesh([h, 2 * h, 3 * h]) cell_points = np.array([[0.5, 0.5, 0.5], [0.5, 2.5, 0.5]]) cell_levels = np.array([4, 4]) mesh.insert_cells(cell_points, cell_levels) - self.mesh = mesh + return mesh - def test_UBC3Dfiles(self): - mesh = self.mesh - # Make a vector + +def test_UBCfiles(mesh, tmp_path): + # Make a vector + vec = np.arange(mesh.n_cells) + # Write and read + mesh_file = tmp_path / "temp.msh" + model_file = tmp_path / "arange.txt" + + mesh.write_UBC(mesh_file, {model_file: vec}) + meshUBC = discretize.TreeMesh.read_UBC(mesh_file) + vecUBC = meshUBC.read_model_UBC(model_file) + + assert mesh is not meshUBC + assert mesh.equals(meshUBC) + np.testing.assert_array_equal(vec, vecUBC) + + # Write it again with another IO function + mesh.write_model_UBC([model_file], [vec]) + vecUBC2 = mesh.read_model_UBC(model_file) + np.testing.assert_array_equal(vec, vecUBC2) + + +if has_vtk: + + def test_write_VTU_files(mesh, tmp_path): vec = np.arange(mesh.nC) - # Write and read - mesh.write_UBC("temp.msh", {"arange.txt": vec}) - meshUBC = discretize.TreeMesh.read_UBC("temp.msh") - vecUBC = meshUBC.read_model_UBC("arange.txt") - - self.assertEqual(mesh.nC, meshUBC.nC) - self.assertEqual(mesh.__str__(), meshUBC.__str__()) - self.assertTrue(np.allclose(mesh.gridCC, meshUBC.gridCC)) - self.assertTrue(np.allclose(vec, vecUBC)) - self.assertTrue(np.allclose(np.array(mesh.h), np.array(meshUBC.h))) - - # Write it again with another IO function - mesh.write_model_UBC(["arange.txt"], [vec]) - vecUBC2 = mesh.read_model_UBC("arange.txt") - self.assertTrue(np.allclose(vec, vecUBC2)) - - print("IO of UBC octree files is working") - os.remove("temp.msh") - os.remove("arange.txt") - - def test_UBC2Dfiles(self): - mesh0 = discretize.TreeMesh([8, 8]) - - def refine(cell): - xyz = cell.center - dist = ((xyz - 0.25) ** 2).sum() ** 0.5 - if dist < 0.25: - return 3 - return 2 - - mesh0.refine(refine) - - mod0 = np.arange(mesh0.nC) - mesh0.write_UBC("tmp.msh", {"arange.txt": mod0}) - mesh1 = discretize.TreeMesh.read_UBC("tmp.msh") - mod1 = mesh1.read_model_UBC("arange.txt") - - self.assertEqual(mesh0.nC, mesh1.nC) - self.assertEqual(mesh0.__str__(), mesh1.__str__()) - self.assertTrue(np.allclose(mesh0.gridCC, mesh1.gridCC)) - self.assertTrue(np.allclose(np.array(mesh0.h), np.array(mesh1.h))) - self.assertTrue(np.allclose(mod0, mod1)) - print("IO of UBC like 2D TreeMesh is working") - - if has_vtk: - - def test_VTUfiles(self): - mesh = self.mesh - vec = np.arange(mesh.nC) - mesh.write_vtk("temp.vtu", {"arange": vec}) - print("Writing of VTU files is working") - os.remove("temp.vtu") - - -class TestPickle(unittest.TestCase): - def test_pickle2D(self): - mesh0 = discretize.TreeMesh([8, 8]) - - def refine(cell): - xyz = cell.center - dist = ((xyz - 0.25) ** 2).sum() ** 0.5 - if dist < 0.25: - return 3 - return 2 - - mesh0.refine(refine) - - byte_string = pickle.dumps(mesh0) - mesh1 = pickle.loads(byte_string) - - self.assertEqual(mesh0.nC, mesh1.nC) - self.assertEqual(mesh0.__str__(), mesh1.__str__()) - self.assertTrue(np.allclose(mesh0.gridCC, mesh1.gridCC)) - self.assertTrue(np.allclose(np.array(mesh0.h), np.array(mesh1.h))) - print("Pickling of 2D TreeMesh is working") - - def test_pickle3D(self): - mesh0 = discretize.TreeMesh([8, 8, 8]) - - def refine(cell): - xyz = cell.center - dist = ((xyz - 0.25) ** 2).sum() ** 0.5 - if dist < 0.25: - return 3 - return 2 - - mesh0.refine(refine) - - byte_string = pickle.dumps(mesh0) - mesh1 = pickle.loads(byte_string) - - self.assertEqual(mesh0.nC, mesh1.nC) - self.assertEqual(mesh0.__str__(), mesh1.__str__()) - self.assertTrue(np.allclose(mesh0.gridCC, mesh1.gridCC)) - self.assertTrue(np.allclose(np.array(mesh0.h), np.array(mesh1.h))) - print("Pickling of 3D TreeMesh is working") - - -class TestSerialize(unittest.TestCase): - def test_dic_serialize2D(self): - mesh0 = discretize.TreeMesh([8, 8]) - - def refine(cell): - xyz = cell.center - dist = ((xyz - 0.25) ** 2).sum() ** 0.5 - if dist < 0.25: - return 3 - return 2 - - mesh0.refine(refine) - - mesh_dict = mesh0.serialize() - mesh1 = discretize.TreeMesh.deserialize(mesh_dict) - - self.assertEqual(mesh0.nC, mesh1.nC) - self.assertEqual(mesh0.__str__(), mesh1.__str__()) - self.assertTrue(np.allclose(mesh0.gridCC, mesh1.gridCC)) - self.assertTrue(np.allclose(np.array(mesh0.h), np.array(mesh1.h))) - print("dic serialize 2D is working") - - def test_save_load_json2D(self): - mesh0 = discretize.TreeMesh([8, 8]) - - def refine(cell): - xyz = cell.center - dist = ((xyz - 0.25) ** 2).sum() ** 0.5 - if dist < 0.25: - return 3 - return 2 - - mesh0.refine(refine) - - mesh0.save("tree.json") - with open("tree.json", "r") as outfile: - jsondict = json.load(outfile) - mesh1 = discretize.TreeMesh.deserialize(jsondict) - - self.assertEqual(mesh0.nC, mesh1.nC) - self.assertEqual(mesh0.__str__(), mesh1.__str__()) - self.assertTrue(np.allclose(mesh0.gridCC, mesh1.gridCC)) - self.assertTrue(np.allclose(np.array(mesh0.h), np.array(mesh1.h))) - print("json serialize 2D is working") - - def test_dic_serialize3D(self): - mesh0 = discretize.TreeMesh([8, 8, 8]) - - def refine(cell): - xyz = cell.center - dist = ((xyz - 0.25) ** 2).sum() ** 0.5 - if dist < 0.25: - return 3 - return 2 + mesh_file = tmp_path / "temp.vtu" + mesh.write_vtk(mesh_file, {"arange": vec}) + + +def test_pickle(mesh): + byte_string = pickle.dumps(mesh) + mesh_pickle = pickle.loads(byte_string) + + assert mesh is not mesh_pickle + assert mesh.equals(mesh_pickle) + - mesh0.refine(refine) - - mesh_dict = mesh0.serialize() - mesh1 = discretize.TreeMesh.deserialize(mesh_dict) - - self.assertEqual(mesh0.nC, mesh1.nC) - self.assertEqual(mesh0.__str__(), mesh1.__str__()) - self.assertTrue(np.allclose(mesh0.gridCC, mesh1.gridCC)) - self.assertTrue(np.allclose(np.array(mesh0.h), np.array(mesh1.h))) - print("dic serialize 3D is working") +def test_dic_serialize(mesh): + mesh_dict = mesh.serialize() + mesh2 = discretize.TreeMesh.deserialize(mesh_dict) + assert mesh is not mesh2 + assert mesh.equals(mesh2) - def test_save_load_json3D(self): - mesh0 = discretize.TreeMesh([8, 8, 8]) - - def refine(cell): - xyz = cell.center - dist = ((xyz - 0.25) ** 2).sum() ** 0.5 - if dist < 0.25: - return 3 - return 2 - - mesh0.refine(refine) - - mesh0.save("tree.json") - with open("tree.json", "r") as outfile: - jsondict = json.load(outfile) - mesh1 = discretize.TreeMesh.deserialize(jsondict) - - self.assertEqual(mesh0.nC, mesh1.nC) - self.assertEqual(mesh0.__str__(), mesh1.__str__()) - self.assertTrue(np.allclose(mesh0.gridCC, mesh1.gridCC)) - self.assertTrue(np.allclose(np.array(mesh0.h), np.array(mesh1.h))) - print("json serialize 3D is working") +def test_json_serialize(mesh, tmp_path): + json_file = tmp_path / "tree.json" -if __name__ == "__main__": - unittest.main() + mesh.save(json_file) + with open(json_file, "r") as outfile: + jsondict = json.load(outfile) + mesh2 = discretize.TreeMesh.deserialize(jsondict) + assert mesh is not mesh2 + assert mesh.equals(mesh2) diff --git a/tests/tree/test_tree_operators.py b/tests/tree/test_tree_operators.py index 22b2e2c8b..12a9b1ec2 100644 --- a/tests/tree/test_tree_operators.py +++ b/tests/tree/test_tree_operators.py @@ -30,9 +30,6 @@ ) ) -# np.random.seed(None) -# np.random.seed(7) - class TestCellGrad2D(discretize.tests.OrderTest): name = "Cell Gradient 2D, using cellGradx and cellGrady" @@ -58,8 +55,7 @@ def getError(self): return err def test_order(self): - np.random.seed(7) - self.orderTest() + self.orderTest(random_seed=421) class TestCellGrad3D(discretize.tests.OrderTest): @@ -108,8 +104,7 @@ def getError(self): return err def test_order(self): - np.random.seed(6) - self.orderTest() + self.orderTest(5532) class TestFaceDivxy2D(discretize.tests.OrderTest): @@ -139,8 +134,7 @@ def getError(self): return err def test_order(self): - np.random.seed(4) - self.orderTest() + self.orderTest(random_seed=19647823) class TestFaceDiv3D(discretize.tests.OrderTest): @@ -167,8 +161,7 @@ def getError(self): return np.linalg.norm((divF - divF_ana), np.inf) def test_order(self): - np.random.seed(7) - self.orderTest() + self.orderTest(random_seed=81725364) class TestFaceDivxyz3D(discretize.tests.OrderTest): @@ -205,13 +198,12 @@ def getError(self): return err def test_order(self): - np.random.seed(7) - self.orderTest() + self.orderTest(random_seed=6172824) class TestCurl(discretize.tests.OrderTest): name = "Curl" - meshTypes = ["notatreeTree", "uniformTree"] # , 'randomTree']#, 'uniformTree'] + meshTypes = ["notatreeTree", "uniformTree"] meshSizes = [8, 16] # , 32] expectedOrders = [2, 1] # This is due to linear interpolation in the Re projection @@ -241,13 +233,12 @@ def getError(self): return err def test_order(self): - np.random.seed(7) self.orderTest() class TestNodalGrad(discretize.tests.OrderTest): name = "Nodal Gradient" - meshTypes = ["notatreeTree", "uniformTree"] # ['randomTree', 'uniformTree'] + meshTypes = ["notatreeTree", "uniformTree"] meshSizes = [8, 16] # , 32] expectedOrders = [2, 1] @@ -270,13 +261,12 @@ def getError(self): return err def test_order(self): - np.random.seed(7) self.orderTest() class TestNodalGrad2D(discretize.tests.OrderTest): name = "Nodal Gradient 2D" - meshTypes = ["notatreeTree", "uniformTree"] # ['randomTree', 'uniformTree'] + meshTypes = ["notatreeTree", "uniformTree"] meshSizes = [8, 16] # , 32] expectedOrders = [2, 1] meshDimension = 2 @@ -299,7 +289,6 @@ def getError(self): return err def test_order(self): - np.random.seed(7) self.orderTest() @@ -921,7 +910,7 @@ def test_order1_edges_invert_model(self): class TestTreeAveraging2D(discretize.tests.OrderTest): """Integrate an function over a unit cube domain using edgeInnerProducts and faceInnerProducts.""" - meshTypes = ["notatreeTree", "uniformTree"] # 'randomTree'] + meshTypes = ["notatreeTree", "uniformTree"] meshDimension = 2 meshSizes = [4, 8, 16] expectedOrders = [2, 1] @@ -1001,7 +990,7 @@ def test_orderCC2F(self): class TestAveraging3D(discretize.tests.OrderTest): name = "Averaging 3D" - meshTypes = ["notatreeTree", "uniformTree"] # , 'randomTree'] + meshTypes = ["notatreeTree", "uniformTree"] meshDimension = 3 meshSizes = [8, 16] expectedOrders = [2, 1] diff --git a/tests/tree/test_tree_plotting.py b/tests/tree/test_tree_plotting.py index a932ed037..8ed4623be 100644 --- a/tests/tree/test_tree_plotting.py +++ b/tests/tree/test_tree_plotting.py @@ -6,6 +6,8 @@ matplotlib.use("Agg") +rng = np.random.default_rng(4213678) + class TestOcTreePlotting(unittest.TestCase): def setUp(self): @@ -22,8 +24,8 @@ def test_plot_slice(self): mesh.plot_grid(faces=True, edges=True, nodes=True) # CC plot - mod_cc = np.random.rand(len(mesh)) + 1j * np.random.rand(len(mesh)) - mod_cc[np.random.rand(len(mesh)) < 0.2] = np.nan + mod_cc = rng.random(len(mesh)) + 1j * rng.random(len(mesh)) + mod_cc[rng.random(len(mesh)) < 0.2] = np.nan mesh.plot_slice(mod_cc, normal="X", grid=True) mesh.plot_slice(mod_cc, normal="Y", ax=ax) @@ -31,11 +33,11 @@ def test_plot_slice(self): mesh.plot_slice(mod_cc, view="imag", ax=ax) mesh.plot_slice(mod_cc, view="abs", ax=ax) - mod_ccv = np.random.rand(len(mesh), 3) + mod_ccv = rng.random((len(mesh), 3)) mesh.plot_slice(mod_ccv, v_type="CCv", view="vec", ax=ax) # F plot tests - mod_f = np.random.rand(mesh.n_faces) + mod_f = rng.random(mesh.n_faces) mesh.plot_slice(mod_f, v_type="Fx", ax=ax) mesh.plot_slice(mod_f, v_type="Fy", ax=ax) mesh.plot_slice(mod_f, v_type="Fz", ax=ax) @@ -43,7 +45,7 @@ def test_plot_slice(self): mesh.plot_slice(mod_f, v_type="F", view="vec", ax=ax) # E plot tests - mod_e = np.random.rand(mesh.n_edges) + mod_e = rng.random(mesh.n_edges) mesh.plot_slice(mod_e, v_type="Ex", ax=ax) mesh.plot_slice(mod_e, v_type="Ey", ax=ax) mesh.plot_slice(mod_e, v_type="Ez", ax=ax) @@ -51,7 +53,7 @@ def test_plot_slice(self): mesh.plot_slice(mod_e, v_type="E", view="vec", ax=ax) # Nodes - mod_n = np.random.rand(mesh.n_nodes) + mod_n = rng.random(mesh.n_nodes) mesh.plot_slice(mod_n, v_type="N") plt.close("all") @@ -70,32 +72,32 @@ def test_plot_slice(self): mesh.plot_grid(faces=True, edges=True, nodes=True) # CC plot - mod_cc = np.random.rand(len(mesh)) + 1j * np.random.rand(len(mesh)) - mod_cc[np.random.rand(len(mesh)) < 0.2] = np.nan + mod_cc = rng.random(len(mesh)) + 1j * rng.random(len(mesh)) + mod_cc[rng.random(len(mesh)) < 0.2] = np.nan mesh.plot_image(mod_cc) mesh.plot_image(mod_cc, ax=ax) mesh.plot_image(mod_cc, view="imag", ax=ax) mesh.plot_image(mod_cc, view="abs", ax=ax) - mod_ccv = np.random.rand(len(mesh), 2) + mod_ccv = rng.random((len(mesh), 2)) mesh.plot_image(mod_ccv, v_type="CCv", view="vec", ax=ax) # F plot tests - mod_f = np.random.rand(mesh.n_faces) + mod_f = rng.random(mesh.n_faces) mesh.plot_image(mod_f, v_type="Fx", ax=ax) mesh.plot_image(mod_f, v_type="Fy", ax=ax) mesh.plot_image(mod_f, v_type="F", ax=ax) mesh.plot_image(mod_f, v_type="F", view="vec", ax=ax) # E plot tests - mod_e = np.random.rand(mesh.n_edges) + mod_e = rng.random(mesh.n_edges) mesh.plot_image(mod_e, v_type="Ex", ax=ax) mesh.plot_image(mod_e, v_type="Ey", ax=ax) mesh.plot_image(mod_e, v_type="E", ax=ax) mesh.plot_image(mod_e, v_type="E", view="vec", ax=ax) # Nodes - mod_n = np.random.rand(mesh.n_nodes) + mod_n = rng.random(mesh.n_nodes) mesh.plot_image(mod_n, v_type="N", ax=ax) plt.close("all") diff --git a/tests/tree/test_tree_utils.py b/tests/tree/test_tree_utils.py index e868058a5..692d27f56 100644 --- a/tests/tree/test_tree_utils.py +++ b/tests/tree/test_tree_utils.py @@ -3,7 +3,6 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz TOL = 1e-8 -np.random.seed(12) class TestRefineOcTree(unittest.TestCase): diff --git a/tutorials/inner_products/1_basic.py b/tutorials/inner_products/1_basic.py index bd86eec09..4d317251d 100644 --- a/tutorials/inner_products/1_basic.py +++ b/tutorials/inner_products/1_basic.py @@ -74,6 +74,8 @@ import matplotlib.pyplot as plt import numpy as np +rng = np.random.default_rng(8572) + # sphinx_gallery_thumbnail_number = 2 @@ -257,10 +259,10 @@ def fcn_y(xy, sig): Mf_inv = mesh.get_face_inner_product(invert_matrix=True) # Generate some random vectors -phi_c = np.random.rand(mesh.nC) -# phi_n = np.random.rand(mesh.nN) -vec_e = np.random.rand(mesh.nE) -vec_f = np.random.rand(mesh.nF) +phi_c = rng.random(mesh.nC) +# phi_n = rng.random(mesh.nN) +vec_e = rng.random(mesh.nE) +vec_f = rng.random(mesh.nF) # Generate some random vectors norm_c = np.linalg.norm(phi_c - Mc_inv.dot(Mc.dot(phi_c))) diff --git a/tutorials/inner_products/2_physical_properties.py b/tutorials/inner_products/2_physical_properties.py index 531e8b077..d5845da5d 100644 --- a/tutorials/inner_products/2_physical_properties.py +++ b/tutorials/inner_products/2_physical_properties.py @@ -75,6 +75,8 @@ import numpy as np import matplotlib.pyplot as plt +rng = np.random.default_rng(87236) + # sphinx_gallery_thumbnail_number = 1 ##################################################### @@ -181,17 +183,17 @@ mesh = TensorMesh([h, h, h]) # Isotropic case: (nC, ) numpy array -sig = np.random.rand(mesh.nC) # sig for each cell +sig = rng.random(mesh.nC) # sig for each cell Me1 = mesh.get_edge_inner_product(sig) # Edges inner product matrix Mf1 = mesh.get_face_inner_product(sig) # Faces inner product matrix # Linear case: (nC, dim) numpy array -sig = np.random.rand(mesh.nC, mesh.dim) +sig = rng.random((mesh.nC, mesh.dim)) Me2 = mesh.get_edge_inner_product(sig) Mf2 = mesh.get_face_inner_product(sig) # Anisotropic case: (nC, 3) for 2D and (nC, 6) for 3D -sig = np.random.rand(mesh.nC, 6) +sig = rng.random((mesh.nC, 6)) Me3 = mesh.get_edge_inner_product(sig) Mf3 = mesh.get_face_inner_product(sig) @@ -235,17 +237,17 @@ mesh = TensorMesh([h, h, h]) # Isotropic case: (nC, ) numpy array -sig = np.random.rand(mesh.nC) +sig = rng.random(mesh.nC) Me1_inv = mesh.get_edge_inner_product(sig, invert_matrix=True) Mf1_inv = mesh.get_face_inner_product(sig, invert_matrix=True) # Diagonal anisotropic: (nC, dim) numpy array -sig = np.random.rand(mesh.nC, mesh.dim) +sig = rng.random((mesh.nC, mesh.dim)) Me2_inv = mesh.get_edge_inner_product(sig, invert_matrix=True) Mf2_inv = mesh.get_face_inner_product(sig, invert_matrix=True) # Full anisotropic: (nC, 3) for 2D and (nC, 6) for 3D -sig = np.random.rand(mesh.nC, 6) +sig = rng.random((mesh.nC, 6)) Me3 = mesh.get_edge_inner_product(sig) Mf3 = mesh.get_face_inner_product(sig) diff --git a/tutorials/inner_products/4_advanced.py b/tutorials/inner_products/4_advanced.py index 8f97d1641..e384f5f24 100644 --- a/tutorials/inner_products/4_advanced.py +++ b/tutorials/inner_products/4_advanced.py @@ -21,6 +21,8 @@ import numpy as np import matplotlib.pyplot as plt +rng = np.random.default_rng(4321) + ##################################################### # Constitive Relations and Differential Operators @@ -74,8 +76,8 @@ # Make basic mesh h = np.ones(10) mesh = TensorMesh([h, h, h]) -sig = np.random.rand(mesh.nC) # isotropic -Sig = np.random.rand(mesh.nC, 6) # anisotropic +sig = rng.random(mesh.nC) # isotropic +Sig = rng.random((mesh.nC, 6)) # anisotropic # Inner product matricies Mc = sdiag(mesh.cell_volumes * sig) # Inner product matrix (centers) diff --git a/tutorials/mesh_generation/1_mesh_overview.py b/tutorials/mesh_generation/1_mesh_overview.py index c6d918533..f7bb20428 100644 --- a/tutorials/mesh_generation/1_mesh_overview.py +++ b/tutorials/mesh_generation/1_mesh_overview.py @@ -21,7 +21,7 @@ # # - **Tree meshes** (:class:`discretize.TreeMesh`): also referred to as QuadTree or OcTree meshes # -# - **Curvilinear meshes** (:class:`discretize.CurviMesh`): also referred to as logically rectangular non-orthogonal +# - **Curvilinear meshes** (:class:`discretize.CurvilinearMesh`): also referred to as logically rectangular non-orthogonal # # Examples for each mesh type are shown below. # diff --git a/tutorials/pde/1_poisson.py b/tutorials/pde/1_poisson.py index 17dfc3095..d8665d1b7 100644 --- a/tutorials/pde/1_poisson.py +++ b/tutorials/pde/1_poisson.py @@ -92,7 +92,7 @@ from discretize import TensorMesh -from pymatsolver import SolverLU +from scipy.sparse.linalg import spsolve import matplotlib.pyplot as plt import numpy as np from discretize.utils import sdiag @@ -124,8 +124,7 @@ rho[kpos] = 1 # LU factorization and solve -AinvM = SolverLU(A) -phi = AinvM * rho +phi = spsolve(A, rho) # Compute electric fields E = Mf_inv * DIV.T * Mc * phi diff --git a/tutorials/pde/2_advection_diffusion.py b/tutorials/pde/2_advection_diffusion.py index 8783360db..7cc7bf326 100644 --- a/tutorials/pde/2_advection_diffusion.py +++ b/tutorials/pde/2_advection_diffusion.py @@ -164,7 +164,7 @@ # from discretize import TensorMesh -from pymatsolver import SolverLU +from scipy.sparse.linalg import splu import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np @@ -224,7 +224,7 @@ B = I + dt * M s = Mc_inv * q -Binv = SolverLU(B) +Binv = splu(B) # Plot the vector field @@ -252,7 +252,7 @@ n = 3 for ii in range(300): - p = Binv * (p + s) + p = Binv.solve(p + s) if ii + 1 in (1, 25, 50, 100, 200, 300): ax[n] = fig.add_subplot(3, 3, n + 1)